Skip to content

Fix: Zip Slip path traversal in POST /file-upload: ZIP entry paths are only checked with a substring match, allowing arbitrary file write anywhere under the application root - #368

Closed
WesternConcrete wants to merge 1 commit into
masterfrom
devin/1789456057-fix-zip-slip-file-upload
Closed

WesternConcrete wants to merge 1 commit into
masterfrom
devin/1789456057-fix-zip-slip-file-upload

Conversation

@WesternConcrete

@WesternConcrete WesternConcrete commented Sep 15, 2026

Copy link
Copy Markdown

Description

Finding: Zip Slip path traversal in POST /file-upload: ZIP entry paths are only checked with a substring match, allowing arbitrary file write anywhere under the application root
Repo: COG-GTM/juice-shop

Fix approach: handleZipFileUpload in routes/fileUpload.ts now resolves each entry against uploads/complaints and only writes it when the resolved path starts with that directory (absolutePath.startsWith(uploadDir + path.sep)), replacing the includes(path.resolve('.')) substring check; entries that escape the directory are drained.

Note: this intentionally neutralises the fileWriteChallenge training challenge, which relies on this behaviour.

Resolved or fixed issue: none

AI Tool Disclosure

  • My contribution does not include any AI-generated content
  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: Devin
    • LLMs and versions: Claude
    • Prompts: Daily security sweep: find and fix substantiated vulnerabilities in COG-GTM/juice-shop

Affirmation

Link to Devin session: https://app.devin.ai/sessions/4b1b2e7dcc0a41848f4c6a189d79dfef
Open in Devin Desktop: https://app.devin.ai/desktop/session/4b1b2e7dcc0a41848f4c6a189d79dfef?variant=devin
Requested by: @WesternConcrete


Devin Review

Co-Authored-By: Wes Convery <2wconvery@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions github-actions Bot added the invalid This doesn't seem right label Sep 15, 2026
@github-actions

Copy link
Copy Markdown

Hi @WesternConcrete, thank you for your contribution! 🙌

Unfortunately, this PR does not meet our contributing guidelines and has been closed:

  • 🎯 Wrong target branch: This PR targets the master branch. Per our contributing guidelines, all PRs must be based on the develop branch. Please re-open this PR against develop.
  • ✍️ Missing DCO Sign-off: All commits in this PR must be signed off to indicate your agreement with the Developer Certificate of Origin. Please use git commit -s for all your commits.

Please address the above and open a new PR. If you have questions, check our contributing guidelines

@github-actions github-actions Bot closed this Sep 15, 2026
@github-actions

Copy link
Copy Markdown

Hi @WesternConcrete, thank you for your contribution! 🙌

Unfortunately, this PR does not meet our contributing guidelines and has been closed:

  • 🎯 Wrong target branch: This PR targets the master branch. Per our contributing guidelines, all PRs must be based on the develop branch. Please re-open this PR against develop.
  • ✍️ Missing DCO Sign-off: All commits in this PR must be signed off to indicate your agreement with the Developer Certificate of Origin. Please use git commit -s for all your commits.

Please address the above and open a new PR. If you have questions, check our contributing guidelines

@devin-ai-integration devin-ai-integration 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.

Devin Review found 3 potential issues.

Devin Review

Comment thread routes/fileUpload.ts
Comment on lines 44 to +45
challengeUtils.solveIf(challenges.fileWriteChallenge, () => { return absolutePath === path.resolve('ftp/legal.md') })
if (absolutePath.includes(path.resolve('.'))) {
entry.pipe(fs.createWriteStream('uploads/complaints/' + fileName).on('error', function (err) { next(err) }))
if (absolutePath.startsWith(uploadDir + path.sep)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Blocked overwrite still solves challenge

A traversal entry targeting ftp/legal.md triggers solveIf before the containment check drains it. The legal file stays unchanged, but the Arbitrary File Write challenge is awarded.

Learn more

The challenge completion predicate runs when the parser sees a target path, not when extraction writes that path. The new containment guard rejects the only path matching ftp/legal.md, but it runs after solveIf. The challenge definition still requires overwriting the legal file in the challenge metadata. Consequently, the endpoint reports success for the challenge without performing its objective.

Example: An archive containing ../../ftp/legal.md resolves to the legal file. solveIf marks the challenge solved, then the guard drains the entry. A subsequent GET /ftp/legal.md returns the original content although the scoreboard shows completion.

Recommended fix: Retire or disable fileWriteChallenge when traversal is blocked, or redesign its objective and completion condition around a safe training target. Do not call solveIf merely from the rejected entry path; completion must verify the challenge's documented effect.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread routes/fileUpload.ts
Comment on lines +42 to +46
const uploadDir = path.resolve('uploads/complaints')
const absolutePath = path.resolve(uploadDir, fileName)
challengeUtils.solveIf(challenges.fileWriteChallenge, () => { return absolutePath === path.resolve('ftp/legal.md') })
if (absolutePath.includes(path.resolve('.'))) {
entry.pipe(fs.createWriteStream('uploads/complaints/' + fileName).on('error', function (err) { next(err) }))
if (absolutePath.startsWith(uploadDir + path.sep)) {
entry.pipe(fs.createWriteStream(absolutePath).on('error', function (err) { next(err) }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Containment fix lacks regression tests

No test covers accepted entries, rejected traversal, or challenge completion. Repository rules require tests and RSN verification for challenge-related changes.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread routes/fileUpload.ts
Comment on lines 44 to 48
challengeUtils.solveIf(challenges.fileWriteChallenge, () => { return absolutePath === path.resolve('ftp/legal.md') })
if (absolutePath.includes(path.resolve('.'))) {
entry.pipe(fs.createWriteStream('uploads/complaints/' + fileName).on('error', function (err) { next(err) }))
if (absolutePath.startsWith(uploadDir + path.sep)) {
entry.pipe(fs.createWriteStream(absolutePath).on('error', function (err) { next(err) }))
} else {
entry.autodrain()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Challenge neutralization lacks lifecycle changes

The PR intentionally neutralizes the challenge while leaving it enabled and documented as solvable. Its metadata and training lifecycle need follow-up.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

invalid This doesn't seem right

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant