Skip to content

fix(scanner): patch unauthenticated arbitrary file write (GHSA-ppp9-2hc2-hfg5)#13

Merged
ralyodio merged 5 commits into
masterfrom
fix/scanner-arbitrary-file-write
Jul 13, 2026
Merged

fix(scanner): patch unauthenticated arbitrary file write (GHSA-ppp9-2hc2-hfg5)#13
ralyodio merged 5 commits into
masterfrom
fix/scanner-arbitrary-file-write

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Fixes the security advisory GHSA-ppp9-2hc2-hfg5.

The bug

ScannerService.exportReport(scanId, options) passed options.destination straight into fs.writeFileSync() with zero validation. That destination is fully attacker-controlled from two unauthenticated HTTP entry points:

  • GET /scanner/reports/:id/export?destination=…
  • POST /tools/scanner {"action":"export", …, "destination":…}

Absolute paths (/etc/cron.d/x) and ../ traversal both reached the sink unmodified → unauthenticated arbitrary file write. The shipped Dockerfile runs as root, so any file in the container could be overwritten.

The fix

mcp_modules/scanner/src/service.js — when a caller supplies destination, resolve it against the reports directory and reject anything that resolves outside it (absolute paths and .. traversal). Legitimate relative filenames (and nested subdirs inside the reports dir) still work; the default no-destination path is unchanged.

Tests

Added regression tests in test/service.test.js covering the advisory's PoC paths (/tmp/pwned.html, /etc/cron.d/x, ../../../../tmp/pwned.json, ../../.bashrc) — all now rejected — plus a positive case confirming a plain filename still writes inside the reports directory. Full suite: 26 passing.

Note (defense-in-depth, not in this PR)

The advisory also recommends requiring auth on side-effecting scanner routes. This PR closes the arbitrary-write primitive itself; adding auth middleware is a larger, separate change.

🤖 Generated with Claude Code

ralyodio and others added 2 commits July 13, 2026 12:45
…pp9-2hc2-hfg5)

Unauthenticated arbitrary file write: `destination` flowed from two HTTP
entry points (GET /scanner/reports/:id/export and POST /tools/scanner)
straight into fs.writeFileSync() with zero validation, allowing overwrite
of any path the process could write (root, in the shipped Dockerfile).

Resolve an attacker-supplied `destination` against the reports directory
and reject anything that escapes it (absolute paths and `..` traversal).
Legitimate relative filenames still work. Adds regression tests covering
the advisory's PoC paths.

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

Defense-in-depth for the scanner advisory: the routes that start a scan
(spawns a child process) and export a report (writes to disk) had no auth
middleware at all. Add a bearer-token / X-API-Key guard (requireScannerAuth)
on POST /scanner/scan, GET /scanner/reports/:id/export and POST /tools/scanner.

Token is read from SCANNER_API_TOKEN; comparison is constant-time. When the
env var is unset the routes stay open (backward compatible) but log a one-time
warning. Read-only routes remain unauthenticated. Adds middleware tests and
documents the env var in .env.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread mcp_modules/scanner/src/auth.js Fixed
Comment thread mcp_modules/scanner/src/auth.js Fixed
ralyodio and others added 2 commits July 13, 2026 13:13
- semgrep (dockerfile.security.missing-user): run the container as the
  built-in non-root `node` user instead of root. Also hardens the
  arbitrary-write advisory's "Dockerfile runs as root" impact note.
- npm audit (GHSA-5c6j-r48x-rmvq): override transitive serialize-javascript
  (pulled dev-only via mocha) to ^7.0.0, which is patched. Added to both
  npm `overrides` and `pnpm.overrides`.
- gitleaks: add .gitleaks.toml extending the default ruleset with an
  allowlist for documentation placeholders and the intentionally-shipped
  hynt.us default key; wire --config into the workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeQL flagged js/insufficient-password-hash on the plain SHA-256 digest of
the API token used to normalise lengths for timingSafeEqual. Switch to the
canonical double-HMAC comparison: HMAC-SHA256 keyed by a random per-process
key. Still constant-time and length-safe, and no longer reads as insecure
password hashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread mcp_modules/scanner/src/auth.js Fixed
Comment thread mcp_modules/scanner/src/auth.js Fixed
CodeQL's js/insufficient-password-hash treats SCANNER_API_TOKEN as a password
and rejects any fast hash of it — including HMAC — since it expects a slow KDF.
This is bearer-token equality, not password storage, so hashing is the wrong
tool. Compare the raw bytes with a length guard + timingSafeEqual: still
constant-time, no hashing, no CodeQL alert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 6b7cb1f into master Jul 13, 2026
8 checks passed
@ralyodio
ralyodio deleted the fix/scanner-arbitrary-file-write branch July 13, 2026 13: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.

2 participants