Repository navigation
Conversation
Release cherry-pick
|
There was a problem hiding this comment.
Code Review
This pull request enhances the security of Unix Domain Socket (UDS) mirrors by ensuring the sockets directory is restricted to owner-only access (0o700) across all worker environments. It introduces the ensureSocketsDirectory helper to enforce these permissions, updates metadata writing to be atomic, and adds comprehensive integration and unit tests. The review feedback correctly identifies a style guide violation in the new unit test file, recommending that Node built-in modules use the node: prefix for imports.
The TLS UDS mirrors skip TLS and trust the PROXY v2 identity a fronting proxy passes, so the sockets directory is their only access gate. It was created with the default mode (0755 under a typical umask). Every creator now forces it to 0700 before binding (http.ts, threadServer.js Bun and raw TLS paths), tightening a pre-existing directory with a logged warning, and skips that mirror when the directory cannot be secured. writeUdsMetadata publishes the sibling yaml through atomicWriteFile, so a reader never sees a truncated file while overlapping workers or cert reloads rewrite it. Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
…rkers loudly ensureSocketsDirectory now computes the sockets path and returns it, or undefined when a shared worker cannot secure it. The path join no longer runs for servers without a UDS mirror, and the three creators share one guard. An isolated application's worker is served only through its mirror, so its failure throws instead of starting without a listener. Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
An isolated worker's directory failure was thrown after the server was cached, so later registrations reused a listener with no mirror and the worker still reported started. The check now runs before the cache assignment (http.ts) and before any registration (raw TLS onSocket), so a failure leaves nothing registered and the component load reports it. Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
A mirror that cannot be secured is skipped and logged. An isolated application is reachable only through its mirror, so its worker now fails startup when the worker registered none, instead of reporting ready with no ingress. The directory helper no longer throws, which leaves server registration unchanged. Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
A mirror whose bind failed stays registered, so the readiness check must skip paths in failedUdsPaths or an isolated worker reports ready with nothing listening. Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
Other suites in the same mocha process re-point the base path, so a path captured at module load names a directory the helper never touches. Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
…tory suite Dispatch-Task: harper-uds-mirror-dir-hardening Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7YjJTe8TxQEVdys3cRbrB
4877612 to
3a4111a
Compare
Review found two related gaps: afterEach still ran after a failed beforeEach containment assert (SOCKETS_DIR was assigned before the assert), and the assert itself used startsWith, which treats a sibling directory like <root>-evil as contained. Both could let test cleanup rm -rf a directory outside the sandboxed test root. isWithinTestRoot() resolves and checks a real path boundary, and SOCKETS_DIR is only assigned after it passes. Dispatch-Task: pr-maint-6991910019c44bbcd3ab780c19e52cd5 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review found both wrote the rationale for a reviewer rather than the invariant for the next reader, and one overstated the failed-assertion case (the previous test's validated target stays set, not nothing). Dispatch-Task: pr-maint-6991910019c44bbcd3ab780c19e52cd5 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restricts the TLS unix-domain-socket mirrors'
socketsdirectory to its owner (mode 0700) at every mirror creator, publishes each mirror's metadata.yamlby atomic rename, and stops an isolated application's worker from reporting ready without a bound mirror.⊙ Problem
The TLS unix-domain-socket mirrors skip TLS and trust the PROXY v2 identity a fronting proxy passes, so the sockets directory (
<hdbBase>/sockets) is their only access gate. It is created with the default mode, 0755 under a typical umask, so other local users can traverse it. Separately,writeUdsMetadatarewrote the mirror's.yamlin place, so a concurrent reader or an overlapping worker could observe a truncated file.Background: fix: make UDS mirror cleanup ownership-aware, not path-based (#2035) deferred the atomic write, and Keep per-worker UDS mirror sockets alive across an overlapping HTTP worker restart (#2964) covered restart survival.
💡 Solution
ensureSocketsDirectory()before it publishes or binds a mirror. It creates the directory with mode 0700, or tightens an existing one whose mode differs, logging a warning with the previous mode. Creators: the native HTTP/1.1, HTTP/2 and uWS mirrors inserver/http.ts, and the Bun and raw TLS mirrors inserver/threads/threadServer.js..yamlis published withatomicWriteFile: a temp file in the same directory, then a rename. A reader sees the previous complete file or the new complete file, never a truncated one.After upgrade, an existing
<hdbBase>/socketsdirectory is tightened to 0700 on the first secure-port start.⚖️ Alternatives
Framing-Verdict: better-alternative-exists (53cba34eac9d)
process.umask(0o077)at boot: rejected. The umask is process-wide, so it changes the mode of every file Harper creates, including databases and logs, and it cannot tighten an existing directory.XDG_RUNTIME_DIR): rejected. Fronting proxies discover mirrors at<hdbBase>/sockets, so this changes that contract.mountHdbcreates a fresh root withHDB_FILE_PERMISSIONS(0700), but it does not repair an existing or widened root, so the invariant would still fail on upgraded installs.writeUdsMetadata: rejected in favor of the existingatomicWriteFileinconfig/configUtils.ts, which names a unique sibling, renames, and removes the temp file on either failure. One atomic-write implementation, not two.atomicWriteFileretries withAtomics.waiton the calling thread, andmaxRetries: 0keeps a serving worker from blocking during a certificate reload.🔧 Changes
server/http.ts:ensureSocketsDirectorycreates or tightens the directory and logs the warning and the error path, returning the path orundefined.writeUdsMetadatapublishes throughatomicWriteFile.hasUdsMirrorcounts only mirrors that did not fail to bind. The native mirror calls the helper.server/threads/threadServer.js: the Bun mirror and the raw TLS mirror call the helper in place of their ownmkdirSync; the old import is removed.startServersrefuses ready for an isolated worker with no bound mirror.server/DESIGN.md: the UDS symbol row records the directory as the mirror access gate, the enforcement at each creator, and the readiness rule.unitTests/server/udsMirrorDirectory.test.js: a new suite for the helper and the publication path. It sits outsideudsMirror.test.js, which the Windows gate excludes.integrationTests/server/uds-mirror-overlapping-restart.test.ts: asserts the 0700 mode after startup, and that replacement workers re-tighten a directory widened to 0755 before the restart.✅ Verification
npm run test:unit:mainat1d0366f: 6447 passing, 202 pending, 1 failing. The failure isnonInteractiveSpawn git credential scopinginunitTests/components/gitCredentials.test.js. It inherits the dispatch worker'sGIT_*environment, and with those variables unset the file passes 19/19.npm run test:unit:resourcesat1d0366f: 3951 passing, 54 pending, 0 failing.npm run test:integration:allat1d0366f: 2296 passing, 0 failing, 18 skipped, 6 cancelled. All 6 cancellations are in theOllamaBackendsuite, which needs Ollama models this machine does not have.npx mocha unitTests/server/udsMirrorDirectory.test.jsat4bdde9a: 10 passing. The suite proves that creation yields 0700, that a rename replaces the yaml (the inode changes), that an open descriptor keeps the previous complete generation, and that a non-empty directory at the yaml path does not throw or leave a temp file. The same file also covers tightening a pre-created 0755 directory, restoring an owner-unusable 0500 directory, idempotence, a regular file occupying the path, and a failed temp write keeping the previous yaml (non-root POSIX).npm run test:integration -- integrationTests/server/uds-mirror-overlapping-restart.test.tsandintegrationTests/components/isolated-application.test.ts: 8/8 passing with the readiness check in place. The overlapping-restart test asserts 0700 after startup; it would fail on base, where the default mkdir mode is 0755.npm run lint:required,prettier --checkon the changed files, andnpm run check:design-docs: pass.Not verified:
socketspath occupied by a regular file would show it.-<port>.yamlby path after a 500 ms debounce, and its-<port>.yamlfilename filter skips the.tmpsibling. Not exercised live.🤖 Generated by Claude Sonnet 5 (Claude Code); posted via @kriszyp.
Related PRs: #372 independent (same files, different concern), #562 independent (adds no non-erasable TypeScript syntax), #2532 independent, #2763 independent, #2946 independent, #2956 overlaps (same http.ts area; merged cleanly through this rebase, no conflict), #2981 independent (its admission flow runs before this gate in threadServer.js; no duplication), #3029 independent (no overlapping behavior), #3035 independent (merged; its ownership gates are preserved, this PR is additional), #3069 independent (different DESIGN.md section)
Complexity: complicated
Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi,cursor-muse; rounds=14; full=4 @ b7f9179
Review-Attention: study ~9m (hot: http.ts, threadServer.js; decisions: exact-0700, pool-degrade-vs-fail, gate-placement, any-vs-all-mirrors) @ b7f9179