Skip to content

fix(tools): add executable path validation to prevent directory traversal - #603

Open
a-oren wants to merge 6 commits into
guacsec:mainfrom
a-oren:TC-5485
Open

fix(tools): add executable path validation to prevent directory traversal#603
a-oren wants to merge 6 commits into
guacsec:mainfrom
a-oren:TC-5485

Conversation

@a-oren

@a-oren a-oren commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds defense-in-depth path validation for CVE-2026-18389. The new validateExecutablePath() function in src/tools.js rejects user-configured executable paths that contain .. traversal segments or ./ relative prefixes before they reach execFileSync(). Bare command names and valid absolute paths are allowed. Wrapper paths resolved by traverseForWrapper() are intentionally unaffected — they are protected by Workspace Trust gating in the VS Code extension.

  • TC-5619 fix: Removed early-return optimization that allowed bare .. (without path separators) to bypass validation. .. is now correctly caught by the segment check.

Changes

  • src/tools.js: Added validateExecutablePath(binPath) function and integrated it into getCustomPath() as the validation choke point. Removed early-return optimization that bypassed validation for bare ...
  • test/tools.test.js: Added 9 tests covering bare names, absolute paths, traversal rejection, ./ rejection, bare .. rejection, opts-based paths, error messages, and wrapper path regression.

Test plan

  • npm test — 576 passing (9 new), 17 failing (pre-existing, poetry not installed)
  • npm run lint — 0 errors
  • Wrapper path regression test confirms resolveBinary()traverseForWrapper() bypasses validation
  • Bare .. is now correctly rejected

Implements TC-5485
Implements TC-5619

…rsal

Adds validateExecutablePath() to reject user-configured executable paths
containing '..' traversal segments or './' relative prefixes before they
reach execFileSync(). Bare command names and absolute paths are allowed.
Wrapper paths from traverseForWrapper() are unaffected.

Implements TC-5485

Assisted-by: Claude Code
@sourcery-ai

sourcery-ai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds centralized executable path validation to getCustomPath() to block directory traversal and relative ./ paths, and backs it with targeted tests including a regression test ensuring wrapper resolution remains unaffected.

File-Level Changes

Change Details Files
Add centralized executable path validation for custom binary paths and integrate it into getCustomPath().
  • Introduced validateExecutablePath(binPath) to reject paths with directory traversal segments ('..') or leading './' or '.' while allowing bare command names and normal absolute paths.
  • Updated getCustomPath() to route the resolved custom path through validateExecutablePath() before returning it, ensuring all env/opts-driven custom paths are vetted.
src/tools.js
Extend tools tests to cover executable path validation behavior and wrapper path regression.
  • Imported getCustomPath alongside getCustom in tools tests to exercise the new validation logic.
  • Added a dedicated test suite validating allowed bare commands and absolute paths, and rejecting traversal paths, relative './' paths, opts-provided traversal paths, and verifying that error messages include the offending path.
  • Added a regression test suite confirming resolveBinary() returns wrapper paths discovered by traverseForWrapper() without passing them through getCustomPath() validation, using esmock to stub fs access.
