Skip to content

Maintainer RCE risk in docs formatter due to python -m ruff (module shadowing) #150

@0x-Professor

Description

@0x-Professor

Summary
A contributor can run arbitrary code on a maintainer’s machine or CI by abusing Python module shadowing in the docs formatter. The script calls Ruff using python -m ruff from the repository root. If a PR adds a file named ruff.py (or ruff/init.py) at the repo root, that file is imported and executed instead of the real Ruff module/binary.

Impact/Severity
High. This can lead to arbitrary code execution with maintainer privileges when running the docs formatting task locally or in CI.

Where it happens

  • scripts/utils/ruffen-docs.py calls:
    • python -m ruff format ...
  • pyproject.toml exposes a “format:docs” command that runs the script.

Steps to reproduce

  1. In the repository root, add a file named ruff.py with malicious code, for example:
    # ruff.py (malicious)
    import os, sys
    with open("PWNED.txt", "w", encoding="utf-8") as f:
        f.write(f"GITHUB_TOKEN={os.environ.get('GITHUB_TOKEN','<none>')}\n")
    print("Malicious ruff.py executed!", file=sys.stderr)
  2. Run the docs formatter:
    • rye run format:docs
    • or: python scripts/utils/ruffen-docs.py README.md api.md
  3. Observe that PWNED.txt is created and the malicious print appears, confirming arbitrary code execution.

Actual behavior
The formatter imports and runs ruff from the current repo if a file named ruff.py (or package ruff/) exists in the root, executing untrusted code.

Expected behavior
The formatter should not import any Python module named ruff from the repo. It should run Ruff safely without allowing local module shadowing.

Proposed fix

  • Do not use python -m ruff.
  • Call the Ruff binary directly (found via PATH) using shutil.which("ruff"), e.g.:
    • [ruff_binary, "format", "--stdin-filename=script.py", f"--line-length={DEFAULT_LINE_LENGTH}"]
  • Optionally run the subprocess with a safe working directory (e.g., a temp directory) to avoid CWD side effects.
  • If calling through Python is required, sanitize sys.path (remove "") or use isolated mode, but invoking the Ruff binary is simpler and safer.

Environment

  • Affected when running the docs formatter locally or in CI where the working directory is the repo root and where Ruff is invoked via python -m ruff.

Additional context
This is a classic Python module shadowing issue. Ruff is typically a Rust binary; invoking the binary directly avoids Python import path risks. The minimal change is to replace sys.executable -m ruff with the Ruff executable and keep the same arguments.

Metadata

Metadata

Assignees

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