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:
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:
which verifies only a lexical string prefix and does not enforce a directory boundary.
Appending:
ensures that the next character following the workspace root is a path separator rather than another filename character, preventing collisions such as:
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
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
CWE Classification
Affected Components
backend/apps/outputs/outputs.pyserve_workspace_file()—GET /workspace/{id}/serve/{filepath:path}+ os.sepbackend/apps/outputs/outputs.py+ os.sepbackend/apps/outputs/outputs.pywrite_workspace_file()—PUT /workspace/{id}/file/{filepath:path}+ os.sepbackend/apps/outputs/outputs.pydelete_workspace_file()—DELETE /workspace/{id}/file/{filepath:path}+ os.sepVulnerability Description
The workspace path validation compares the resolved target path against the workspace directory using a bare string prefix.
Because the validation checks:
instead of:
a sibling directory whose name merely begins with the workspace ID is treated as though it were contained inside that workspace.
For example:
A request targeting workspace
abccan resolve to:which still satisfies:
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:
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:
which verifies only a lexical string prefix and does not enforce a directory boundary.
Appending:
ensures that the next character following the workspace root is a path separator rather than another filename character, preventing collisions such as:
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 impact is limited because exploitation requires:
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:
A request such as:
normalizes to:
which satisfies:
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.
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
normpathvs.realpathissue) to eliminate subtle inconsistencies between implementations.References
backend/apps/outputs/outputs.py(write_workspace_file, L530–531)