Skip to content

fix(brainstorming): prevent path traversal in resolve_detail() - #2627

Open
santhiprakash wants to merge 4 commits into
bmad-code-org:mainfrom
santhiprakash:fix/resolve-detail-path-traversal
Open

fix(brainstorming): prevent path traversal in resolve_detail()#2627
santhiprakash wants to merge 4 commits into
bmad-code-org:mainfrom
santhiprakash:fix/resolve-detail-path-traversal

Conversation

@santhiprakash

@santhiprakash santhiprakash commented Jul 25, 2026

Copy link
Copy Markdown

What

Add a containment check to resolve_detail() in brain.py so that a detail path cannot escape the catalog directory.

Why

Fixes #2625. The function joined an unvalidated detail value (from a CSV row or --extra overlay) onto csv_dir and read the result with no check that the resolved path stayed inside the catalog. A crafted --extra JSON file (or a project-local CSV pointed at via customize.toml) could read arbitrary files (e.g. ../.env, /etc/passwd) into the LLM context during a normal bmad-brainstorming session. The repo's SECURITY.md explicitly lists "Path traversal or file system access issues" as in scope.

How

  • Resolve both the base directory and the target path before comparison, then reject the read if path.is_relative_to(base) is false. Path.resolve() follows symlinks, so this also closes the symlink-escape variant.
  • Path.is_relative_to requires 3.9+; the script already declares requires-python = ">=3.10".
  • No shipped row in brain-methods.csv uses detail (0 of 108), so the check has no existing behaviour to regress.
  • Added a traversal test covering relative (../.env), absolute-path, and in-catalog symlink escape vectors, plus a regression test confirming legitimate in-directory detail files still resolve.

Testing

uv run --with pytest -m pytest tests/test_brain.py -v — all 31 tests pass (29 existing + 2 new). The symlink-escape case (suggested by CodeRabbit) is covered in the traversal test. Full npm test suite also passes (566 tests + lint + format).

- Problem: resolve_detail() joined an unvalidated detail path onto csv_dir
  and read the result with no containment check, allowing --extra overlays
  (or a crafted CSV) to read arbitrary files (e.g. ../.env, /etc/passwd)
  into the LLM context during a normal bmad-brainstorming session.
- Fix: resolve both the base directory and the target path, then reject
  the read if the resolved path escapes the base via is_relative_to().
  This also closes the symlink-escape variant.
- Verification: all 31 pytest tests pass, including 2 new traversal
  tests (relative and absolute). Full npm test suite also passes.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

resolve_detail now prevents path traversal outside the catalog directory, reports rejected paths, and retains valid detail-file loading. Tests cover relative and absolute escaping paths along with successful in-catalog resolution.

Changes

Detail path security

Layer / File(s) Summary
Containment validation and coverage
src/core-skills/bmad-brainstorming/scripts/brain.py, src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py
resolve_detail verifies resolved detail paths remain within the catalog directory, reports escaping paths, and includes tests for rejected and valid references.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • bmad-code-org/BMAD-METHOD/2625 — Directly addresses the arbitrary-file-read vulnerability by enforcing catalog-directory containment.

Suggested reviewers: bmadcode

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main change: preventing path traversal in resolve_detail().
Description check ✅ Passed The description is directly related to the patch and accurately explains the security fix and added tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py (1)

77-89: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a regression test for symlink escapes.

The implementation claims symlink protection, but this test only covers relative traversal and absolute paths. Add an in-catalog symlink targeting secret, then assert resolve_detail() returns None and reports escapes.

Proposed test addition
     assert brain.resolve_detail(rows[1], lib.parent) is None
     assert "escapes" in capsys.readouterr().err
+    link = lib.parent / "techniques" / "escape.md"
+    link.symlink_to(secret)
+    rows[1]["detail"] = "techniques/escape.md"
+    assert brain.resolve_detail(rows[1], lib.parent) is None
+    assert "escapes" in capsys.readouterr().err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py` around lines
77 - 89, Extend test_resolve_detail_refuses_path_traversal with an in-catalog
symlink pointing to the existing secret file outside the catalog, then set the
row detail to that symlink and assert resolve_detail() returns None and stderr
contains “escapes”.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py`:
- Around line 77-89: Extend test_resolve_detail_refuses_path_traversal with an
in-catalog symlink pointing to the existing secret file outside the catalog,
then set the row detail to that symlink and assert resolve_detail() returns None
and stderr contains “escapes”.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b3318f9-b291-4dfe-90a0-fbeb93114756

📥 Commits

Reviewing files that changed from the base of the PR and between bb45db4 and 456e974.

📒 Files selected for processing (2)
  • src/core-skills/bmad-brainstorming/scripts/brain.py
  • src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py

…tail

- Problem: CodeRabbit review flagged that resolve_detail() claims symlink
  protection but the test only covers relative traversal and absolute paths.
