Skip to content

fix: path traversal in download_job_artifacts local_path - #644

Open
brennanneoh wants to merge 3 commits into
zereight:mainfrom
brennanneoh:fix/download-job-artifacts-path-traversal
Open

fix: path traversal in download_job_artifacts local_path#644
brennanneoh wants to merge 3 commits into
zereight:mainfrom
brennanneoh:fix/download-job-artifacts-path-traversal

Conversation

@brennanneoh

Copy link
Copy Markdown

Summary

  • downloadJobArtifacts() (index.ts, backing the download_job_artifacts tool) joined the caller-supplied local_path into the filesystem save path with no validation, allowing an absolute path or ../ traversal to write the downloaded artifact zip anywhere the process has write permission.
  • The sibling downloadAttachment() function already validates its own local_path the same way; this brings downloadJobArtifacts in line with that existing, hardened pattern (reject absolute paths and .. traversal).

Why

Security issue: an MCP client (or anything able to issue tool calls in local/stdio mode) could pass local_path: "/etc/cron.d" or local_path: "../../../../home/user/.ssh" to download_job_artifacts and get an arbitrary file write on the host.

Reported privately to the maintainer alongside a second, unrelated finding before opening this PR.

How tested

  • npx tsc --noEmit
  • npm run test:mock (all suites pass; added two new tests to test/test-job-artifacts.ts covering traversal and absolute-path rejection, and updated two existing tests that relied on an absolute local_path to use a relative one, matching the contract already established by downloadAttachment's test suite)
  • npm run test:consumer-smoke

Breaking changes

local_path for download_job_artifacts must now be a relative, non-traversal path — matching the existing contract for download_attachment's local_path. Absolute paths or .. segments now return an error instead of silently writing outside the intended location.

downloadJobArtifacts() joined the caller-controlled local_path
argument (download_job_artifacts MCP tool) directly into the
filesystem save path with no validation, allowing an absolute path
or ../ traversal sequence to write the artifact zip anywhere the
process has permission to write.

Apply the same normalize/validate check already used by
downloadAttachment() for its localPath parameter: reject absolute
paths, "..", and any path escaping the intended base directory.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved artifact download path validation.
    • Downloads now reject absolute paths and directory-traversal patterns, helping prevent unsafe file destinations.
    • Existing default filenames and destination directory creation remain supported.

Walkthrough

downloadJobArtifacts validates normalized local paths before constructing artifact output paths. It rejects absolute and traversal paths while preserving default filename behavior and file streaming.

Changes

Artifact path security

Layer / File(s) Summary
Path validation and output construction
index.ts, test/test-job-artifacts.ts
downloadJobArtifacts normalizes and validates the local directory. It rejects absolute paths and traversal components, then joins valid directories with the generated filename. Tests create an explicit relative directory and cover both rejection cases.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the path traversal fix, security impact, validation behavior, tests, and contract change.
Title check ✅ Passed The title clearly and concisely identifies the path traversal fix in download_job_artifacts local_path.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

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.

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@index.ts`:
- Around line 7490-7500: Update the artifact download tests to pass relative
fixture directories below the process working directory instead of the absolute
tmpDir paths. Replace both local_path values based on tmpDir, including the
artifacts/run-42 case, while preserving their intended directory structure and
download assertions.
- Around line 7490-7498: Move the localPath validation block in
downloadJobArtifacts before constructing the artifact URL and issuing the
authenticated GitLab fetch. Preserve the existing traversal checks and error
message, and only proceed to handleGitLabError and streamPipeline after
localPath has been validated.
- Around line 7491-7500: Update the local path validation around
normalizedLocalPath to reject drive-relative Windows paths by parsing the
normalized path and rejecting any path with a non-empty parsed root, including
values such as C:foo and C:..\\outside. Preserve the existing absolute-path and
traversal checks before constructing savePath with path.join.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 389289d6-53fe-4a5d-9ee3-6754657c22ee

📥 Commits

Reviewing files that changed from the base of the PR and between 926d42c and 96c0401.

📒 Files selected for processing (1)
  • index.ts
📜 Review details
🔇 Additional comments (1)
index.ts (1)

7491-7500: 🔒 Security & Privacy

Verify the symlink threat model.

This check validates only lexical path components. A relative localPath such as link can name a symlink below the working directory. The later filesystem calls follow that symlink, so the archive can still be written outside the intended base without an absolute path or ... If callers are untrusted, canonicalize the parent directory against a fixed base and use no-follow or atomic file creation to avoid a race.

Comment thread index.ts Outdated
Comment thread index.ts Outdated
Comment thread index.ts Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 8, 2026
- validate local_path before the GitLab fetch, not after
- reject Windows drive-relative paths (e.g. "C:foo") via path.parse().root
- update job-artifact tests to use relative fixture dirs, add traversal
  and absolute-path rejection test cases
@brennanneoh

Copy link
Copy Markdown
Author

Pushed 4426f29 addressing the 3 actionable comments:

  • moved local_path validation before the GitLab fetch
  • added a check for Windows drive-relative paths (path.parse().root)
  • updated test/test-job-artifacts.ts to use relative fixture dirs, plus added traversal + absolute-path rejection tests

On the symlink note: agreed it's a real gap, but it's pre-existing in downloadAttachment too (same lexical-only check), so I kept this PR scoped to bringing downloadJobArtifacts in line with that existing pattern rather than introducing new canonicalization/no-follow behavior across both. Happy to file that as a separate follow-up if useful.

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@test/test-job-artifacts.ts`:
- Around line 226-230: Restructure the traversal test around the
download_job_artifacts call so a successful call cannot be caught as the
expected rejection. Use assert.rejects or capture only the callTool error in the
try/catch, then perform the rejection/message assertion outside the catch while
preserving the traversal-message validation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1c3a1bec-5da0-42f5-bf33-a4480e881c96

📥 Commits

Reviewing files that changed from the base of the PR and between 96c0401 and 4426f29.

📒 Files selected for processing (2)
  • index.ts
  • test/test-job-artifacts.ts
📜 Review details
🔇 Additional comments (4)
index.ts (2)

7469-7481: LGTM!


7502-7502: LGTM!

test/test-job-artifacts.ts (2)

145-150: LGTM!


235-253: LGTM!

Comment thread test/test-job-artifacts.ts Outdated
assert.fail() inside the same try block as the awaited call could be
swallowed by its own catch, silently passing on an unexpected success.
Capture the error outside the try/catch and assert on it there.
@brennanneoh

Copy link
Copy Markdown
Author

Pushed bbd734f: fixed the flagged issue in the two new traversal-rejection tests. `assert.fail()` was inside the same `try` its own `catch` handled, so an unexpected success would have been silently swallowed as a pass. Now the error is captured outside the `try/catch` and asserted on there, so a non-throwing call correctly fails the test.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/test-job-artifacts.ts (1)

237-256: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add coverage for Windows drive-relative paths.

The absolute-path test covers only the host-native absolute-path branch. It does not exercise path.parse(normalizedLocalPath).root with a value such as C:artifacts. If Windows runtimes are supported, add this case to a Windows test job or verify that an existing test covers it. Node.js path parsing is platform-specific. (nodejs.org)

🤖 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 `@test/test-job-artifacts.ts` around lines 237 - 256, Add coverage for the
Windows drive-relative path case in the download_job_artifacts tests, using a
path such as C:artifacts and running it in a Windows-specific test job or
environment. Verify that callTool rejects it with the same traversal error, and
avoid relying on the host-native absolute-path test to cover platform-specific
path.parse behavior.
🤖 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.

Outside diff comments:
In `@test/test-job-artifacts.ts`:
- Around line 237-256: Add coverage for the Windows drive-relative path case in
the download_job_artifacts tests, using a path such as C:artifacts and running
it in a Windows-specific test job or environment. Verify that callTool rejects
it with the same traversal error, and avoid relying on the host-native
absolute-path test to cover platform-specific path.parse behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ac6d3d37-c8c1-4db3-8e38-7b787666ff72

📥 Commits

Reviewing files that changed from the base of the PR and between 4426f29 and bbd734f.

📒 Files selected for processing (1)
  • test/test-job-artifacts.ts
📜 Review details
🔇 Additional comments (2)
test/test-job-artifacts.ts (2)

7-7: LGTM!

Also applies to: 145-150


216-235: LGTM!

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.

1 participant