# Vulnerability Report — FlowiseAI Flowise: Unauthenticated Arbitrary File Read via Document-Store Loader Rehydrate (`getFileFromStorage`) **Status:** NEW / proposed CVE — not covered by any existing CVE (see [§12 Distinction](#12-distinction-from-existing-cves)). **Reporter verification date:** 2026-08-03 **Vendor:** FlowiseAI · **Product:** Flowise (npm `flowise` / `flowise-components`) · **Ecosystem:** npm / Docker **Class:** CWE-22 (Path Traversal) → CWE-200 (Information Exposure); enabled by CWE-915 (Mass Assignment) **Affected:** `flowise` **1.7.1 – 2.2.3** · **Fixed in `flowise@2.2.4`** **Auth required:** None (stock default) · **Attack vector:** Network · **Impact:** Arbitrary file read as root → full credential compromise --- ## 1. Summary Flowise's document-store subsystem re-reads files from local storage when a loader configuration value uses the `FILE-STORAGE::` marker (the "rehydrate" path). The underlying helper `flowise-components/src/storageUtils.ts::getFileFromStorage(file, ...paths)` joins the **`file`** argument onto the storage root **without any sanitization** and returns its bytes: ```js const fileInStorage = path.join(getStoragePath(), ...paths, file) // file operand RAW return fs.readFileSync(fileInStorage) ``` The `file` value is attacker-controlled, and the loader entry that gates the read is forged through a **mass-assignment** on `PUT /api/v1/document-store/store/:id` (`updateDocumentStore` does `Object.assign(entity, req.body)` with no `loaders` allow-list). The file's contents are returned in the `POST /api/v1/document-store/loader/preview` response, so an **unauthenticated** attacker can read **any file the Flowise process can read** — which, in the official Docker image, is **root**. The highest-value target is `/root/.flowise/encryption.key`, the AES key Flowise uses to encrypt all stored credentials (LLM API keys, vector-DB credentials, cloud credentials) in `database.sqlite`. Reading it converts this file-disclosure bug into **full credential compromise**. This is **distinct from CVE-2025-71338** (which is the *write* sink `addSingleFileToStorage(fileName,…)`, fixed in 2.1.0). CVE-2025-71338's patch (`8bd3de41`) never touched `getFileFromStorage`; this read sink remained vulnerable for three more minor releases, until `2.2.4`. --- ## 2. CVSS **CVSS 3.1 Base: 7.5 (High)** — `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` | Metric | Value | Justification | |---|---|---| | Attack Vector | Network | HTTP API | | Attack Complexity | Low | deterministic; no race, no special conditions | | Privileges Required | None | stock deploy admits the forgeable `x-request-from: internal` header (FLOWISE_USERNAME/PASSWORD unset by default) | | User Interaction | None | | | Scope | Unchanged | read stays within the app's OS authority | | Confidentiality | High | reads arbitrary files incl. `encryption.key` / `database.sqlite` | | Integrity | None | pure read, nothing is written | | Availability | None | | **CVSS 4.0 Base ≈ 8.7 (High)** — `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N` > **Prioritization note.** The base score treats this as confidentiality-only, but the practical > impact is amplified: `encryption.key` decrypts every stored third-party credential, so a single > unauthenticated request can lead to downstream compromise of the connected LLM / vector-DB / > cloud accounts. Treat as High/critical for any Flowise instance holding real provider keys. --- ## 3. Root Cause `flowise-components/src/storageUtils.ts::getFileFromStorage` never sanitized its `file` argument (nor the `...paths` directory components). Prior to `2.2.4` the local-storage branch was: ```js export const getFileFromStorage = async (file, ...paths) => { // ... (s3 branch omitted) ... const fileInStorage = path.join(getStoragePath(), ...paths, file) // [SINK] file RAW return fs.readFileSync(fileInStorage) } ``` There is no `basename`, no `sanitize-filename`, no `..` rejection, and no `startsWith(getStoragePath())` confinement check (unlike the sibling `streamStorageFile`, which *does* have those guards). A `file` of `../../../../../../../../root/.flowise/encryption.key` escapes the storage root because `path.join` normalizes the `..` sequences. The reachability gate — the store's `loaders[]` must contain a `files[]` entry whose `name` matches `file` — is defeated by **mass assignment**: `updateDocumentStore` (`PUT /store/:id`) runs `Object.assign(updateDocStore, req.body)` and then `repository.merge().save()`, with no allow-list on `loaders`. The client therefore supplies an arbitrary `loaders[]` array containing the traversal filename. (The DocumentStore-id mass-assignment primitive is separately tracked as CVE-2026-41277, framed there as IDOR; here it is the *enabler* for the file read.) --- ## 4. Vulnerability Path (source → sink) ```text POST /api/v1/document-store/store [unauth via x-request-from: internal] -> createDocumentStore -> returns storeId (server-generated uuid) PUT /api/v1/document-store/store/:storeId [unauth] -> controllers/documentstore::updateDocumentStore const updateDocStore = new DocumentStore(); Object.assign(updateDocStore, req.body) // [MASS ASSIGN] -> services::updateDocumentStore -> repository.merge(store, updateDocStore).save() // body.loaders = [{ id:"L1", files:[{ name:"../../../../../../../../root/.flowise/encryption.key", ... }], ... }] // -> the forged loaders[] (incl. the traversal filename) is persisted on the row POST /api/v1/document-store/loader/preview [unauth] body: { storeId, id:"L1", loaderConfig:{ txtFile:'FILE-STORAGE::[""]' }, preview:true } -> services::previewChunks -> _normalizeFilePaths(data, entity) input.startsWith('FILE-STORAGE::') && loaders.find(id === data.id) matches the forged loader -> getFileFromStorage(file="", 'docustore', entity.id) // [SOURCE -> SINK] const fileInStorage = path.join(getStoragePath(), 'docustore', entity.id, "") return fs.readFileSync(fileInStorage) // [SINK] arbitrary read -> _splitIntoChunks -> chunks[].pageContent = // [returned in HTTP response] ``` Three unauthenticated HTTP requests; the file contents come back in the third response. No file is written to disk at any point. --- ## 5. Impact - **Unauthenticated arbitrary file read** as the Flowise process user (root in the official `flowiseai/flowise` image) — any path readable by that user. - **Full credential compromise (primary consequence):** `/root/.flowise/encryption.key` is the key that decrypts all stored provider credentials in `/root/.flowise/database.sqlite`. Reading both yields every LLM / vector-store / cloud credential the instance holds. - **Secrets & configuration disclosure:** `/etc/passwd`, environment/config files, source, TLS keys, other application data on the same host/container. Demonstrated in this report: byte-exact exfiltration of `encryption.key` and `/etc/passwd`. Not demonstrated (and not claimed): using the recovered key to decrypt `database.sqlite` offline (straightforward follow-on, out of scope for a read-primitive PoC). --- ## 6. Affected Versions (verified) | Version | `getFileFromStorage` `file` operand | Read chain | Basis | |---|---|---|---| | 1.7.1 | unsanitized | affected | first published release with the document-store rehydrate path (`FILE-STORAGE::` → `getFileFromStorage` in the docstore service); `flowise@1.7.0` was **never published to npm** | | 2.0.7 | `path.join(…, file)` (raw) | affected | npm artifact | | **2.1.0** | raw | **affected — confirmed live** | live PoC: `encryption.key` exfil, byte-exact | | **2.2.3** | raw | **affected — confirmed live (upper bound)** | live PoC: `encryption.key` exfil, byte-exact | | **2.2.4** | `_sanitizeFilename(file)` | **fixed — confirmed live** | live PoC: NOT-EXPLOITABLE | | 3.x | sanitized + provider refactor + JWT auth (no header bypass) | not affected | prior analysis | **Affected: 1.7.1 – 2.2.3. Fixed: 2.2.4.** (`flowise@1.7.0` was tagged upstream but never published to npm; 1.7.1 is the earliest installable affected release.) Boundary verified against the published npm artifacts: `flowise-components@2.2.3` = `path.join(getStoragePath(), ...paths, file)`; `flowise-components@2.2.4` = `const sanitizedFilename = _sanitizeFilename(file); path.join(getStoragePath(), ...paths, sanitizedFilename)`. --- ## 7. Proof of Concept `poc.py` — Python 3 standard library only; pure network; **self-proving** (the exfiltrated bytes are returned in-band, so no listener or target-side access is needed). ```bash # read the credential-decryption key (default): python3 poc.py http://TARGET:PORT --read-path /root/.flowise/encryption.key # read any file: python3 poc.py http://TARGET:PORT --read-path /etc/passwd # compare to ground truth: docker exec cat /root/.flowise/encryption.key ``` The PoC (1) creates a store, (2) forges `loaders[].files[]` via the `PUT /store/:id` mass assignment, (3) triggers the `FILE-STORAGE::` rehydrate read and prints the file contents from the `/loader/preview` response. It reports `not_exploitable` (rather than faking success) against fixed targets (`>= 2.2.4`). --- ## 8. Live Verification (2026-08-03) Against stock vendor images `flowiseai/flowise:`, zero `-e` flags (category-A defaults only). Full transcript: `evidence.txt`. | Target | Result | encryption.key vs `docker exec cat` | |---|---|---| | **2.1.0** (affected) | EXFILTRATED 32 bytes | **BYTE-EXACT MATCH** (`0SXPAZucOV4Tb7JmAk9xfXiAq76Ey+ZC`) | | **2.2.3** (affected, upper bound) | EXFILTRATED 32 bytes | **BYTE-EXACT MATCH** (`bHAVi9Dcmldma467Su8aNpBBJlhPDXdT`) | | **2.2.4** (fixed) | `not_exploitable=true` | no exfil (read confined) — correct | The 2.2.3 → 2.2.4 pair is a clean exploit/neutralized boundary demonstrating the fix. --- ## 9. Solution / Remediation - **Upgrade to `flowise >= 2.2.4`** — `getFileFromStorage` sanitizes the `file` operand (`_sanitizeFilename`). Recommended target: current `3.x`, which also refactors storage behind a provider with per-segment sanitization and removes the `x-request-from: internal` auth bypass, and (via CVE-2026-41277's fix in 3.1.0) removes the store-id/loaders mass-assignment enabler. - **Defense in depth:** run Flowise as a non-root, least-privilege user on a read-only root filesystem; keep `encryption.key` / `database.sqlite` off any path the process can traverse to; set `FLOWISE_USERNAME`/`FLOWISE_PASSWORD` so `/api/v1` is not reachable via the forgeable header. **Suggested code fix (for the sink itself, matching the 2.2.4 approach):** sanitize the `file` argument with `sanitize-filename` (strip separators, `..`, and leading dots) before `path.join`, and confine the resolved path with `if (!fileInStorage.startsWith(getStoragePath())) throw` — the guard the sibling `streamStorageFile` already had. --- ## 10. Disclosure Notes - Reproduced only against local Docker labs running the unmodified vendor image; no third-party or internet-facing instance was tested. - The vulnerability is already **fixed in supported releases (>= 2.2.4)**; this report documents a historically-unassigned issue for completeness/traceability, not a live 0-day. ## 11. References - Fixed artifact: npm `flowise-components@2.2.4` (`getFileFromStorage` adds `_sanitizeFilename`) - Vendor repo: https://github.com/FlowiseAI/Flowise - Related (write sink, separate): CVE-2025-71338 · GHSA-8vvx-qvq9-5948 (`c2b830f2`) - Related (mass-assignment enabler, separate): CVE-2026-41277 · GHSA-3prp-9gf7-4rxx - Related (`streamStorageFile` reads, separate, fixed 3.0.6): CVE-2025-71324 · GHSA-4pwq-xw7j-m297; CVE-2025-71334 · GHSA-w5r9-j49j-2m55 ## 12. Distinction from existing CVEs This read primitive is **not** covered by any assigned CVE: | CVE | What it is | Why it is NOT this bug | |---|---|---| | **CVE-2025-71338** | `fileName` **write** sink `addSingleFileToStorage` on `/api/v1/document-store/loader/process` → RCE, fixed 2.1.0 (GHSA-c3hj-m5hq-59xw) | same subsystem, opposite operation (write, not read); its fix didn't touch `getFileFromStorage` | | **CVE-2025-71324** | unauth arbitrary file read via the `chatId` param → `streamStorageFile()` on `/api/v1/get-upload-file` and `/api/v1/openai-assistants-file/download`, fixed **3.0.6** (GHSA-4pwq-xw7j-m297) | closest neighbour: also unauth read, also CVSS 8.7, also targets `/root/.flowise/`. Different function, endpoints and parameter. `streamStorageFile` *has* a storage-root containment check — 71324 defeats it via a fallback lookup path evaluated *after* the check; `getFileFromStorage` has no such check at all. See the fix-version argument below. | | **CVE-2025-71334** | `chatflowId`/`chatId` → `streamStorageFile()` + `addBase64FilesToStorage()` on `/api/v1/chatflows`, "2.2.8 and earlier", fixed **3.0.6** (GHSA-w5r9-j49j-2m55) | version range overlaps this one, but the sinks are unrelated to the document-store rehydrate path; same fix-version argument as 71324 | | **CVE-2025-71333** | unauthenticated arbitrary file **upload**, through 2.2.4 (GHSA-grch-cc26-w2fv) | write primitive, not a read | | **CVE-2024-36420** | arbitrary read via `/api/v1/openai-assistants-file` `fileName` param, v1.4.3 | different endpoint and different sink | | **CVE-2025-61913** | `ReadFileTool`/`WriteFileTool` agent tools read/write any path, < 3.0.8 | different feature (LLM agent tools), not the document-store storage helper | | **CVE-2026-41277** | DocumentStore id **mass assignment** (IDOR), < 3.1.0 | that is the *enabler* used here, framed as object-takeover; it does not describe the `getFileFromStorage` file-read consequence | ### 12.1 Decisive argument: fix versions diverge The two nearest CVEs (71324, 71334) were **fixed in 3.0.6**. This bug was **fixed in 2.2.4** — verified live, not inferred. At 2.2.4 the PoC here reports `not_exploitable` while 71324/71334 remained fully exploitable for four further releases (2.2.4 … 3.0.5). **Two defects with different fix versions cannot be the same defect.** The divergence is visible in the published npm artifacts: `flowise-components@2.2.4` adds `_sanitizeFilename` to `getFileFromStorage` and changes nothing in `streamStorageFile`, whose containment-check bug was not addressed until 3.0.6. ### 12.2 Advisory-database sweep (2026-08-03) The GitHub Advisory Database was searched for `getFileFromStorage` (**0 results**), `flowise document-store` (7 results), and `flowise arbitrary file read` (22 results). No advisory in the database names `getFileFromStorage`, `storageUtils.ts`, the `FILE-STORAGE::` marker, or `/api/v1/document-store/loader/preview` as a read primitive. The GHSA-only upstream entries GHSA-99pg-hqvx-r4gf ("Arbitrary File Read") and GHSA-q67q-549q-p849 ("arbitrary file access due to missing chat flow id validation") are the upstream advisories for CVE-2025-71324 and CVE-2025-71334 respectively, both already distinguished above. ### 12.3 Conclusion The unauthenticated arbitrary **file read via the document-store `FILE-STORAGE::` rehydrate path into `getFileFromStorage`**, affected 1.7.1–2.2.3 and fixed in 2.2.4, therefore warrants its own CVE identifier.