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.
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
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-15Per-provider findings (verbatim)
Anthropic
claude-opus-4-7
CORS configured with wildcard origin AND credentials, defeating browser protection
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)
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
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
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
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
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
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
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
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
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
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
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
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.