# Root Cause Analysis

## Summary

Flowise 3.0.5 exposes the public `GET /api/v1/get-upload-file` endpoint without authentication and passes its attacker-controlled `chatId` query parameter into the local-storage path built by `streamStorageFile`. The function validates `chatflowId` but not `chatId`. In its legacy no-organization fallback, `path.join(storageRoot, chatflowId, chatId, filename)` therefore normalizes `../` segments and can resolve outside the configured storage root. A remote unauthenticated request can consequently retrieve a file from the parent of local storage. This run reproduced the issue twice against real Flowise 3.0.5 HTTP servers and showed that the identical requests are rejected twice by Flowise 3.0.6.

## Impact

- **Affected package/component:** `flowise` / `flowise-components`, specifically the public `get-upload-file` handler and `streamStorageFile` local-storage fallback.
- **Affected version reproduced:** Flowise `3.0.5` (source commit `ba6a602cbe87d9f55c9ee6aebb6407ec2f2066b5`; exact official linux/amd64 image manifest `sha256:30d4fdf8b9e215abff31a67ab104a9750ca25354fe98fe97a3481bbca0352098`). The ticket describes Flowise versions before `3.0.6` as affected.
- **Fixed version tested:** Flowise `3.0.6` (source commit `89a0f23fe5e9c0b1ee85ee1175032c6b9e5ac9c1`; exact official linux/amd64 image manifest `sha256:86b83c5f55cd7989453789a39c568d08885be50e74faf9abd5e238269bcfe489`).
- **Risk:** High/critical confidentiality risk. An unauthenticated network client with a valid chatflow UUID can read files reachable through traversal from the configured local storage hierarchy. In the default layout, this can expose application state such as the SQLite database and its sensitive records. The vulnerable fallback also copies the source into storage and unlinks the original, creating a data-tampering/availability side effect.

## Impact Parity

- **Disclosed/claimed maximum impact:** Pre-auth arbitrary file read/write, information disclosure, and data tampering through Flowise file-storage APIs.
- **Reproduced impact:** Pre-auth arbitrary file read through the real HTTP endpoint. A unique secret was created outside `BLOB_STORAGE_PATH`; an HTTP request sent without cookies, `Authorization`, API key, or `x-request-from` returned that exact secret with status 200. The vulnerable fallback then moved the source file into storage, also demonstrating an unauthorized filesystem mutation.
- **Parity:** `full` for the canonical contract's `info_leak` impact and the unauthenticated API surface.
- **Not demonstrated:** A general attacker-controlled arbitrary-file-write primitive was not needed for the canonical claim and was not claimed as independently proven here. Code execution was neither required nor attempted.

## Root Cause

The endpoint is included in `WHITELIST_URLS`, so Flowise's global API middleware allows requests to `/api/v1/get-upload-file` without authentication. The controller reads `chatflowId`, `chatId`, and `fileName` directly from query parameters, resolves the organization from the referenced chatflow, and calls:

```ts
streamStorageFile(chatflowId, chatId, fileName, orgId)
```

In Flowise 3.0.5, `streamStorageFile` validates that `chatflowId` is a UUID and rejects traversal only in `chatflowId`. It does not apply `isPathTraversal` to `chatId`. The primary local path is checked, but when it does not exist the migration fallback constructs a second path without the organization prefix:

```ts
const fallbackPath = path.join(getStoragePath(), chatflowId, chatId, sanitizedFilename)
```

Because Node's `path.join` normalizes traversal segments, a value such as `chatId=../..` transforms `storageRoot/<chatflow UUID>/../../outside-secret.txt` into a path above `storageRoot`. Critically, this fallback path is not checked with the primary path's absolute/root-containment checks before `existsSync`, `copyFileSync`, `unlinkSync`, and `createReadStream` are used. Filename sanitization cannot constrain traversal supplied through the separate `chatId` component.

Flowise 3.0.6 fixes the reproduced mechanism by extending the early guard to both path components:

```ts
if (isPathTraversal(chatflowId) || isPathTraversal(chatId)) {
    throw new Error('Invalid path characters detected in chatflowId or chatId')
}
```

