AntFleet

Retro · n8n-workflows-api-server-2025-08

n8n-workflows api_server.py file download fix

Blind neutral-label scan: Opus and GPT-5 both caught CVE-2025-55526; second-hand AI attribution.

$0incident · 2025-08-15

Outcome A

A neutral-label scan of n8n-workflows' api_server.py fix made the unanimous gate fire on CVE-2025-55526. GPT-5 graded the Windows backslash traversal high/high; Opus found the same path class plus CORS, FTS5, and leakage issues. Attribution is second-hand CSA Lab Space, not a commit trailer.

Methodology

Repo
Zie619/n8n-workflows@0d321e0abaf2
Introducing PR
ee254131c189fb49005aef3edc9a07226eaebf47
Prompt SHA
c2a18bc692d5f2327f1bfc5caaad6d12fbf482be00c8e3c6d41301b9cf53ccda
Pipeline
0.1.0 @ 7e7e21b
Models
claude-opus-4-7, openai-gpt-55
Scanned at
2026-06-10T01:34:54.933Z
On 2025-08-15, the n8n-workflows `api_server.py` file-download fix introduced the surface later tracked as CVE-2025-55526: untrusted workflow filenames were joined into filesystem paths without a containment check. AntFleet re-ran the two-provider retro scan blind against the introducing commit (`ee254131`). The prompt label was deliberately neutral: "n8n-workflows api_server.py file download fix" — no "CVE", no "path traversal", no "directory traversal". Both providers still found the bug class cold. The unanimous gate fired. Claude Opus 4.7 flagged a path-traversal risk in `api_server.py`, citing the same download/detail/diagram path joins and recommending `Path.resolve()` containment. GPT-5 independently graded the issue HIGH/HIGH and named the Windows backslash bypass Opus did not enumerate: `..\README.md` can resolve outside `workflows` on Windows even when FastAPI's default path segment blocks literal forward slashes. That overlap is the conservative unanimous finding in the receipt. The two-model spread also mattered beyond the overlap. Opus produced unique findings for wildcard CORS with credentials, an FTS5 query built from raw filename input, global exception leakage, a Content-Disposition concern, deprecated startup wiring, and category matching bugs. GPT-5 separately called out the unauthenticated cross-origin reindex endpoint, an integrations API contract gap, the category substring bug, and a README path typo. Counting the overlapping path-traversal issue once, the run produced roughly eleven unique issues across both providers. Attribution is intentionally weaker than the Moonwell, OpenClaw, and Zcash Orchard retros. The n8n-workflows introducing commit (`ee254131`) does not carry a public `Co-Authored-By: <AI>` trailer. The "AI-coded" classification is second-hand: CSA Lab Space's March 2026 research note on AI-generated-code vulnerability surge catalogs 74 confirmed AI-generated CVEs, including this case, using attribution methodology that is not public to us: https://labs.cloudsecurityalliance.org/research/csa-research-note-ai-generated-code-vulnerability-surge-2026/. Treat that as a caveat. This receipt's value is the technical result: a blind, neutral-label, two-provider unanimous gate on a real CVE, with the attribution caveat kept explicit.

Reproduce this scan

Requires the AntFleet retro-scan tool — open-source release pending. The prompt SHA and pipeline commit SHA in the methodology block above pin the methodology regardless of when the tool ships.

pnpm exec tsx apps/web/scripts/run-retro-scan.ts \
  --repo Zie619/n8n-workflows --pr-sha ee254131c189fb49005aef3edc9a07226eaebf47 \
  --case-id n8n-workflows-api-server-2025-08 --label "n8n-workflows api_server.py file download fix" \
  --loss-usd 0 --incident-date 2025-08-15

Per-provider findings (verbatim)

Anthropic

claude-opus-4-7

CORS configured with wildcard origin AND credentials, defeating browser protection

medium / highsecurity

api_server.py:28-34

Setting allow_origins=['*'] together with allow_credentials=True is an explicitly insecure pattern per the CORS spec. Starlette's CORSMiddleware will, when credentials are present, reflect the request's Origin header into Access-Control-Allow-Origin (rather than literal '*'), effectively granting any origin the ability to make authenticated cross-origin requests. If/when authentication is added to this API (e.g., cookies, basic auth via a reverse proxy), any third-party site can impersonate the user's session and exfiltrate workflow data via /api/workflows endpoints.

Fix

Either set allow_credentials=False (since the API exposes no credentials today), or replace allow_origins with an explicit list of trusted origins. Do not combine '*' with credentials.

