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 traversal

View on GitHub (pinned to 94007c1445)

Solutions

  1. Replace symlinks with real files (or hardlinks/copies) inside workflows/ subdirectories.
  2. Audit workflows/ with: find workflows -type l — every link targeting outside the tree will trip this check.
  3. Keep the containment check intact; it is the last line of defense for the download route.
  4. 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

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

Related errors


AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15). Data as JSON: /api/errors/51abb1d97f4ef1da. Report an issue: GitHub.