fix(installer): confine tarball extraction with os.Root#1934
Conversation
Lexical containment of tar entry names is not enough on its own: an archive can ship a symlink whose target passes the check (`a` -> `.` resolves to destDir itself) and then write through it with a later entry (`a/x` -> `..`) that really lands outside destDir. Confirmed by extracting a crafted tarball — it wrote a file into destDir's parent. Anchor extraction at an os.Root opened on destDir so the OS-level path walk refuses to follow a link out of the tree, and keep the lexical checks for clear up-front errors. Adds a regression test for the two-hop symlink chain. Fixes CodeQL alert #19 (go/zipslip). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughTarball extraction now uses ChangesTarball extraction security
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b12d59655c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
b12d596 to
14fd0d1
Compare
Root.Symlink follows symlinked parent components, so an entry named "a/x" is created at "x" when "a" is a symlink to ".". The link still lands inside destDir, but one level shallower than its archive path — which changes what a relative target means. "a/x" -> ".." passes a check computed against parent "a" and then resolves to destDir's parent from its real home, leaving an escaping symlink in the install tree. Nothing can be written through it during extraction (os.Root refuses to follow it), but it persists for any later consumer that walks the tree with ordinary os calls. Require every parent component of a symlink entry to be a real directory, so an entry's archive path and its on-disk location agree — the assumption the existing target check already depended on. Reported by the Codex reviewer on #1934. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Triage of all 11 open code scanning alerts. One was a real, reproducible bug; the other ten were false positives or by-design and have been dismissed in the UI with per-alert reasoning.
The real one: zip-slip in tarball tool installs (CodeQL #19, high)
extractTarGzvalidated tar entry paths lexically only. That is escapable in two hops:a→ symlink to.— resolves todestDiritself, so it passes containmenta/x→ symlink to..— lexically "resolves" todestDir(allowed), but really lands ondestDir's parent, becauseaisdestDira/x/pwned.txt— an ordinary file, no..anywhere in its name, written through the chainBuilding that tarball and extracting it against the old code wrote
PWNEDoutsidedestDir. Not theoretical.The gap is that lexical validation resolves
a/xas ifawere a real directory. It isn't — it's a symlink, and the kernel resolves it at a shallower depth than the string math assumes.Fix
Anchor extraction at an
os.Rootopened ondestDir, so containment is enforced by the OS-level path walk, which refuses to follow a link out of the tree.os.Rootalso permits the legitimate cases we already supported (npm-style.bin/x -> ../pkg/bin/x.jsrelative links). The lexical checks stay in place — they now serve to surface a clear error up front rather than being the sole defense.Requires Go 1.24+ for
os.Rootand 1.25+ forRoot.MkdirAll/Root.Symlink; the module is already ongo 1.26.0.Reachability
Narrow — it needs a malicious or compromised asset on a pinned GitHub release that the tool installer pulls. Worth fixing regardless: the fix is small and the primitive is a full arbitrary-file-write outside the install directory.
The ten dismissed alerts
go/command-injection(critical)go/path-injectiongo/path-injectionjs/request-forgery(critical)Each carries its reasoning in the dismissal comment on the alert itself.
Test plan
TestExtractTarGz_WriteThroughSymlinkChainBlockedreproduces the two-hop chain, asserts extraction errors and that nothing lands outsidedestDir. It fails against the pre-fix code.make -C apps/backend test build,golangci-lintclean.🤖 Generated with Claude Code