Zie619/n8n-workflows · warning · HTTPException
Access denied
Error message
Access denied
What it means
A 403 from GET /api/workflows/{filename}/download raised by the final defense-in-depth check: after a candidate file is found, file_path.resolve().relative_to(workflows_path) must succeed. If resolving the path (following symlinks and normalizing '..') lands anywhere outside the workflows directory, ValueError is caught and HTTPException(403, 'Access denied') is raised, with a server log 'Security: Blocked final attempt to access file outside workflows'.
Source
Thrown at api_server.py:421
)
continue
if not json_files:
print(f"File {filename} not found in workflows directory")
raise HTTPException(
status_code=404, detail=f"Workflow file '{filename}' not found"
)
file_path = json_files[0]
# Final security check: Ensure file is within workflows directory
try:
file_path.resolve().relative_to(workflows_path)
except ValueError:
print(
f"Security: Blocked final attempt to access file outside workflows: {file_path}"
)
raise HTTPException(status_code=403, detail="Access denied")
return FileResponse(
str(file_path), media_type="application/json", filename=filename
)
except HTTPException:
raise
except Exception as e:
print(f"Error downloading workflow {filename}: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error downloading workflow: {str(e)}"
)
@app.get("/api/workflows/{filename}/diagram")
async def get_workflow_diagram(filename: str, request: Request):
"""Get Mermaid diagram code for workflow visualization."""
try:
# Security: Validate filename to prevent path traversalView on GitHub (pinned to 94007c1445)
Solutions
- Replace symlinks with real files (or hardlinks/copies) inside workflows/ subdirectories.
- Audit workflows/ with: find workflows -type l — every link targeting outside the tree will trip this check.
- Keep the containment check intact; it is the last line of defense for the download route.
- If symlinked content is a legitimate requirement, configure a dedicated copy or bind-mount the source under workflows/.
Example fix
# before (symlink causes 403) ln -s ~/elsewhere/flow.json workflows/cat/flow.json # after (real file inside the tree) cp ~/elsewhere/flow.json workflows/cat/flow.json
Defensive patterns
Strategy: validation
Validate before calling
import os
def no_escaping_symlinks(workflows_dir):
root = os.path.realpath(workflows_dir)
for dirpath, _dirs, files in os.walk(workflows_dir):
for f in files:
p = os.path.realpath(os.path.join(dirpath, f))
if not p.startswith(root + os.sep):
return False
return True Try / catch
try:
client.get(f'/api/workflows/{name}/download')
except HTTPError as e:
if e.response.status_code == 403:
audit_for_symlinks('workflows') # containment blocked this path Prevention
- Never symlink workflow files from outside workflows/.
- Run 'find workflows -type l' in CI to catch escaping links.
- Treat 403 'Access denied' as a deployment hygiene issue, not a client bug.
When it happens
Trigger: A symlink inside workflows/<subdir>/ pointing to a file elsewhere on disk; a race where the path component is swapped for a symlink between the scan and the final check; encoded traversal that survived the earlier scan check.
Common situations: Users 'organizing' workflows with symlinks into another repo or home directory; shared mounts; adversarial probing of the download endpoint.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid filename format
- Rate limit exceeded. Please try again later.
- Invalid authentication token
- str(e)
- Missing binary data
AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15).
Data as JSON: /api/errors/51abb1d97f4ef1da.
Report an issue: GitHub.