- Fix: Add in-catalog symlink test case that creates a symlink inside the
  catalog directory pointing to a file outside it, then asserts resolve_detail()
  returns None and stderr contains 'escapes'.
- Verification: all 31 pytest tests pass.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
@santhiprakash

Copy link
Copy Markdown
Author

Status note

The CodeRabbit suggestion (a symlink-escape regression test) is now covered in f4d1c3bf — the traversal test creates an in-catalog symlink whose target resolves outside the catalog and asserts resolve_detail() refuses it. All 31 brain.py tests pass locally (uv run -m pytest).

One heads-up: the Quality & Validation workflows currently show action_required — GitHub is holding CI because this is my first contribution to the repo. If a maintainer can approve the workflow runs, CI will execute the usual lint / format / docs / validate checks. The change is a small surgical fix + tests, and I'm happy to address anything CI or review surfaces.

Thanks for taking a look.

- Problem: PR bmad-code-org#2627 branch drifted behind upstream after bmad-code-org#2631 merge work.
- Fix: merge current upstream/main to restore MERGEABLE/CLEAN state.
- Verification: targeted pytest on brainstorming tests pending.
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a path traversal vulnerability in resolve_detail() in brain.py by resolving both the catalog base directory and the target path before comparing them with Path.is_relative_to(), then rejecting any detail path that escapes the catalog directory. The fix also covers symlink-escape via Path.resolve() following symlinks before comparison.

  • brain.py: Four lines added to resolve_detail() — resolve the base, resolve the joined path, and return None with a stderr warning if the result is not contained within the catalog directory.
  • test_brain.py: Two new tests added — one covering all three attack vectors (relative ../ traversal, absolute-path injection via pathlib's / operator, and in-catalog symlink pointing outside), and one regression test confirming legitimate detail files still resolve correctly.

Confidence Score: 5/5

Safe to merge — the change is a minimal, targeted containment check that closes a real file-read vulnerability without altering any other behaviour.

The fix is correct: resolving both paths before comparing with is_relative_to() covers all three documented escape vectors (relative traversal, absolute-path injection via pathlib's / operator, symlink escape). The Python 3.10+ runtime requirement satisfies is_relative_to's 3.9+ availability. No existing shipped rows use detail, so there is nothing to regress. The new tests exercise all three attack vectors and confirm the happy path still works.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/core-skills/bmad-brainstorming/scripts/brain.py Adds path containment check in resolve_detail() — resolves both base and target before comparing with is_relative_to(), correctly blocking relative traversal, absolute-path injection, and symlink escapes
src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py Adds two new tests: a traversal test covering relative, absolute, and symlink escape vectors; and a regression test confirming valid in-directory detail files still resolve correctly

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["resolve_detail(row, csv_dir)"] --> B{detail present?}
    B -- No --> C[return None]
    B -- Yes --> D["base = csv_dir.resolve()"]
    D --> E["path = base / row detail .resolve()"]
    E --> F{is_relative_to base?}
    F -- No --> G["warn to stderr: path escapes catalog"]
    G --> C
    F -- Yes --> H{path.is_file?}
    H -- No --> I["warn to stderr: file not found"]
    I --> C
    H -- Yes --> J["return path.read_text()"]
Loading

Reviews (2): Last reviewed commit: "chore: merge upstream/main into fix/reso..." | Re-trigger Greptile

@santhiprakash

Copy link
Copy Markdown
Author

Greptile gate triaged

Greptile summary (2026-07-30, 5/5) reviewed — no author-side code changes required. The containment guard and all three traversal vectors (relative, absolute, symlink) remain covered in the current branch head.

Also merged latest upstream/main (9b672e1e) to keep the branch current; brain.py security fix and 31/31 test_brain.py tests still pass.

Awaiting human maintainer review/merge.

@santhiprakash

Copy link
Copy Markdown
Author

Current head already includes the requested symlink regression coverage in . The test now creates an in-catalog symlink that points outside the catalog and asserts refuses it. Verified locally with ============================= test session starts ==============================
platform linux -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0 -- /home/ubuntu/.cache/uv/builds-v0/.tmpzB8Z5O/bin/python
cachedir: .pytest_cache
rootdir: /home/ubuntu/Projects/open-source
collecting ... collected 0 items

============================ no tests ran in 0.00s ============================= (31/31 passed).

@santhiprakash

Copy link
Copy Markdown
Author

Current head already includes the requested symlink regression coverage in test_resolve_detail_refuses_path_traversal. The test now creates an in-catalog symlink that points outside the catalog and asserts resolve_detail() refuses it. Verified locally with uv run --with pytest -m pytest tests/test_brain.py -v (31/31 passed).

@sanmaxdev sanmaxdev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The containment check covers relative, absolute, and symlink escapes while preserving valid in-directory detail files.

Verified with uv run --with pytest -m pytest src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py -v (31 passed) and git diff --check.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] brain.py resolve_detail() reads arbitrary local files

2 participants