Filename path parameter used in os.path.join without containment check (path traversal risk)

medium / mediumsecurity

api_server.py:206-219

The download/detail/diagram endpoints take {filename} from the URL and concatenate it directly into a filesystem path with no validation that the resolved path lives inside the workflows/ directory and no allow-list against the database. While Starlette path parameters won't capture an un-encoded '/', os.path.join still gives an attacker control over the leaf name (e.g., dotfiles, symlinks placed inside workflows/, or platform-specific quirks like backslashes on Windows) and over the Content-Disposition filename echoed back to the browser. The 'file download fix' commit (ee254131) referenced in the feature title is precisely the area to harden; today there is no Path.resolve() containment check and no validation that the filename was returned by the database before opening it.

Fix

Resolve the candidate path via Path('workflows').resolve() and Path(file_path).resolve(); reject if the resolved path is not a child of the workflows root. Additionally verify the filename exists in the indexed database before opening, and sanitize the filename used in the Content-Disposition header (e.g., os.path.basename + allow-list of characters).

FTS5 query built via f-string with raw filename allows query corruption

medium / highbug

api_server.py:176-180

The filename is interpolated unescaped into an FTS5 query string. A filename containing a double-quote, asterisk, colon, or FTS5 operator will either silently break the lookup (returning 404 even when the workflow exists in the DB) or, depending on how search_workflows constructs the SQL, raise an FTS5 syntax error that the global handler converts into a generic 500. This makes the endpoint unreliable for any filename containing FTS5-special characters and also means a 404 in get_workflow_detail does not actually prove the workflow is missing — it may merely prove the filename couldn't be safely expressed as FTS.

Fix

Look up workflows by filename through a parameterized exact-match SQL query (WHERE filename = ?) rather than through FTS5. If FTS must be used, escape embedded quotes by doubling them and reject characters that are FTS operators.

Global exception handler leaks internal error details

medium / highsecurity

api_server.py:325-330

Every uncaught exception's message is returned to the client verbatim. SQLite/sqlalchemy errors, filesystem errors, and JSON parse errors typically embed absolute paths, table names, and stack-relevant strings — information useful to an attacker mapping the deployment. Several endpoints also propagate str(e) into HTTPException details, compounding the disclosure. This is straightforward to fix and unrelated to legitimate observability needs (logs already capture detail server-side).

Fix

In the global handler and per-endpoint handlers, log the exception with traceback and return a generic 'Internal server error' message without str(exc). Reserve detailed messages for validation errors that are safe to disclose.

Overly aggressive substring matching mis-categorizes many workflows

medium / highbug

create_categories.py:31-41

The fallback partial-match performs a bidirectional substring test (`token in integration or integration in token`) and returns the first hit from dict iteration order. Short or common tokens trivially match the wrong integration: e.g., token 'a' is a substring of 'asana', 'agent', 'paypal', etc.; token 'http' matches but so does 'mq' matching 'mqtt' the wrong way; token 'git' matches 'gitlab' or 'github' non-deterministically; token 'the' matches 'thehive'. Filenames are tokenized to include ID numerics and generic words ('manual', 'scheduled', 'webhook', 'send', 'create'), all of which can substring-match an integration. Because the README documents this script as the source of truth for the category filter shown to end users, the result is silently incorrect labelling propagated to context/search_categories.json and surfaced in the UI.

Fix

Drop the bidirectional substring fallback. Either require exact (case-folded) token equality, or implement a vetted alias map (e.g., 'gdrive'->'GoogleDrive'). Also filter out generic stop-tokens (numeric IDs, 'manual', 'webhook', 'scheduled', 'create', 'send', etc.) before matching.

Filename echoed into Content-Disposition without sanitization

low / mediumsecurity

api_server.py:210-219

FileResponse's `filename` parameter is placed into the Content-Disposition header. Starlette quotes/encodes it, but the value still originates from user-controlled URL input that was never validated to be a real workflow filename. Combined with the lack of containment check above, a request for a non-existent file (or one resolved by symlink) would still echo the attacker-chosen filename to the browser, encouraging users to save attacker-named files. Low severity on its own because Starlette does header-encode the value.

Fix

Use os.path.basename of the database-verified filename for both the disk path and the Content-Disposition filename; reject any filename not present in the workflows table.

@app.on_event('startup') is deprecated in current FastAPI/Starlette

low / mediummaintainability

api_server.py:37-47

