Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Arbitrary file write via unsanitised need ID in the `needs_id` builder (path traversal)

Open
#2,190 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
68/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
security

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

bug pkg: sphinx-needs
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 .json config files consumed by downstream tooling
  • The written content (title, content, status, tags, links) is fully attacker-controlled
  • The forced .json suffix prevents direct script overwrite, but on Windows NTFS alternate data streams apply (id = "victim:stream" writes victim: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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from useblocks/sphinx-needs

All issues in useblocks/sphinx-needs

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.