Skip to content

Path-containment prefix guard applied inconsistently — serve_workspace_file and workspace seed writer omit the os.sep boundary check present in the write/delete endpoints #136

Description

@rajarshidattapy

Body

Severity

Low — CVSS v3.1 Base Score: 3.7 — AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N

Note: Exploitation requires the per-install token and a sibling workspace whose ID is a string prefix of the target workspace ID. This is not exploitable under the current UUID-based workspace identifiers, but the affected code is inconsistent with sibling endpoints that already implement the stronger guard and could become exploitable under a future identifier scheme.


CWE Classification

CWE ID Name
CWE-22 Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")
CWE-706 Use of Incorrectly-Resolved Name or Reference

Affected Components

File Endpoint / Function Line Guard
backend/apps/outputs/outputs.py serve_workspace_file()GET /workspace/{id}/serve/{filepath:path} L81 ❌ Missing + os.sep
backend/apps/outputs/outputs.py Workspace seed writer (create/seed path) L368 ❌ Missing + os.sep
backend/apps/outputs/outputs.py write_workspace_file()PUT /workspace/{id}/file/{filepath:path} L531 ✅ Uses + os.sep
backend/apps/outputs/outputs.py delete_workspace_file()DELETE /workspace/{id}/file/{filepath:path} L550 ✅ Uses + os.sep

Vulnerability Description

The workspace path validation compares the resolved target path against the workspace directory using a bare string prefix.

# serve_workspace_file (L80–82)

folder = os.path.join(WORKSPACE_DIR, workspace_id)
full_path = os.path.normpath(os.path.join(folder, filepath))

if not full_path.startswith(os.path.normpath(folder)):
    raise HTTPException(status_code=403, detail="Path traversal not allowed")

Because the validation checks:

startswith(folder)

instead of:

startswith(folder + os.sep)

a sibling directory whose name merely begins with the workspace ID is treated as though it were contained inside that workspace.

For example:

WORKSPACE_DIR/
├── abc/
└── abc-evil/

A request targeting workspace abc can resolve to:

.../WORKSPACE_DIR/abc-evil/...

which still satisfies:

".../abc-evil/...".startswith(".../abc")

even though it is outside the intended workspace boundary.

This is a lexical prefix collision rather than genuine path containment.

The stronger validation already exists in the write/delete endpoints:

folder_norm = os.path.normpath(folder)
full_path = os.path.normpath(os.path.join(folder, filepath))

# startswith(folder_norm + os.sep) prevents
# abc from matching abc-evil

if full_path != folder_norm and not full_path.startswith(folder_norm + os.sep):
    raise HTTPException(status_code=403, detail="Path traversal not allowed")

serve_workspace_file() and the workspace seed writer were never updated to use this stronger form, leaving two of the four workspace-path validation sites with inconsistent protections.


Root Cause Analysis

The current validation relies on:

startswith(folder)

which verifies only a lexical string prefix and does not enforce a directory boundary.

Appending:

+ os.sep

ensures that the next character following the workspace root is a path separator rather than another filename character, preventing collisions such as:

abc
abc-evil

The write and delete endpoints already implement this correction, while the read and seed endpoints continue using the older comparison, creating an inconsistency rather than introducing a new design flaw.


Impact

  • Confidentiality (Low)

    • serve_workspace_file() could expose files belonging to a sibling workspace whose name shares the target workspace ID as a prefix.
  • Integrity (Low)

    • The workspace seed writer could create files inside a sibling workspace whose directory name shares the same prefix.

The impact is limited because exploitation requires:

  • control over a workspace ID that is a prefix of another workspace ID, and
  • the existence of such a sibling workspace.

Current UUID-based workspace identifiers make this condition effectively impossible, so the issue is primarily a forward-looking consistency problem.


Steps To Reproduce

This issue is not reproducible under the current UUID-based workspace identifiers.

Conceptually:

WORKSPACE_DIR/
├── abc/
└── abc-evil/

A request such as:

GET /api/outputs/workspace/abc/serve/../abc-evil/secret.txt

normalizes to:

.../WORKSPACE_DIR/abc-evil/secret.txt

which satisfies:

startswith(.../WORKSPACE_DIR/abc)

allowing the request despite targeting a sibling workspace.


Suggested Remediation

Update both affected sites to use the same containment check already implemented by the write/delete endpoints.

folder_norm = os.path.normpath(folder)
full_path = os.path.normpath(os.path.join(folder, filepath))

if full_path != folder_norm and not full_path.startswith(folder_norm + os.sep):
    raise HTTPException(
        status_code=403,
        detail="Path traversal not allowed",
    )

Apply this change to:

  • outputs.py:81 (serve_workspace_file)
  • outputs.py:368 (workspace seed writer)

Ideally, consolidate all four workspace path checks into a single shared helper (as proposed in the related normpath vs. realpath issue) to eliminate subtle inconsistencies between implementations.


References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions