fix(security): close path traversal and shell injection paths - #508
Open
nichochar wants to merge 2 commits into
Open
fix(security): close path traversal and shell injection paths#508nichochar wants to merge 2 commits into
nichochar wants to merge 2 commits into
Conversation
Second half of the security pass. All of these are reachable by a user who imports a .src.md from an untrusted source, which is a normal thing to do — the CLI's import command exists to fetch notebooks off the internet. - DELETE /api/srcbooks/:id built a path with Path.join and handed it to fs.rm(recursive), and express URL-decodes params, so `..%2F..%2FDocuments` escaped SRCBOOKS_DIR. Paths from user input now go through containedPath, which resolves and asserts containment. removeSrcbook returns its promise so a failure is reported rather than becoming an unhandled rejection. - POST /api/file read any path on disk. Now confined to SRCBOOKS_DIR, which still covers what go-to-definition needs: a srcbook's own sources and the type declarations under its node_modules. - formatCode interpolated a filename into a shell command string. Filenames are validated on rename but not when they arrive from a decoded .src.md, so `###### $(id).mjs` in an imported notebook reached a shell. Now execFile with an argument array, run in the srcbook's own directory so npx resolves that copy of prettier. Prettier warns on stderr while still succeeding, so only a non-zero exit counts as failure now. - Same treatment for the depcheck invocation in deps.mts. - Filenames are validated at the decoder, which is where untrusted input enters. Both the h6 heading and the link target of an external-form cell, since the latter carries the real filename on disk. - POST /api/settings passed req.body straight to db.update().set(), so any column was writable — including aiBaseUrl, which decides where AI requests and the user's API key get sent. Now an explicit zod allowlist. 21 new tests: path containment (including the sibling-prefix case, where /tmp/srcbooks-evil starts with /tmp/srcbooks as a string but is not inside it) and decoder filename rejection. Suite is 38 tests, up from 11.
nichochar
force-pushed
the
fix/network-security
branch
from
July 29, 2026 16:48
9c1c3f0 to
191312a
Compare
Contributor
Author
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Stacked on #507. Closes task 0002.
The second half of the security pass. Everything here is reachable by importing a
.src.mdfrom an untrusted source — which is a normal thing to do, sincesrcbook importexists specifically to fetch notebooks off the internet.Unlike #507, these were found by reading rather than testing: both are destructive, so I didn't exercise them against a live instance.
Fixes
Path traversal into
fs.rm(recursive: true).DELETE /api/srcbooks/:idbuilt the path withPath.join, and express URL-decodes route params, so..%2F..%2FDocumentsescapedSRCBOOKS_DIR. Paths from user input now go through acontainedPathhelper that resolves and asserts containment.removeSrcbookreturns its promise so failures are reported instead of becoming unhandled rejections.Arbitrary file read.
POST /api/fileread any path on disk. Now confined toSRCBOOKS_DIR. Not deleted, because go-to-definition genuinely needs it — tsserver reports paths into a srcbook's ownsrc/and into type declarations under itsnode_modules, and that root covers both.Shell injection via cell filename.
formatCodeinterpolated a filename into a command string run throughexec. Filenames are validated on rename but not when they arrive from a decoded.src.md, so###### $(id).mjsin an imported notebook reached a shell. NowexecFilewith an argument array, run in the srcbook's own directory sonpxresolves that copy of prettier rather than whatever is near the server's cwd. Same treatment for the depcheck call indeps.mts.Validation moved to the boundary. Filenames are now checked when a
.src.mdis decoded, which is where untrusted input enters. This covers the link target as well as the h6 heading — the external form is###### foo.tsfollowed by[foo.ts](./src/foo.ts), and it's the link that carries the real filename, so validating only the heading would have left the path open.Mass assignment on settings.
POST /api/settingshandedreq.bodystraight todb.update(configs).set(...), so any column was writable — includingaiBaseUrl, which decides where AI requests, and the user's API key, get sent. Now an explicit zod allowlist.Incidental
formatCodetreated any stderr output as failure, but prettier writes warnings to stderr while still exiting 0. Only a non-zero exit counts now.Tests
21 new, suite is 38 (from 11 at the start of the stack). The containment tests include the case that makes this kind of check go wrong in practice:
/tmp/srcbooks-evilpasses a naivestartsWith('/tmp/srcbooks')while being an entirely different directory, so the check compares againstroot + path.sep.Build, lint, and format all pass.
Noticed, not fixed
Two format bugs from
REVIEW.md§2.2 that belong in their own change rather than a security PR:decode()still throws instead of returning{error: true}when metadata is missing, andencode()still assumescells[0]/cells[1]are the title and package.json.🤖 Generated with Claude Code