The version-paired source diff is captured in `bundle/repro/root_cause_source.txt`. The release change is present between commits `ba6a602cbe87d9f55c9ee6aebb6407ec2f2066b5` and `89a0f23fe5e9c0b1ee85ee1175032c6b9e5ac9c1`.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` from any directory. It honors `PRUVA_ROOT` and the prepared project cache described by `bundle/project_cache_context.json`.
2. The script verifies the exact Flowise Git tags/commits and the presence/absence of the fixing hunk. It then downloads and digest-pins the official linux/amd64 Flowise 3.0.5 and 3.0.6 container filesystems, and runs their bundled Node runtimes and real Flowise CLI/server binaries directly.
3. For each of two isolated attempts per version, it starts Flowise with SQLite and local storage, waits for `/api/v1/ping`, performs administrative setup to create a valid chatflow, places a unique secret just outside `BLOB_STORAGE_PATH`, and sends the exploit request with no authentication material:

   ```text
   GET /api/v1/get-upload-file?chatflowId=<valid UUID>&chatId=../..&fileName=outside-secret.txt
   ```

4. Expected evidence:
   - Both 3.0.5 attempts return HTTP 200 and the exact unique outside secret, followed by `VULNERABLE_UNAUTHENTICATED_READ_CONFIRMED`.
   - Both 3.0.6 attempts return HTTP 500 with `Invalid path characters detected in chatflowId or chatId`; the source file remains unchanged, followed by `FIXED_REJECTION_CONFIRMED`.
   - The script exits 0 only after all four checks pass and prints `REPRODUCTION_CONFIRMED`.

## Evidence

- `bundle/logs/reproduction_steps.log` — complete image acquisition, server startup, request/response, and four-attempt verdict transcript.
- `bundle/logs/vulnerable_attempt1_response.txt` and `bundle/logs/vulnerable_attempt2_response.txt` — unique bytes disclosed by the unauthenticated endpoint.
- `bundle/logs/fixed_attempt1_response.txt` and `bundle/logs/fixed_attempt2_response.txt` — fixed-build rejection JSON.
- `bundle/logs/vulnerable_attempt1_service.log`, `bundle/logs/vulnerable_attempt2_service.log`, `bundle/logs/fixed_attempt1_service.log`, and `bundle/logs/fixed_attempt2_service.log` — real Flowise initialization and listening-server evidence.
- `bundle/logs/flowise_3.0.5_image_manifest.json` and `bundle/logs/flowise_3.0.6_image_manifest.json` — exact official linux/amd64 OCI manifest and layer identities.
- `bundle/repro/source_identity.log` — source tags, commits, and image digests.
- `bundle/repro/runtime_manifest.json` — strict runtime manifest with `entrypoint_kind=endpoint` and service, healthcheck, and target-path flags set to true.
- `bundle/repro/root_cause_source.txt` — bounded vulnerable code, fixed diff, whitelist, and controller source evidence.

Representative successful-run excerpts:

```text
unauthenticated_status=200
PRUVA_VULNERABLE_1_<run token>
VULNERABLE_UNAUTHENTICATED_READ_CONFIRMED
```

```text
unauthenticated_status=500
{"statusCode":500,"success":false,"message":"Invalid path characters detected in chatflowId or chatId","stack":{}}
FIXED_REJECTION_CONFIRMED
```

## Recommendations / Next Steps

- Upgrade self-managed Flowise installations to `3.0.6` or later.
- Validate every attacker-controlled path segment (`chatflowId`, `chatId`, organization identifiers, and filenames) before storage-provider use. Prefer strict expected-format validation, such as UUID validation where applicable.
- After constructing both primary and fallback paths, resolve them with `path.resolve` and enforce containment using a separator-aware relative-path check; a simple string prefix is insufficient.
- Remove or tightly constrain legacy fallback/migration behavior on unauthenticated routes. Filesystem moves should not occur as a side effect of a public download request.
- Add integration tests over the real unauthenticated HTTP boundary for encoded and unencoded traversal forms, both download endpoints, all storage providers, and fixed-version fail-closed behavior.
- Return a client error (for example HTTP 400) rather than HTTP 500 for invalid path input to reduce unnecessary internal-error behavior.

## Additional Notes

- **Idempotency:** The final script performs two isolated vulnerable and two isolated fixed attempts in one run, and it is being executed twice consecutively. Per-attempt directories, databases, accounts, chatflows, ports, and secrets are recreated; each run uses a new random token.
- **Authentication boundary:** Registration/login and chatflow creation are setup actions only. The exploit request itself is emitted by a fresh `curl` invocation without a cookie jar or any authentication-related header.
- **Execution mode:** No sanitizer, direct parser harness, mock server, or reimplemented storage function is used. The script invokes the real released Flowise CLI and HTTP server from digest-pinned official product images.
- **Operational detail:** The official image filesystems are streamed and extracted rather than imported into the rootless Docker daemon because that daemon's private layer store was too small for these multi-gigabyte images. This does not change product bytes or runtime behavior: each image's own bundled musl loader, Node executable, Flowise package, dependencies, and CLI are executed.
- **Known precondition:** The read handler requires a valid chatflow UUID. The script creates one through the normal authenticated product API before testing the separate public download boundary.
