Skip to content

fix(bundle): validate module dirs and file keys with posixPathEscapes, matching the lockfile parser#60

Open
exo-nikita wants to merge 1 commit into
mainfrom
claude/stasis-core-plugins-review-wxauv4-bundle-parse
Open

fix(bundle): validate module dirs and file keys with posixPathEscapes, matching the lockfile parser#60
exo-nikita wants to merge 1 commit into
mainfrom
claude/stasis-core-plugins-review-wxauv4-bundle-parse

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Summary

Bundle.parse validated v1 module/source bucket dirs, per-file rel keys, and v0 flat source paths with only a '..'-prefix check (!x.startsWith('..')), while Lockfile.parse — and Bundle.parse's own formats and imports checks — already use the stronger posixPathEscapes(), which also rejects a mid-path .. that pops above the root (a/../../x) and absolute paths. This PR replaces the four remaining prefix checks in stasis-core/src/bundle.js with posixPathEscapes, so both parsers validate paths the same way.

Why (defense-in-depth)

No known exploit: stasis extract independently re-validates every write for containment against the output dir, and the loader serves files by canonical key. But the schema boundary should fail closed — a tampered bundle carrying node_modules/a/../../../evil or /node_modules/pkg as a bucket dir previously parsed successfully and only failed (if at all) at a downstream consumer. Now it is rejected at parse time, consistent with Lockfile.parse.

Invariant preserved: rel === '' (a directory capture keyed at a module root, see moduleFileKey) and v0's inferred '.' dir both normalize to '.' and still pass — pinned by a new test rather than assumed.

Tests

New cases in tests/public-exports.test.js (alongside the existing Bundle/Lockfile rejection tests):

  • v1 modules dir escaping mid-path (node_modules/a/../../../evil — contains node_modules, so only the path check can catch it) → rejected
  • absolute v1 modules dir (/node_modules/pkg) → rejected
  • v1 per-file key escaping its bucket (sub/../../x) → rejected
  • v1 sources dir escaping mid-path (src/../../evil) → rejected
  • v0 flat source paths ../x, a/../../x, /etc/passwd → rejected
  • sanity: a v1 bundle with a directory-format listing keyed at a module root (rel === '') still parses and survives a serialize → parse round-trip

Two tests in tests/extract-cmd.test.js hand-crafted escaping bundles specifically because "Bundle.parse only rejects paths that start with .."; those fixtures are now rejected at parse, so the tests assert the earlier (wrapped) parse failure and still assert nothing is written to disk. extractCommand's own containment check stays in place as a second layer.

Ran node --test on public-exports, extract-cmd, sbom, diff, ecosystem, fs, state, state-load-proto, state-split-bundle — all pass. bundle-cmd/state-load/state-sidecar/fs-sidecar/cli-fs show 41 failures both on this branch and on pristine main in this environment (missing installed deps, e.g. oxc-parser, and node v22 vs engines >=24) — unrelated and unchanged by this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M16Lwt8cM1azwev94UP81X


Generated by Claude Code

…, matching the lockfile parser

Bundle.parse guarded v1 module/source bucket dirs, per-file keys, and v0
flat source paths with only a '..'-prefix check, while Lockfile.parse (and
Bundle.parse's own formats/imports checks) already use posixPathEscapes,
which also catches a mid-path '..' that pops above the root and absolute
paths. Defense-in-depth: no known exploit (extract re-validates containment
against the output dir and load serves by canonical key), but the schema
boundary should fail closed the same way in both parsers.

rel === '' (a `directory` capture keyed at a module root) and the v0
inferred '.' dir normalize to '.' and still pass, pinned by a new test.
The two extract-cmd tests whose escaping fixtures relied on parse being
weaker now assert the earlier parse-time rejection (and still assert
nothing is written).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M16Lwt8cM1azwev94UP81X
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