`on_event` is deprecated in favor of the lifespan context manager. The handler also raises on database failure (intended), which under the deprecated path still works but emits deprecation warnings and may be removed in future Starlette versions, breaking startup health checks silently.

Fix

Migrate to a lifespan async context manager passed to FastAPI(lifespan=...).

OpenAI

openai-gpt-55

Workflow download joins untrusted filename paths, enabling Windows path traversal

high / highsecurity

api_server.py:225-237

The route parameter is used directly in filesystem paths. FastAPI's default path segment prevents literal forward slashes, but it does not reject backslashes or Windows absolute paths. On Windows, backslash is a path separator and a value such as `..\README.md` resolves outside `workflows`; an absolute value such as `C:\Windows\win.ini` can also discard the intended base path. The download endpoint then serves the file with FileResponse without checking that the resolved path is still inside the workflows directory or that it is a workflow JSON file.

Fix

Resolve paths against a fixed base directory and enforce containment before opening or serving files. Reject empty names, `..`, absolute paths, and both `/` and `\` separators; require `.json` and `is_file()`. For example, resolve `(base / filename)` and ensure `candidate.relative_to(base)` succeeds before returning FileResponse. Prefer also checking that the filename exists in the workflow database.

Unauthenticated reindex endpoint can be triggered cross-origin

medium / highsecurity

api_server.py:31-37

`POST /api/reindex` starts a database indexing job, optionally forced, without authentication, authorization, CSRF protection, or rate limiting. Because CORS permits all origins and methods, any website can issue the request from a browser that can reach the server, causing repeated CPU/disk-intensive background work and database writes. This may be an intentional local-admin convenience endpoint, so severity is capped pending policy confirmation.

Fix

Require an admin token or other authentication for reindexing, disable the endpoint by default in production, and add rate limiting or a single-flight lock so repeated requests cannot enqueue overlapping indexing jobs. If the endpoint is intentionally public, document that policy explicitly and constrain deployment to localhost-only.

Integrations endpoint advertises a list but always returns an empty list

medium / highapi-contract

api_server.py:352-358

The endpoint docstring and README promise integration data, and the database statistics include a unique integration count, but the handler hard-codes `integrations` to an empty array. API consumers cannot obtain the advertised integration list/statistics, and the response is internally inconsistent when `count` is nonzero.

Fix

Implement the endpoint using database-backed integration aggregation, returning the actual integration names and counts. If detailed integrations are not supported, change the endpoint name/docs and response model so clients do not rely on a missing list.

Category generator misclassifies short tokens through unrestricted substring matching

medium / highbug

create_categories.py:39-43

After exact matching fails, any substring match in either direction is accepted. This is too broad for filename tokens: a token like `AI` becomes `ai`, matches the substring `ai` inside `activecampaign`, and is categorized as `Marketing & Advertising Automation` instead of remaining uncategorized or being mapped deliberately to an AI category. Similar false positives can occur with other short/common tokens, polluting `search_categories.json` and making category filters unreliable.

Fix

Replace unrestricted substring matching with explicit aliases and safer normalization. At minimum, ignore very short tokens for partial matching, prefer longest/word-boundary matches, handle camel-case/service aliases intentionally, and add a specific mapping for common tokens such as `ai` if desired.

README points contributors to a non-existent category definitions file

low / highdocs-gap

README.md:108-110

The contributor instructions use `context/defs_categories.json` with an extra `s`, but the script reads `context/def_categories.json` and the owned file is named that way. Following the README would cause contributors to edit or create the wrong file, so their mappings would not be used.

Fix

Change the README path to `context/def_categories.json` everywhere category definitions are referenced.

Unanimous gate (intersection)

Workflow download joins untrusted filename paths, enabling Windows path traversal

high / highsecurity

api_server.py:225-237

The route parameter is used directly in filesystem paths. FastAPI's default path segment prevents literal forward slashes, but it does not reject backslashes or Windows absolute paths. On Windows, backslash is a path separator and a value such as `..\README.md` resolves outside `workflows`; an absolute value such as `C:\Windows\win.ini` can also discard the intended base path. The download endpoint then serves the file with FileResponse without checking that the resolved path is still inside the workflows directory or that it is a workflow JSON file.

Fix

Resolve paths against a fixed base directory and enforce containment before opening or serving files. Reject empty names, `..`, absolute paths, and both `/` and `\` separators; require `.json` and `is_file()`. For example, resolve `(base / filename)` and ensure `candidate.relative_to(base)` succeeds before returning FileResponse. Prefer also checking that the filename exists in the workflow database.