fix(security): normalize changed-file paths + recurse D20 lint into subpackages #93

Merged
pdurlej merged 1 commit from claude/security-hardening-wave into main 2026-06-17 23:29:51 +02:00
Collaborator

What

Two defense-in-depth hardening fixes surfaced by the 2026-06-16 opportunity audit (security-governance finder). Both are M2-permitted (D21 defense hardening), stdlib-only, with regression tests.

1. Path-traversal bypass in classify_files (pr_check.py)

classify_files matched policy prefixes against raw paths via startswith. A changed-files entry like docs/../secrets/.env would startswith("docs/") and classify as safe_docs_status (eligible_clean) instead of secrets — bypassing the manual gate.

  • Reachability (honest scope): git normalizes tree paths, so this is not reachable through a normal Forgejo diff. It is reachable via the offline-metadata pipeline (#27), which accepts hand-crafted changed_files. So: real defense-in-depth gap, not a live RCE.
  • Fix: posixpath.normpath each path before classification → it's classified by its true target (docs/../secrets/.envsecrets/.envsecrets). Any path that escapes the repo root (absolute, or .. after normalization) fails closed to unknown (a sensitive class).

2. D20 boundary lint was flat (test_d20_architectural_boundary.py)

_src_module_paths used SRC_DIR.glob("*.py")top-level only. A future subpackage (e.g. src/patchwarden/adapters/forgejo.py) calling a merge endpoint would slip past the build-time D20 check — the exact Swiss-Cheese gap the lint exists to close.

  • Fix: rglob (recursive), parametrized root, plus a recursion self-test that proves a nested merge_pull_request( call is caught and __init__.py stays excluded.

Tests

  • +7 (6 path-traversal/normalization in test_pr_check, 1 recursion in test_d20).
  • Full suite 303 green (PYTHONPATH=src python3 -m unittest discover tests).

Deliberately NOT included (needs your design call — reported separately)

effective_cloud_review() is defined + unit-tested but never called in the live review_run._findings_from_ollama dispatch path. A sensitive-classification PR could therefore have its diff sent to a cloud Ollama endpoint (OLLAMA_BASE_URL). The fix needs a design decision on the enforcement layer (review_run vs cli vs a dedicated guard), so I did not bundle a possibly-wrong-layer change here.

From the 2026-06-16 opportunity audit. Do not merge — operator merge only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## What Two **defense-in-depth** hardening fixes surfaced by the 2026-06-16 opportunity audit (security-governance finder). Both are M2-permitted (**D21 defense hardening**), stdlib-only, with regression tests. ### 1. Path-traversal bypass in `classify_files` (`pr_check.py`) `classify_files` matched policy prefixes against **raw** paths via `startswith`. A changed-files entry like `docs/../secrets/.env` would `startswith("docs/")` and classify as **`safe_docs_status` (eligible_clean)** instead of `secrets` — bypassing the manual gate. - **Reachability (honest scope):** git normalizes tree paths, so this is **not** reachable through a normal Forgejo diff. It **is** reachable via the offline-metadata pipeline (#27), which accepts hand-crafted `changed_files`. So: real defense-in-depth gap, not a live RCE. - **Fix:** `posixpath.normpath` each path before classification → it's classified by its **true target** (`docs/../secrets/.env` → `secrets/.env` → `secrets`). Any path that **escapes the repo root** (absolute, or `..` after normalization) **fails closed to `unknown`** (a sensitive class). ### 2. D20 boundary lint was flat (`test_d20_architectural_boundary.py`) `_src_module_paths` used `SRC_DIR.glob("*.py")` — **top-level only**. A future subpackage (e.g. `src/patchwarden/adapters/forgejo.py`) calling a merge endpoint would slip past the build-time D20 check — the exact Swiss-Cheese gap the lint exists to close. - **Fix:** `rglob` (recursive), parametrized `root`, plus a recursion **self-test** that proves a nested `merge_pull_request(` call is caught and `__init__.py` stays excluded. ## Tests - **+7** (6 path-traversal/normalization in `test_pr_check`, 1 recursion in `test_d20`). - Full suite **303 green** (`PYTHONPATH=src python3 -m unittest discover tests`). ## Deliberately NOT included (needs your design call — reported separately) `effective_cloud_review()` is **defined + unit-tested but never called** in the live `review_run._findings_from_ollama` dispatch path. A sensitive-classification PR could therefore have its diff sent to a cloud Ollama endpoint (`OLLAMA_BASE_URL`). The fix needs a **design decision on the enforcement layer** (review_run vs cli vs a dedicated guard), so I did not bundle a possibly-wrong-layer change here. From the 2026-06-16 opportunity audit. **Do not merge — operator merge only.** Co-Authored-By: Claude Opus 4.8 (1M context) &lt;noreply@anthropic.com&gt;
fix(security): normalize changed-file paths + recurse D20 lint into subpackages
All checks were successful
fallow-py / fallow-py-advisory (pull_request) Successful in 14s
8ab52d020a
Two defense-in-depth fixes from the 2026-06-16 opportunity audit (security-governance
finder), both M2-permitted (D21 defense hardening), stdlib-only.

1. Path-traversal in classify_files (pr_check.py)
   classify_files matched prefixes against raw paths, so a changed-files entry like
   "docs/../secrets/.env" would startswith("docs/") and classify as safe_docs_status
   (eligible_clean) instead of secrets. git normalizes tree paths so this is not
   reachable via a normal Forgejo diff, but the offline metadata pipeline (#27)
   accepts hand-crafted changed_files where it is. Fix: posixpath.normpath each path
   before classification (so it is classified by its true target —
   "docs/../secrets/.env" -> "secrets/.env" -> secrets), and fail closed to "unknown"
   on any path that escapes the repo root (absolute, or ".." after normalization).

2. D20 boundary lint was flat (test_d20_architectural_boundary.py)
   _src_module_paths used SRC_DIR.glob("*.py") — top-level only. A future subpackage
   (e.g. src/patchwarden/adapters/forgejo.py) calling a merge endpoint would slip past
   the build-time D20 check. Fix: rglob, parametrized root, plus a recursion self-test
   proving a nested merge call is caught.

Tests: +7 (6 path-traversal/normalization in test_pr_check, 1 recursion in test_d20).
Full suite 303 green.

NOT included (needs an operator design call, reported separately): effective_cloud_review
is defined + unit-tested but never called in the live review_run dispatch path, so a
sensitive-classification PR could have its diff sent to a cloud Ollama endpoint. The
right enforcement layer (review_run vs cli vs a guard) is a design decision, not a
drop-in fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
agent/claude-code
agent/codex
agent/gemini
agent/hermes
agent/iskra
agent/ollama
agent/patchwarden
area:business-model
area:competitive
area:discovery
area:forgejo
area:metrics
area:product-strategy
area:v0-core
cagan-grade-approved
client:platform
dependency/blocked
dependency/blocks-others
dependency/cross-repo
dependency/needs-confirmation
domain:agents
domain:ci
domain:docs
domain:forgejo
domain:infra
domain:memory
domain:runtime
domain:signal
domain:ux
flow/architecture
flow/blocked
flow/deployed
flow/done
flow/implementation
flow/intake
flow/maintained
flow/observed
flow/ready
flow/refining
flow/retired
flow/review
judge/codex-candidate
judge/hermes-candidate
judge/low-confidence
judge/needs-refinement
judge/operator-needed
judge/p0
judge/p1
judge/p2
judge/p3
judge/park
judge/patchwarden-candidate
judge/stale-priority
kind/adr
kind/bug
kind/chore
kind/feature
kind/infra
kind/ops
kind/refactor
kind/research
kind:artifact
kind:decision
kind:dogfood
kind:epic
kind:implementation
kind:research
merge/auto
merge/manual
merge/manual-dependency-conflict
merge/manual-failing-tests
merge/manual-merge-conflict
merge/manual-missing-review
merge/manual-operator-preference
merge/manual-red-zone
merge/manual-security-sensitive
merge/manual-unclear-scope
merge/manual-unknown
mode:operator-only
mode:patchwarden-iskra-approved
mode:safe-auto
observed/erroring
observed/needs-followup
observed/pending
observed/retire-candidate
observed/unused
observed/used
priority:p0
priority:p1
priority:p2
priority:p3
ready-for-agent
review:claude-reviewed
review:codex-reviewed
review:dziadek-reviewed
review:needs-human
safety:external-write
safety:no-prod-mutation
safety:prod-impact
safety:secret-touch
size/large
size/medium
size/small
size/tiny
size/unknown
source/adr
source/agent-generated
source/manual
source/operator-chat
source/voice-note
status:blocked
status:blocked-on-discovery
status:cagan-grade-review-pending
status:codex-ready
status:merged:pending-evidence
status:needs-evidence
status:needs-operator-decision
status:operator-needed
status:parked
tier:0-anchor
tier:0-platform-substrate
tier:1-core
tier:1-iskra-value-layer
tier:2-supporting
tier:2-tools-products-modules
type:bug
type:chore
type:docs
type:feat
type:policy
type:research
wave:1-foundation
wave:2-positioning
wave:3-validation
wave:4-economics
wave:5-operating
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pdurlej/patchwarden!93
No description provided.