Skip to content

Security sweep: hostile repositories, terminal, updater, DoS; tests that pin the security rules - #21

Merged
jkyberneees merged 14 commits into
mainfrom
fix/bug-security-sweep
Nov 3, 2026
Merged

jkyberneees merged 14 commits into
mainfrom
fix/bug-security-sweep

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

This PR collects one manual review and four adversarial review passes (the last by eight reviewers, each on a separate area). Each finding was reproduced before it was fixed, and each fix has a regression test. DESIGN.md §24 has the findings → fixes tables; SECURITY.md is updated.

Critical

  • Opening a folder ran its code. The compiled server autoloaded bunfig.toml and .env from its working directory, which is often the viewed folder, so a repository's preload script ran inside the server. Fixed with --no-compile-autoload-*; the Mac app now starts the server in /.

  • Git ran programs named by the repository:

    • a partial clone's promisor remote ran on lazy fetch;
    • submodule filters ran through git diff;
    • a driver listing cut short left drivers active;
    • drivers added later through included config files were not blanked;
    • git diff rewrote the index and ran the post-index-change hook.

    Now: no transports, no hooks, no index refresh, submodules ignored, and the driver overrides fail closed and are rebuilt for every operation.

High and medium

  • Session token: a process in a terminal could read the token from the app window's on-disk profile and type into other terminals. The window now runs --incognito, and the boot page is removed as soon as the page connects.
  • Containment: a folder swapped for a symlink could make file reads and search open files outside the root. Special files in .git (a /dev/zero symlink, a FIFO) could exhaust memory; git is now turned off for such a folder.
  • Terminal: the server's replay mode tracking now mirrors the page's parser, so a reload can no longer turn bracketed paste off. Also: hostile-output limits (REP, clusters, OSC 8, OSC 4, bell and title floods), DECRQM echo, legacy mouse reports.
  • Updater:
    • the checksum is now taken from the file as written to disk (a full disk installed a truncated binary);
    • strict install detection, so it can't replace an unrelated folder or app;
    • pre-releases are never offered;
    • capped reads, no rollback races, actions pinned to SHAs.
  • Client: Trojan Source (bidi characters) in code and names; SVG previews from data: URLs; SVG <use> bombs and image decompression bombs refused before decoding.
  • Denial of service:
    • quadratic highlighter, Go to Symbol and Markdown paths;
    • unbounded bidi markers and an unbounded HEAD peek;
    • huge diffs;
    • superseded blame processes left running;
    • a 20 s freeze when deleting many files;
    • a shell that fails to start no longer crashes the server;
    • the settings write blow-up and Bun's debug error page.

Tests

The suite went from 1368 to 1459 tests. New files:

  • invariants.static — the AGENTS.md rules, checked against the source.
  • http.routes — every route found in http.ts: auth, Host, CSRF, headers.
  • redos.matrix — every highlighter and Go to Symbol language, plus the Markdown parser, against pathological input.
  • terminal.fuzz — seeded hostile terminal output.
  • files.resolve.fuzz — random paths through symlinks in and out of the root.
  • git.hostile-config — every hook and program-naming key armed; .git is snapshotted before and after.
  • pty.tracker.diff — differential check of the server's mode tracking against the page's parser.

Needs a Mac

packaging/macos/main.swift changed but could not be compiled here:

  • the server starts in /
  • the restart message is honoured only after an update, from the main frame
  • the command-line install uses ln -sfh

Run xcrun swiftc -typecheck -target arm64-apple-macos13.3 -framework Cocoa -framework WebKit packaging/macos/main.swift before merging.

Recommendations (not done here)

  • Move the release signing key to a protected GitHub Environment with required reviewers, and verify that tags point at main.
  • Pin hashes of the Bun runtimes used for cross-compiling.
  • The "Workers Builds" check is a Cloudflare project setting that also fails on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S

- git: no transport for any git call (GIT_ALLOW_PROTOCOL="", GIT_NO_LAZY_FETCH=1): a partial
  clone's promisor remote config could run a program when blobs were read. Blank
  blame.ignoreRevsFile; accept viewed folders named "..x".