test/tools.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="test/tools.test.js" line_range="105" />
<code_context>
+			)
+		})
+
+		/** Verifies that paths starting with "./" (workspace-relative) are rejected. */
+		test('rejects paths starting with "./"', () => {
+			process.env['TRUSTIFY_DA_DUMMY_PATH'] = './malicious.sh'
</code_context>
<issue_to_address>
**question (testing):** Consider adding a positive test for allowed relative paths without './' to document intended behavior

The validation now rejects `./`-prefixed paths but still permits other relative paths without `./` or `..` (e.g. `bin/mvn`, `tools/mvnw`). If that’s the desired behavior, please add a test confirming that `getCustomPath` accepts these paths so the distinction is documented and future changes don’t accidentally alter it.
</issue_to_address>

### Comment 2
<location path="test/tools.test.js" line_range="113-119" />
<code_context>
+			)
+		})
+
+		/** Verifies that traversal paths supplied via opts are also rejected. */
+		test('rejects traversal paths provided via opts', () => {
+			const opts = { 'TRUSTIFY_DA_DUMMY_PATH': '../../tmp/evil' }
</code_context>
<issue_to_address>
**suggestion (testing):** Also cover './' rejection when the path is supplied via opts, not only via process.env

You already verify traversal rejection via `opts`. To align coverage with the env-based `'./'` test, please add a similar case using `const opts = { TRUSTIFY_DA_DUMMY_PATH: './malicious.sh' }` and assert that `getCustomPath('dummy', opts)` throws the expected `'./'` error. This confirms both env and opts inputs share the same validation behavior.

```suggestion
		/** Verifies that traversal paths supplied via opts are also rejected. */
		test('rejects traversal paths provided via opts', () => {
			const opts = { 'TRUSTIFY_DA_DUMMY_PATH': '../../tmp/evil' }
			expect(() => getCustomPath('dummy', opts)).to.throw(
				Error, 'path contains directory traversal segment (..)'
			)
		})

		/** Verifies that paths starting with "./" provided via opts are rejected. */
		test('rejects "./" paths provided via opts', () => {
			const opts = { TRUSTIFY_DA_DUMMY_PATH: './malicious.sh' }
			expect(() => getCustomPath('dummy', opts)).to.throw(
				Error, "relative paths starting with './' are not allowed"
			)
		})
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread test/tools.test.js
)
})

/** Verifies that paths starting with "./" (workspace-relative) are rejected. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question (testing): Consider adding a positive test for allowed relative paths without './' to document intended behavior

The validation now rejects ./-prefixed paths but still permits other relative paths without ./ or .. (e.g. bin/mvn, tools/mvnw). If that’s the desired behavior, please add a test confirming that getCustomPath accepts these paths so the distinction is documented and future changes don’t accidentally alter it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as question — asks whether allowing relative paths without ./ or .. (e.g., bin/mvn) is intentional behavior. This is a valid design question for the PR author to clarify, but does not require a code change. No sub-task created.

Comment thread test/tools.test.js
The CI runner has TRUSTIFY_DA_PIP3_PATH set, causing getCustomPath('pip3')
to return the env var value instead of the bare name. Save and restore any
matching env vars during the test to isolate from the CI environment.

Implements TC-5485

Assisted-by: Claude Code
@codecov-commenter

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.28571% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.32%. Comparing base (00529d4) to head (286d1c3).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
src/tools.js 94.28% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #603      +/-   ##
==========================================
+ Coverage   90.95%   91.32%   +0.37%     
==========================================
  Files          41       43       +2     
  Lines        8984     9592     +608     
  Branches     1573     1727     +154     
==========================================
+ Hits         8171     8760     +589     
- Misses        813      832      +19     
Flag Coverage Δ
unit-tests 91.32% <94.28%> (+0.37%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/tools.js 92.35% <94.28%> (+3.27%) ⬆️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@a-oren

a-oren commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-5485 (commit db61fcf)

Check Result Details
Review Feedback PASS 2 comments classified (1 question, 1 suggestion); no code change requests
Root-Cause Investigation N/A No sub-tasks created
Scope Containment PASS 2 files changed: src/tools.js (in-scope), test/tools.test.js (test for modified source)
Diff Size PASS ~122 lines across 2 files
Commit Traceability PASS Both commits reference TC-5485, include Assisted-by trailer
Sensitive Patterns PASS No credentials, secrets, or API keys found
CI Status PASS 5/5 checks pass (lint+test Node 22 & 24, PR title, commit messages, Sourcery)
Acceptance Criteria PASS 8 of 8 criteria met
Test Quality PASS No repetitive tests; all tests documented; Eval Quality: N/A
Test Change Classification ADDITIVE 8 new tests added, none modified or deleted
Verification Commands PASS npm test and npm run lint both pass

Overall: PASS

All checks pass. The implementation adds validateExecutablePath() as a defense-in-depth measure against CVE-2026-18389, correctly integrated at the getCustomPath() choke point. Wrapper paths are architecturally excluded from validation. Test coverage is comprehensive across all acceptance criteria.


This comment was AI-generated by sdlc-workflow/verify-pr v0.13.2.

@a-oren
a-oren requested review from Strum355 and ruromero August 6, 2026 06:45
ruromero

This comment was marked as outdated.

@ruromero ruromero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Security review of the path traversal fix. Found a confirmed bypass where bare .. evades validation, plus minor issues.

Comment thread src/tools.js Outdated
*/
function validateExecutablePath(binPath) {
if (!binPath.includes('/') && !binPath.includes('\\')) {
return binPath

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug — Bare .. bypasses all validation (CONFIRMED)

The early-return short-circuit lets a bare .. (no slashes) pass unchecked. The check !binPath.includes('/') && !binPath.includes('\\') is true for .., so it returns immediately — the segments.includes('..') check on line 65 never runs.

Reproducer: TRUSTIFY_DA_MVN_PATH=..

Suggested fix: Remove the early return entirely. '..'.split(/[\/\\]/) produces ['..'], which includes('..') catches, while bare names like mvn still pass all subsequent checks.


Secondary concern: validateExecutablePath(null) would throw a TypeError (.includes() on null) rather than a descriptive validation error. Current callers always pass string defaults via getCustom so this is not reachable today, but the function is fragile if upstream call patterns change. A typeof binPath !== 'string' guard at the top would harden it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as code change request — sub-task TC-5619 created to address this feedback.

Comment thread src/tools.js
return binPath
}

if (binPath.startsWith('./') || binPath.startsWith('.\\')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit — Relative paths without ./ prefix still pass (PLAUSIBLE)

subdir/malicious contains / (no early return), does not start with ./, and splits into segments without .. — so validation passes. While this doesn't enable upward traversal (the CVE target), it allows execution of binaries resolved relative to cwd, which may not match the intent of blocking ./-prefixed paths.

Worth considering whether any path that isn't absolute and isn't a bare command name should be rejected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as nit — advisory observation about relative paths without ./ prefix. The reviewer notes this is PLAUSIBLE but uses "Worth considering" language, not a required change. No sub-task created.

Comment thread test/tools.test.js
/** Verifies that valid absolute paths are accepted. */
test('allows valid absolute paths', () => {
process.env['TRUSTIFY_DA_DUMMY_PATH'] = '/usr/bin/mvn'
expect(getCustomPath('dummy')).to.equal('/usr/bin/mvn')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit — Redundant delete

This delete process.env['TRUSTIFY_DA_DUMMY_PATH'] is unnecessary — the next line overwrites it with a new value, and afterEach already handles cleanup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as nit — minor cleanup feedback about a redundant delete call that is handled by afterEach. No sub-task created.

@ruromero

Copy link
Copy Markdown
Collaborator

Verification: Path traversal validation in getCustomPath (PR #603)

Verdict: FAIL

Claim: The PR adds validateExecutablePath() to reject directory traversal (..) and relative (./) paths in environment-provided executable paths, fixing a security issue where malicious TRUSTIFY_DA_*_PATH values could execute arbitrary binaries.

Method: Compiled the library (npm run compile), drove the CLI binary (node dist/src/cli.js component /tmp/pom.xml) with various TRUSTIFY_DA_MVN_PATH values to exercise the validation through the real provider call chain (Maven provider → selectToolBinarygetCustomPathvalidateExecutablePath).

Steps

  1. ../../etc/maliciousError: Executable path rejected: path contains directory traversal segment (..) — exit 1
  2. ./maliciousError: Executable path rejected: relative paths starting with './' are not allowed — exit 1
  3. /usr/bin/../../../tmp/evilError: Executable path rejected: path contains directory traversal segment (..) — exit 1
  4. ..\\..\\etc\\evil → Correctly rejected (backslash traversal)
  5. /usr/bin/mvn (valid absolute) → Passed validation, failed on "mvn not found" as expected
  6. ✅ Default bare mvn (no override) → Passed validation, proceeded to backend fetch
  7. .. (bare, no slashes) → NOT rejected — passed validation, proceeded to execute .. as binary ("failed to check for maven")
  8. 🔍 subdir/binary (relative, no ./) → Passed validation, accepted for execution
  9. 🔍 . (bare dot) → Passed validation, tried to execute (EACCES)
  10. 🔍 Empty string → Passed validation, failed at binary execution

Findings

  • ⚠️ The confirmed bypass from the review is real. TRUSTIFY_DA_MVN_PATH=.. bypasses validateExecutablePath because the early-return at line 54 short-circuits before the segments.includes('..') check. The error message is "failed to check for maven" (from the binary execution attempt), not the security rejection message. While .. alone isn't directly exploitable as a binary, it demonstrates the validation logic is incomplete — the function's own JSDoc says it rejects .. segments but doesn't.
  • ⚠️ subdir/binary (relative without ./ prefix) passes validation silently. The rejection of ./ but acceptance of bare relative paths is an inconsistent policy — either relative paths are dangerous or they aren't.
  • . and empty string pass validation too — not security-critical but the function doesn't validate that the input is a meaningful executable name.

@a-oren

a-oren commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

[sdlc-workflow/verify-pr] Re: @sourcery-ai[bot] review — Classified as suggestion — meta-summary of 2 inline issues (question about relative path testing + suggestion about opts-based ./ test). Both inline comments were individually classified and addressed. No sub-task created.

@a-oren

a-oren commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

[sdlc-workflow/verify-pr] Re: @ruromero review — Classified as code change request — confirms a bypass where bare .. (no slashes) evades validateExecutablePath() due to the early-return short-circuit. Sub-task TC-5619 created to address the specific inline finding (comment 3750471629).

@a-oren

a-oren commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-5485 (commit db61fcf)

Check Result Details
Review Feedback WARN 7 items classified (1 code change request, 2 suggestions, 1 question, 2 nits, 1 review body code change request); sub-task TC-5619 created for bare .. bypass
Root-Cause Investigation DONE implement-task skill gap — early-return optimization bypassed security validation; root-cause task TC-5620 created
Scope Containment WARN test/tools.test.js out of scope per task spec, but justified as test coverage for the new validation
Diff Size PASS ~119 lines across 2 files, proportionate to task
Commit Traceability PASS Both commits reference TC-5485
Sensitive Patterns PASS No secrets or credentials detected
CI Status PASS 5/5 checks pass
Acceptance Criteria WARN 7 of 8 criteria met; bare .. bypasses validation (confirmed by reviewer and correctness sub-agent)
Test Quality WARN 2 traversal tests are parameterization candidates; all tests documented; Eval Quality: N/A
Test Change Classification ADDITIVE 8 new tests added across 2 suites
Verification Commands PASS npm test and npm run lint both pass

Overall: WARN

The implementation correctly adds validateExecutablePath() as defense-in-depth against CVE-2026-18389 with comprehensive test coverage. However, reviewer ruromero identified a confirmed bypass: bare .. (without path separators) evades the early-return optimization at line 53, skipping the segments.includes('..') check. Sub-task TC-5619 created to fix this bypass. Root-cause task TC-5620 created to improve implement-task skill guidance on verifying optimization shortcuts in validation functions.


This comment was AI-generated by sdlc-workflow/verify-pr v0.13.8.

a-oren added 4 commits August 11, 2026 13:59
Remove the early-return optimization that short-circuited validation for
inputs without path separators. `..` contains no `/` or `\` and was
incorrectly treated as a safe bare command name, skipping the
segments.includes('..') check entirely. Without the early return,
'..'.split(/[/\\]/) correctly produces ['..'] which the existing segment
check catches.

Implements TC-5619

Assisted-by: Claude Code
…ation

Tighten validateExecutablePath to reject any path that contains a
separator but is not absolute (e.g. subdir/binary, bin/mvn). Custom
executable paths must be either a bare command name resolved via PATH
or an explicit absolute path — relative paths with directory components
could resolve to workspace-internal files.

Implements TC-5619

Assisted-by: Claude Code
The next line immediately overwrites the env var, and afterEach
handles cleanup. Addresses reviewer nit.

Implements TC-5619

Assisted-by: Claude Code
Add a typeof check at the top of validateExecutablePath to throw a
descriptive error instead of a TypeError if null, undefined, or a
non-string value is passed. Not reachable from current callers but
hardens the function against future call-site changes.

Implements TC-5619

Assisted-by: Claude Code
@a-oren
a-oren requested a review from ruromero August 11, 2026 11:45
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.

3 participants