Skip to content

fix(UNSAFE_TOOL_OUTPUT_PATH): resolve path traversal before matching system dirs#31

Open
dmchaledev wants to merge 1 commit into
mainfrom
claude/amazing-franklin-or35ez
Open

fix(UNSAFE_TOOL_OUTPUT_PATH): resolve path traversal before matching system dirs#31
dmchaledev wants to merge 1 commit into
mainfrom
claude/amazing-franklin-or35ez

Conversation

@dmchaledev

Copy link
Copy Markdown
Contributor

Summary

A CRITICAL-severity false negative in the UNSAFE_TOOL_OUTPUT_PATH rule: a tool whose outputPath uses path traversal to escape a benign-looking prefix into a sensitive system directory is not flagged, so the scanner reports PASSED on a config that lets a tool write to /etc/passwd.

The rule (src/rules/injection-rules.ts) matched outputPath against the system-directory list by literal prefix:

const isUnsafe = UNSAFE_OUTPUT_DIRS.some((dir) => {
  const p = tool.outputPath!;
  return p === dir || p.startsWith(dir + '/');
});

A traversal payload that starts with a safe-looking prefix never matches, even though it resolves into a system path.

Repro (built CLI at current main)

// trav.json — tool writes "below" /var/app, but escapes to /etc/passwd
{ "transport": { "auth": { "token": "abcdefghijklmnopqrstuvwxyz0123456789abcd" } },
  "rateLimit": { "enabled": true },
  "tools": [ { "name": "writer", "outputPath": "/var/app/../../etc/passwd" } ] }
$ node dist/cli.js trav.json --format=table
Found 0 finding(s): 0 critical, 0 high, 0 medium, 0 low
PASSED

The same bypass applies to deeper payloads (verified against the built CLI):

outputPath resolves to before after
/etc/passwd /etc/passwd ✅ flagged ✅ flagged
/etc/../etc/passwd /etc/passwd ✅ flagged ✅ flagged
/var/app/../../etc/shadow /etc/shadow missed ✅ flagged
/app/data/../../../proc/self/mem /proc/self/mem missed ✅ flagged
/app/tmp/../data/output.json /app/data/output.json ✅ no finding ✅ no finding

For a tool sold as a CI/CD security gate, a write target that lands in /etc or /proc slipping through unflagged is a meaningful bypass — exactly the class of issue this rule exists to catch.

Fix

Normalize the path (POSIX ./.. collapse) before comparing against the system-directory list, so the check judges where the path actually lands rather than its literal prefix. The existing exact-match / prefix-boundary logic (and the #8 false-positive guards for /etcfoo, /rootfs, etc.) is preserved — it now just runs on the resolved path. When traversal changes the path, the finding's description and evidence surface both the literal and the resolved form so the bypass is visible:

$ node dist/cli.js trav.json --format=json | grep evidence
"evidence": "tool: writer, outputPath: \"/var/app/../../etc/passwd\" (resolves to \"/etc/passwd\")"

Traversal that stays inside a safe directory (/app/tmp/../data/output.json/app/data/output.json) still produces no finding — no new false positives.

The change is localized to the UNSAFE_TOOL_OUTPUT_PATH rule; no other rule, the parser, or the report shape is touched. POSIX semantics are used regardless of host OS because these directories (/etc, /proc, …) are POSIX paths.

Verification

  • npm run typecheck ✓ · npm run lint (0 warnings) ✓ · npm test122 passed, 5 suites ✓ · npm run build
  • 4 new regression tests in scanner.test.ts: traversal into /etc and /proc, redundant same-dir traversal (/etc/../etc/passwd), and a safe-directory-traversal no-op.

Why this isn't a duplicate

#8 ("UNSAFE_TOOL_OUTPUT_PATH false positive on prefix boundaries") fixed a false positive on the dir + '/' boundary; this fixes a false negative via traversal — complementary, and the boundary guards remain green. None of the open issues (#11, #19, #26, #27) or open PRs (secrets #28/#29, TLS #18, --rule #17/#25, docs, CI #12/#20) touch output-path normalization.

Acceptance criteria

  • A traversal outputPath that resolves into a system directory produces a CRITICAL finding.
  • Evidence/description name both the literal and resolved path when traversal applies.
  • Traversal that resolves to a safe directory does not fire (no new false positives).
  • Existing exact-match and prefix-boundary behavior is unchanged.
  • Regression tests covering the traversal and safe-directory cases.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TVtRsWmF1TvpgQbZ9AzsrC


Generated by Claude Code

The rule matched outputPath against system directories by literal prefix,
so a traversal payload with a benign-looking prefix bypassed it:

  outputPath: "/var/app/../../etc/passwd"  -> resolves to /etc/passwd

but reported 0 findings / PASSED, since the string starts with "/var".
For a CI security gate this is a false negative that lets a tool write to
a sensitive system path undetected.

Normalize the path (POSIX `.`/`..` collapse) before comparing, so the
check judges where the path actually lands. Evidence and description now
surface both the literal and resolved path when traversal changes it.
Traversal that stays inside a safe directory still passes.

Adds regression tests for traversal into /etc and /proc, redundant
same-dir traversal, and a safe-directory traversal no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TVtRsWmF1TvpgQbZ9AzsrC
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.

2 participants