- pty: ignore non-object JSON control frames (a throw exits the server); floor upgrade sizes.
- update: the whole release tag must be a semver version.
- index: new symlinks to files outside the root are not listed.
- client: bidi controls isolated in the code view and HEAD peek; names, tabs, palette, blame and
  terminal titles show hidden characters; tabs stay tabs in diffs, search previews and Markdown
  code; parseTarget handles U+2028/U+2029; esc escapes single quotes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
…he checked real path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
…idden characters

The -c blame.ignoreRevsFile= override never reset the list (git sorts the values); use
--no-ignore-revs-file. Breadcrumbs, the commit pill, File History details and the status bar
root and branch now show controls and bidi overrides as escapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
- http: never Bun's development error page (development:false, an error handler, Host checked
  before URL parsing); own-key MIME lookups; whole-byte ?limit; /api/open opens files only.
- files: openRegular(abs, root) verifies the file it opened is still inside the root (a parent
  swapped for a symlink); backslash is a separator only on Windows.
- settings: refuse deep nesting and cap the written file (indented output was quadratic).
- index: files created during a build are no longer lost.
- client: linear JS regex-after-word check, Go to Symbol, YAML keys, Markdown headings and
  bare-URL trimming (each froze the UI thread on hostile files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
…s, survive a deleted folder

- terminal: clusters capped at 64 units; REP repeats the base code point, at most a screenful;
  OSC 8 uri map, 2 KB targets, ids never reused; at most 256 OSC 4 answers.
- pty: replay applies the mode changes in the skipped head; carry from the last escape; C1 CSI
  (windows-1252 decoding) and ':' sub-parameters; a hidden cursor is restored.
- git: a failed spawn (viewed folder deleted) is a failed run, not an unhandled rejection that
  exited the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
…only copy on a failed swap; pin actions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
…ssions)

- git: never look inside submodules (their own filters ran via diff); fail closed when the driver
  listing is cut; re-list drivers per operation (included configs); rename limits, UTF-8 log
  output and --root forced; a timed-out diff is truncated, not clean.
- server: a shell that cannot start no longer exits the server, at most 64 shells; index deletes
  are not quadratic; git status refresh is throttled, not starved; search reports symlinked
  matches under the real path and falls back to the index in an ignored folder.
- client: open() line races, events-socket resync, Changes refresh on edits, Cmd+B/J in the
  terminal, image blob URLs revoked, IME Enter, stale gutter base, tooltips escaped; settings
  saved as merged PATCHes of changed keys, tabs: roots pruned, layout flushed on close.
- terminal: a capped REP leaves the cursor where the full count would.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
Comment thread test/client.hidden.test.ts Fixed
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Failed ❌

View logs ↗
5c8dc4e 2026-11-03T14:39:17.329Z View logs ↗
  • Build: Failed ❌

View logs ↗
9c7a060 2026-11-03T13:40:07.198Z View logs ↗
  • Build: Failed ❌

View logs ↗
7b1d010 2026-11-03T12:51:00.214Z View logs ↗
  • Build: Failed ❌

View logs ↗
474b7cc 2026-11-03T11:01:54.430Z View logs ↗

… a plain object

Keys come from the request body; a Map never treats __proto__/constructor as anything but data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
The almost-full-ring test detached its first client as soon as FLOODED appeared; a prompt printed
after that reached the replay but not the client, so under load the two outputs differed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S

Copy link
Copy Markdown
Contributor Author

Workers Builds: codeviewer (Cloudflare) is red here, but the failure doesn't come from this PR.

  • It fails the same way on fix(theme): buttons follow the theme, and system follows macOS #20, which merged to main before any of these changes.
  • It completes within the second it starts, which points to a setup problem in the Cloudflare project rather than a build that ran.
  • This PR doesn't touch anything that check builds: docs/ and scripts/site.ts are unchanged.

There's no fix to make from the repository. The Cloudflare project's build settings need a look in the dashboard.

CodeQL reported one new high-severity alert on the earlier head. I couldn't read the alert from this session. The settings PATCH merge wrote request-supplied keys onto a plain object (cur[k] = v), which is the likeliest match, so e306c86 merges through a Map instead and adds a test with __proto__/constructor keys. CodeQL runs again on that push.


Generated by Claude Code

jkyberneees commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

CodeQL still reports one new high alert on e306c86. The alert turned out to be in a test, not product code: an assertion in test/client.hidden.test.ts that stripped tags with /<[^>]+>/g (CodeQL's incomplete multi-character sanitization rule). 9c7a060 replaces it with an exact unwrap of the markup markBidi adds. The settings Map change in e306c86 stays, as hardening.


Generated by Claude Code

…omplete sanitization)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
…ecurity rules

Fixes (each reproduced first):
- build: never autoload bunfig.toml/.env in the compiled server (a repository's preload ran in it);
  the Mac app starts the server in /.
- git: no index refresh and no hooks (git diff wrote the index and ran post-index-change); special
  files in .git (a /dev/zero symlink, FIFOs) turn git off; GIT_DIR-style variables dropped.
- tokens: the app window runs --incognito (a terminal process could read the session token from
  the profile and type into other terminals); boot page removed on connect and on quit; token URL
  printed only to a terminal; no-store JSON; CORP same-origin.
- search: startup root, stat before open, opened-fd containment check.
- pty: a ModeTracker that mirrors the client parser (bracketed paste could be dropped on replay).
- updater: strict install detection, apply installs only its own newer release, capped reads.
- terminal: bell/title coalescing, legacy mouse limit, DECRQM prefix, references with command
  substitution refused; SVG previews from data: URLs; CSS lexer and stripLinks made linear;
  WebRTC disabled in the app window; Mac relaunch gated, ln -sfh; codeviewer open local-only.

Tests: invariants.static, http.routes (every route), redos.matrix (every language), terminal.fuzz,
files.resolve.fuzz, git.hostile-config, pty.tracker.diff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
- codeview: one bidi marker per run, at most 32 per line (a marker per character, my regression,
  made 248 000 elements); HEAD peek capped at 500 rows x 4000 columns.
- images: SVG <use> bombs (expanded reference graph, cycles) and raster decompression bombs
  (PNG/GIF/JPEG/WebP/BMP headers, 50 Mpx) are refused before decoding.
- highlighter: lex at most 2000 columns, regex literals scan 512 characters, shell ${...} stops
  at the next {; the slow-input matrix gained multi-character openers.
- diff: middles over 200 000 lines are one replaced hunk without interning.
- blame: a new request aborts the previous one and the server kills its git process.
- test: the palette timing comparison takes the best of five runs (it flaked 1 run in 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
…n tests written as escapes

The budget test counted raw controls with a replace(/<span…/) (the pattern CodeQL flags as incomplete
sanitization); it now counts characters. Raw U+202A-202E/2066-2069 in seven test files are \\u escapes
(same values, no hidden characters in review), and a hygiene test keeps it that way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
test (macos-15) failed 'many list lines: doubling the input costs less than 3x' at 3.0x. The parser
grows 2.1x per doubling; all test files share one process, and the heavier suite on this branch left
garbage whose collection landed inside a sample. The timing helpers in markdown.limits, fuzzy and the
slow-input matrix now run Bun.gc(true) before each sample (best of five in markdown.limits). With a
large live heap the measured ratio peaked at 2.85x before and 2.48x after; the bounds are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Px3ERqxxjWWCw6ty21M5S
@jkyberneees jkyberneees changed the title fix: block git lazy fetch, Trojan Source in code view, and other bugs Security sweep: hostile repositories, terminal, updater, DoS; tests that pin the security rules Nov 3, 2026
@jkyberneees
jkyberneees merged commit cc86317 into main Nov 3, 2026
4 of 5 checks passed
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.

3 participants