Skip to content

[security-audit] FAIL on 2026-10-01 #873

Description

@github-actions

Audit failed at 2026-10-01T11:05Z. Run · Transcript

  • A domain returned FAIL. audit-application.md opened with VERDICT: FAIL — read that domain's section first. A domain's own verdict outranks the merged one.

Lines that decided this verdict

Lifted out of the fragments so truncation cannot cut them. Each domain's full section follows for as far as the body reaches.

  • audit-supply-chain.md: VERDICT: PASS
  • audit-ci-secrets.md: VERDICT: PASS
  • audit-application.md: VERDICT: FAIL
  • audit-hosted.md: VERDICT: PASS
  • audit-application.md: - FAIL: [local/Loopback] Lint-coverage clause (scans all tracked JS/TS and prints every bind; new unguarded non-test listener fails the build) — hosted/server/dev.ts:45 server.listen(Number(process.env.PORT || 0), "127.0.0.1") is a loopback bind the node-positional BIND_FORM (\.listen\(\s*[^,)]+,) cannot see because [^,)]+ stops at the inner ); node scripts/loopback-lint.mjs output omits it. Listener itself is guarded, so impact is lint drift, not exposure.
  • audit-application.md: - FAIL: [remote/One-time connection] Local networks: the offer the Burrow applies CAN keep a candidate outside the allowed networks — evidence: lib/src/host/remote/local-networks.ts CANDIDATE_LINE=/^a=candidate:\S+ \S+ \S+ \S+ (\S+) / and keepAllowed keeps every non-matching line; split on CRLF only when present. Verified: a candidate with a doubled space or tab before the address, or one after a bare LF inside a CRLF SDP, does not match (line kept), and node-datachannel 0.33.4 setRemoteDescri
  • audit-application.md: - FAIL: [remote/Network posture] (low impact) Tailscale CLI output piped into head -1 for a verify decision on macOS and Linux — evidence: deploy/local/install-macos.sh:902 and deploy/local/install-linux.sh:1131 tsip="$(ts ip -4 2>/dev/null | head -1 || true)"; the plaintext-reachability verdict is reached from that pipe rather than from captured text. The trailing || true absorbs a 141 and ts ip -4 output is tiny, so no wrong verdict is reachable today, but the rule text forbids the shape; Wi
  • audit-application.md: - WARNING: [local/Terminal output] OSC 367 open / Tool designation gate trusts a PTY-forgeable command line. use-dor-control.ts:1110-1116 admits oscOpen when getTerminalPaneState(id).currentCommand.rawCommandLine === tool.command, but rawCommandLine comes from OSC 633;E (terminal-protocol.ts:814-823, the VS Code nonce field is discarded, never checked) or OSC 133;C cmdline, then any OSC 633/133;C commandStart (terminal-state.ts:181-195, :446-452) — all accepted from any program output. A later
  • audit-application.md: - WARNING: [local/Loopback] scripts/loopback-lint.mjs misses a real loopback bind: hosted/server/dev.ts:45 server.listen(Number(process.env.PORT || 0), "127.0.0.1") — the node-positional BIND_FORM \.listen\(\s*[^,)]+, stops at the inner ) of Number(...), so the lint output omits the file. The listener is guarded (hosted/server/dev-host-guard.ts allowedDevRequest), but security-local.md claims the lint prints every bind and fails an unguarded new one; any port expression containing a call
  • audit-application.md: - WARNING: Tailscale CLI output is piped into head -1 for the manage verify plaintext-reachability decision on macOS and Linux (deploy/local/install-macos.sh:902, deploy/local/install-linux.sh:1131 tsip="$(ts ip -4 2>/dev/null | head -1 || true)"), the shape security-remote.md Network posture forbids; Windows captures first (install-windows.ps1:1579-1581). The || true and the one-line output make a wrong verdict unreachable today, but scripts/deploy-lint.mjs does not catch this site (lint green)
  • audit-application.md: - WARNING: [Local networks / One-time + Pocket direct path] localNetworksPath.acceptRemote filters remote candidates with a single-space regex (lib/src/host/remote/local-networks.ts CANDIDATE_LINE and keepAllowed) and splits only on CRLF when present; a candidate line with doubled whitespace/tab before the address, or placed after a bare LF in a CRLF SDP, is kept unfiltered and node-datachannel 0.33.4 accepts it (verified with setRemoteDescription). An approved peer (one-time phone after the cod
  • audit-application.md: - WARNING: External-link validation is a blocklist, not an allowlist: inspectExternalUri (lib/src/lib/external-links.ts:1,38) only rejects javascript:/data:/blob:/about:, so an OSC 8 link printed by untrusted terminal output (e.g. cat of a repo file) can carry file:///…, vscode:///…, ms-*: or any OS-registered protocol handler, and after the one-click confirmation the VS Code host passes it to vscode.env.openExternal (vscode-ext/src/message-router.ts:608-611), which hands non-http sch
  • audit-application.md: - WARNING: File-path paste/drop quotes but never rejects C0/DEL in the filename: pasteFilePaths (lib/src/lib/clipboard.ts:61-69) -> shellEscapePath (lib/src/lib/shell-escape.ts:11-31) -> writePasteToPty, whose unbracketed branch (clipboard.ts:50) writes the bytes raw. On a POSIX shell without bracketed paste (dash, bash with enable-bracketed-paste off, many REPLs), dropping a file named e.g. $'x\x15touch /tmp/pwned\n' yields '''x^Utouch /tmp/pwned''' — ^U kills the line including the ope

Security audit

Supply chain

VERDICT: PASS

FAIL IF results

  • PASS: generate-deps.js against clean tree after install changed none of the three data files (git status clean; 70 npm, 12 direct + 478 transitive cargo, 1 runtime).
  • PASS: .github/workflows/ci.yml:39-46 runs node website/scripts/generate-deps.js and exits 1 on git diff --quiet -- website/src/data/; install precondition is earlier in the job.
  • PASS: root completeness. productDependencyFilters (generate-deps.js:23) = dor, dormouse, dormouse-standalone, dormouse-lib, dormouse-sidecar, relay; exclusions (line 33) = canopy, dormouse-website, dormouse-hosted. All 13 pnpm-workspace.yaml packages covered (remote-lib-common, dor-lib-common, dor-tools-builtin, dor-tools-lib via workspace edges). Sidecar ships via tauri.conf.json bundle.resources "../sidecar/**/*". Excluded packages ship no installed artifact; the only non-registry lockfile tarball (@diffplug/xterm-addon-webgl-sdf) is consumed solely by canopy (excluded).
  • PASS: package.json devEngines.runtime.version = "24.18.0" (exact); (clause 1)
  • PASS: build.rs:43 calls verify_node_version, which runs --version and errors on mismatch (lines 207-224); only skip is host != target. release.yml standalone matrix (x86_64-unknown-linux-gnu on ubuntu, aarch64-apple-darwin on macos-latest, x86_64-pc-windows-msvc on windows-latest) is all host-native.
  • PASS: release.yml build-standalone uses node-version-file: package.json (line 48); root package.json has no volta or engines.node.
  • PASS: pnpm-workspace.yaml has minimumReleaseAge: 1440.
  • PASS: minimumReleaseAgeExclude = pgstencil, @pgstencil/* only; the only renovate minimumReleaseAge: null rule matches pgstencil and @pgstencil/**.
  • PASS: renovate.json enabledManagers includes npm and cargo; minimumReleaseAge package rules exist for ["npm","cargo"] (patch/minor/major).
  • PASS: renovate.json vulnerabilityAlerts block sets minimumReleaseAge "1 day" explicitly.
  • PASS: secret_scanning enabled, secret_scanning_push_protection enabled (AUDIT_PAT); vulnerability-alerts returned HTTP 204.

Qualitative findings

  • INFO: Lockfile resolves one non-registry package (@diffplug/xterm-addon-webgl-sdf GitHub release tarball, integrity-hashed), used only by canopy, which is excluded and not in any production build.
  • INFO: No hasInstallScript/requiresBuild entries in pnpm-lock.yaml; build scripts governed by allowBuilds in pnpm-workspace.yaml.
  • INFO: No new product runtime dependencies identified beyond what the regenerated disclosure already matches.

CI and secrets

VERDICT: PASS

FAIL IF results

  • PASS rulesets: Merge access (16757376) branch ~DEFAULT_BRANCH rules update/deletion/creation, sole bypass RepositoryRole 5; Tag operations (16757382) ~ALL tag creation+update, same bypass; both active.
  • PASS dormouse-bot permission: permission=write, role_name=write (no maintain/admin).
  • PASS actions/permissions/workflow: default read, can_approve false.
  • PASS workflow-audit.yaml active; last successful runs 2026-09-30T13:37Z (<48h) and daily before.
  • PASS environments: all 7 custom branch policies; vscode-extension-publish v* tag; release-attest v* tag; security-audit main + v* tag; tend main; hosted-production/hosted-release-tag main only; hosted-preview main + refs/pull/*/merge. All admin-gated by rulesets.
  • PASS secret inventory: repo = ARGOS_TOKEN, CHROMATIC_PROJECT_TOKEN only; org total 0; AUDIT_PAT only in security-audit; TEND_BOT_TOKEN only tend; CLAUDE_CODE_OAUTH_TOKEN in tend and security-audit only; OVSX_PAT/VSCE_PAT only vscode-extension-publish; no ANTHROPIC_API_KEY; release-attest 0 secrets, 0 vars.
  • PASS Hosted envs: three envs have branch restrictions, reviewers nedtwigg/edgartwigg, can_admins_bypass false; no hosted creds at repo/org; hosted-preview holds CLOUDFLARE_API_TOKEN/NEON_API_KEY/PREVIEW_AUTH_SECRET (names differ from production's DATABASE_URL/BACKUP_AGE_IDENTITY; values unreadable).
  • PASS HOSTED_TAG_TOKEN only in hosted-release-tag (API); workflow-usage check below.
  • PASS vscode-extension-publish: reviewers nedtwigg+edgartwigg, prevent_self_review true, can_admins_bypass false.
  • PASS private vulnerability reporting enabled (security.md:199).
  • INFO: tend and release-attest have can_admins_bypass true; spec requires no such setting for them.
  • PASS pull_request_target: no on: use outside tend-*.yaml (only a comment, hosted-preview.yml:75).
  • PASS effective write perms, non-agent workflows: ci/argos/hosted-* default contents: read; release.yml top-level contents: read, build jobs id-token+attestations write, security-audit job actions: write (release.yml:261) only.
  • PASS agent-managed effective perms: tend-* jobs contents/pull-requests/issues write + actions read; security-audit.yaml and workflow-audit.yaml within allowed set; repo default read.
  • PASS tend merge: .config/tend.yaml:2 merge: restricted; all 8 tend-*.yaml merge: restricted.
  • PASS tend pin: all 8 at max-sixty/tend/claude@0.3.5 (>=0.1.19). Tag pins only inside tend-*.yaml; every other workflow uses 40-hex SHA pins (grep found none unpinned).
  • PASS secrets.allowed in .config/tend.yaml lists CHROMATIC_PROJECT_TOKEN and ARGOS_TOKEN.
  • PASS Renovate: .github/renovate.json packageRule disables github-actions manager for .github/workflows/tend-*.yaml.
  • PASS workflow-audit.yaml lower bound: previous run created_at (line 89); WINDOW (line 147) includes .github/audit/ and .vscode/.
  • PASS tend regeneration / workflow-audit boundary: scripts/workflow-audit.test.mjs passes.
  • PASS VS Code release: publish-vscode has environment vscode-extension-publish (release.yml:317); VSCE_PAT/OVSX_PAT referenced only release.yml:347,359 in that job; no production signing secrets in release.yml (only GITHUB_TOKEN); ephemeral Tauri key generated (release.yml:70-81).
  • PASS Hosted workflows: preview deploy requires same-repo head and needs verify; cleanup checks out refs/heads/main; production tag job needs: deploy whose last step is live smoke; HOSTED_TAG_TOKEN used only in tag job (hosted-production.yml:117) bound to hosted-release-tag.
  • PASS sign-and-deploy.sh: attestation verify (l.455), sha256 manifest (l.422), PIV jsign (l.763); no --private-key on argv (only a comment l.834); jsign --storepass env:EV_SIGN_PIN. scripts/sign-and-deploy.test.mjs passes.
  • PASS security-audit.md: security-audit.yaml active, dispatch/watch/needs edge present (release.yml:249-310); scopes in .github/audit/ files cover security.md, security-ci.md, security-audit.md; opus agents for application-security/hosted in claude_args (l.152, --model sonnet l.149) and security-audit-local.sh l.69-70; prompt files all exist; orchestrator until-loop with persisted audit-deadline and sentinel; BASH_DEFAULT_TIMEOUT_MS 600000, timeout-minutes 40; redactor step covers fragments, report, transcript, fails closed (l.211-255); reporting step exact-verdict handling (l.396-402); AUDIT_PAT pre-check present (l.69-76); scripts/security-audit.test.mjs passes; spec-lint OK.
  • Not individually re-derived in depth: security-audit.md top-level-path coverage by union of qualitative scopes and the per-prompt wording of the reporting/orchestrator FAIL IF bullets were spot-checked via greps and the passing security-audit.test.mjs, not read line by line.

Qualitative findings

  • INFO: environments tend and release-attest have can_admins_bypass=true; security-ci.md requires disabled bypass only for hosted and vscode-extension-publish, so not a violation.
  • INFO: .vscode/tasks.json has no runOn/folderOpen task; .claude/settings.json allowlist is narrow and read-only/test commands.
  • INFO: working tree shows an unrelated modification to lib/src/remote/direct/ice-servers.ts (not from this audit); no finding.
  • INFO: audit-*.md fragments are written at repo root by this run; they are the audit's intended outputs.

Application security

VERDICT: FAIL

FAIL IF results

  • PASS: [local/Terminal output] FAIL IF maybe: drop node-pty for a Rust backend #1 clause (a) isKnownUnsupportedIterm2Osc consumes OSC 52 (and 50) — evidence: lib/src/lib/terminal-protocol.ts:912-920 returns true for 52/52;; called at :319 so parseOsc returns []; 50/52 also in OSC_CONSUMED_IDS :106; test lib/src/lib/terminal-protocol.test.ts:248,435
  • PASS: [local/Terminal output] FAIL IF maybe: drop node-pty for a Rust backend #1 clause (b) every parse site runs TerminalProtocolParser before pty:data leaves it — evidence: standalone sidecar sidecar-entry.ts:185 sends pty:data from createOwnerPtyStream onChunk (processed-pty-stream.ts:87-97 parser.process first); vscode-ext message-router.ts:248 feeds raw data to getOwnerPtyStream and :487 posts only visibleData from onProcessedPtyData; replay via replay-parse.ts:17; fake-adapter.ts:187,468
  • PASS: [local/Terminal output] FAIL IF Leaky-bucket mechanism for soft-TODOs #2 clause (a) retained parser values bounded and control-stripped — evidence: TITLE_LIMIT=256/BODY_LIMIT=4096 terminal-protocol.ts:91-92 applied via sanitizeText (osc-sanitize.ts:17 strips C0/C1, clamps by code point) at :337,371-372,451-452,900,997; MAX_CWD_LENGTH=4096 with boundedCwdValue stripping [\x00-\x1f\x7f-\x9f] terminal-state.ts:768-789 used at :252,261,265,279,697
  • PASS: [local/Terminal output] FAIL IF Leaky-bucket mechanism for soft-TODOs #2 clause (b) COMMAND_LINE_LIMIT binds after unescape with a pre-decode width bound — evidence: commandLineEvents terminal-protocol.ts:721-729 truncateText(encoded, COMMAND_LINE_LIMIT*encodedWidth) then decode then sanitizeCommandLine(...,COMMAND_LINE_LIMIT); encodedWidth 4 for \xNN shell-quoted (:709), 12 for percent-encoded; matches 4x bound for \xNN
  • PASS: [local/Terminal output] FAIL IF Leaky-bucket mechanism for soft-TODOs #2 clause (c) no new unbounded retained value observed — evidence: OSC99 pending accumulators bounded by OSC99_PENDING_TITLE_LIMIT/BODY_LIMIT via appendLimited terminal-protocol.ts:100-101,438-440; id bounded :1008
  • PASS: [local/Loopback] FAIL IF origin rewrite (handleRequest) — upstreamRequestHeaders rewrites Origin only when isOwnOrigin (lib/src/host/iframe-proxy.ts:226); foreign Origin left untouched in the spread copy.
  • PASS: [local/Loopback] FAIL IF origin rewrite (handleUpgrade) — handleUpgrade uses the same upstreamRequestHeaders (lib/src/host/iframe-proxy.ts:462).
  • PASS: [local/Loopback] FAIL IF Cookie forwarded upstream (HTTP + WS) — delete headers.cookie in shared upstreamRequestHeaders (lib/src/host/iframe-proxy.ts:227), used by both paths.
  • PASS: [local/Loopback] FAIL IF Set-Cookie downstream (HTTP) — sanitizeResponseHeaders skips set-cookie (lib/src/host/iframe-proxy.ts:399); passThrough and streamHtml both go through it.
  • PASS: [local/Loopback] FAIL IF Set-Cookie downstream (WS 101 and refused upgrade) — 101 rawHeaders loop skips set-cookie (iframe-proxy.ts:477); refused upgrade goes via passThrough->sanitizeResponseHeaders (iframe-proxy.ts:488-492).
  • PASS: [local/Loopback] FAIL IF Host check dropped — isLoopbackHost(req.headers.host, grant.port) gates handleRequest (iframe-proxy.ts:232, 421) and handleUpgrade (iframe-proxy.ts:456, socket.destroy).
  • PASS: [local/Loopback] FAIL IF framing headers dropped without exact replacement — FRAMING_RESPONSE_HEADERS dropped only when embedderOrigins!==null and then frameAncestorsCsp(chain) = frame-ancestors self + chain appended (iframe-proxy.ts:405-415; iframe-proxy-rewrite.ts:84-86); preserve-csp only retains stricter upstream CSP and still appends.
  • PASS: [local/Loopback] FAIL IF shim targets other than own proxy origin + innermost chain origin — send() posts only to location.origin and TARGET (iframe-proxy-rewrite.ts:119), TARGET = embedderOrigins[0] = webview location.origin (iframe-proxy.ts:268, lib/src/lib/embedder-origins.ts).
  • PASS: [local/Loopback] FAIL IF no-chain case injects or strips — normalizeEmbedderOrigins all-or-nothing returns null (iframe-proxy-rewrite.ts:67-77); replaceFraming false keeps headers; streamHtml skipped when embedder undefined (iframe-proxy.ts:273); error page instrumented only with chain (iframe-proxy.ts:519-524).
  • PASS: [local/Loopback] FAIL IF foreign Origin refreshes idle timer / absent must refresh — guarded by !isForeignOrigin on both paths (iframe-proxy.ts:245, 460); isForeignOrigin false for absent Origin (lib/src/host/loopback-guard.ts:79-81); Host refusal precedes refresh.
  • PASS: [remote/Network posture] deploy-lint, deploy-lint-selftest, installer-verify-test, ps1-cmdlet-lint all run green — evidence: deploy-lint OK (3 installers, 35 rules, 93 checks); selftest OK (124 load-bearing checks); installer-verify-test OK (64 checks); ps1-cmdlet-lint OK (677 calls)
  • PASS: [local/Terminal output] FAIL IF Fix terminal spawning #3 clause (a) no OSC 8 activation reaches openExternal without the dialog — evidence: linkHandler.activate terminal-lifecycle.ts:156-161 calls only activateTerminalLink; terminal-link-activation.ts:35-37,51 routes every non-preview link and every refused preview to requestExternalLinkConfirmation; the only dialog-side openExternal is ExternalLinkModalHost.tsx:26 inside confirm; other openExternal callers (ExternalTextLink.tsx:19, Wall.tsx:2092 port menu) are not OSC 8 paths
  • PASS: [local/Terminal output] FAIL IF Fix terminal spawning #3 clause (b) dialog renders no open action for a deceptive verdict — evidence: ExternalLinkModal.tsx:106-127 deceptive branch renders only Close + Copy deceptive URL, initialFocusRef=primaryButtonRef (copy) at :81; classifyDisplayMatch external-links.ts:70-83 returns deceptive when URL-shaped display host differs; host also refuses: ExternalLinkModalHost.tsx:25 requires verdict !== deceptive
  • PASS: [local/Terminal output] FAIL IF Fix terminal spawning #3 clause (c) second normalizeExternalUri pass before opening — evidence: tauri-adapter.ts:477-478, browser-sidecar-adapter.ts:268-269, vscode-ext message-router.ts:608-610 (extension host normalizes, drops on null)
  • PASS: [local/Terminal output] FAIL IF split a pane and keep the cwd #4 clause (a) preview path only when display text is a whole-component suffix of the decoded target — evidence: localFileLinkPreviewPath external-links.ts:99-119 decodes pathname, rejects controls, and accepts only path===text or path.endsWith(/+text) (one ls -F classifier stripped); activateTerminalLink terminal-link-activation.ts:33-37 sends null to the dialog
  • PASS: [local/Terminal output] FAIL IF split a pane and keep the cwd #4 clause (b) host opens a file: URL only for empty, localhost, or this machine — evidence: resolveLocalToolTarget tool-input.ts:23 routes scheme input to localFileUrlPath :45-61 which throws unless namesThisHost(url.hostname) :65-70 (empty after WHATWG localhost folding, own hostname, or its short form); webview-side PLAIN_HOSTNAME_RE external-links.ts:106 is only a shape filter
  • PASS: [local/Loopback] FAIL IF browser viewer upgrades without own loopback Host — upgrade handler requires isLoopbackHost(req.headers.host, port) before consume (lib/src/host/browser-viewer.ts:101-108); plain HTTP always 403.
  • PASS: [local/Loopback] FAIL IF browser viewer upgrades without single-use 60s grant for one view — BrowserStreamGrants.consume deletes on read, expires now+60_000, 32-byte random token (lib/src/host/browser-stream-guard.ts); path regex /view/[a-f0-9]{64} (browser-viewer.ts:89).
  • PASS: [local/Loopback] FAIL IF provider receives a webview message not rebuilt — every inbound message goes through parseViewerInput (browser-viewer.ts:332), which builds fresh objects field by field (browser-viewer.ts:476-525) before upstream.input (browser-viewer.ts:344).
  • PASS: [local/Loopback] FAIL IF host dials agent-browser stream off loopback — viewStream hardcodes ws://127.0.0.1:${port} (lib/src/host/agent-browser-host.ts:560).
  • PASS: [local/Loopback] FAIL IF host dials CDP off loopback — askCdpEndpoint refuses unless /^ws://(127.0.0.1|localhost):\d+// (agent-browser-host.ts:347); all CDP dials (cdpEndpoint, browserCall) go through it.
  • PASS: [remote/What crosses the boundary] push registration rejects non-public endpoints (https only, no userinfo, no localhost, non-public IP literals) — evidence: relay/src/push-endpoint.ts:102-113, called at relay/src/app.ts:1115
  • PASS: [remote/What crosses the boundary] delivery uses createPublicPushAgent/createPublicLookup — evidence: relay/src/push.ts:260 agent=createPublicPushAgent(), passed at :278; push-endpoint.ts:165-166 Agent({lookup: createPublicLookup()})
  • PASS: [remote/What crosses the boundary] mixed public/blocked DNS answers rejected wholesale — evidence: relay/src/push-endpoint.ts:150 addresses.find(!isPublicNetworkAddress) -> error; empty answer also rejected :146
  • PASS: [remote/What crosses the boundary] /api/push/send takes burrowId from token — evidence: relay/src/app.ts:1232 const { burrowId } = c.get(burrow) (requireBurrow)
  • PASS: [remote/What crosses the boundary] send does not select recipients when absent/empty, clamps at MAX_PUSH_QUERY_DELIVERY_IDS — evidence: relay/src/app.ts:1213-1217 (!Array.isArray || length===0 || length>MAX -> 400)
  • PASS: [remote/What crosses the boundary] read endpoints report only presented delivery ids — evidence: relay/src/app.ts:1145-1173 pushSubscriptionsQuery uses listForDeliveryIds(presented ids); delete always 204 :1180-1187; pushDevices lists ids only to the Burrow token (requireBurrow) :1189
  • PASS: [remote/What crosses the boundary] send route neither reads/rewrites/logs text; forwards burrowId + sealed fields copied individually (no spread) — evidence: relay/src/app.ts:1247-1259 JSON.stringify({burrowId, v: sealed.v, salt: sealed.salt, ct: sealed.ct}); push.ts:224,292 logs endpoint origin only
  • PASS: [local/Loopback] FAIL IF bridge drops per-run token — isAuthorized requires ?t= and compares sha256 digests via timingSafeEqual (standalone/scripts/dev-host-guard.mjs:36-51), gate runs before routing (standalone/scripts/dev-agent-browser.mjs:231).
  • PASS: [local/Loopback] FAIL IF bridge drops loopback Host check — exact 127.0.0.1:port / localhost:port compare inside isAuthorized (dev-host-guard.mjs:37-38).
  • PASS: [local/Loopback] FAIL IF bridge drops application/json on non-GET — enforced in the gate itself (dev-host-guard.mjs:42, isJsonRequest :62-65), so body-less routes are covered.
  • PASS: [local/Loopback] FAIL IF bridge drops exact-origin ACAO — corsHeaders echoes only one of two vite spellings, else viteOrigin, never * (dev-host-guard.mjs:78-85).
  • PASS: [local/Loopback] FAIL IF browser-dev Vite permits cross-origin reads or disables Host check — startDevVite sets cors:false and allowedHosts:[] (standalone/scripts/dev-run.mjs:24-49).
  • PASS: [remote/Trust boundary] FAIL IF Burrow stops being final authority: challenge consumed by Burrow before verify (#challenges.consume) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1357
  • PASS: [remote/Trust boundary] FAIL IF Burrow stops being final authority: verifyPresenceProof against binding of own burrowId, connectionId, burrowChallenge, handshakeHash — evidence: lib/src/remote/burrow/burrow-runtime.ts:1358-1366
  • PASS: [remote/Trust boundary] FAIL IF Burrow stops being final authority: one active BurrowAclRecord holds account, credential, key hash, IK client static (#aclRecord -> acl.authorize on credential+static, then accountId and passkeyPublicKeyHash compared on same record) before #promoteConnection; no Relay claim used (accountId/credential come from verified proof, static from IK handshake) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1374-1405, remote-lib-common/src/security/acl.ts:189
  • PASS: [remote/Trust boundary] FAIL IF BurrowAcl.approve has a caller other than #approvePairing — grep of non-test .approve( on BurrowAcl finds only burrow-runtime.ts:1166 inside #approvePairing (service.ts:802/1019 are PendingPairing.approve closures) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1130-1199
  • PASS: [remote/Trust boundary] FAIL IF comparison not constant-time / not exactly once: constantTimeEqual after pending.attempted=true is set before comparison — evidence: lib/src/remote/burrow/burrow-runtime.ts:1133-1150
  • PASS: [remote/Trust boundary] FAIL IF comparison matched against mutable clientId alone: #approvePairing requires pending.pairingId === pairingId (closure-captured, randomBase64Url minted at reserve) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1119,1132,1050
  • PASS: [remote/Trust boundary] FAIL IF expected code leaves Burrow: PairingQueueItem is exactly {kind, clientId, pairingId, label, requestedAt}; #queueSnapshot projects those five field-by-field — evidence: lib/src/host/remote/service-protocol.ts:110-123, lib/src/host/remote/service.ts:1296-1311
  • PASS: [remote/Trust boundary] FAIL IF answers not routed by kind / missing kind not pairing: approvalKind returns one-time only for kind===one-time; #pendingRequest routes one-time to #oneTimeApproval by random ticket pairingId — evidence: lib/src/host/remote/service-protocol.ts:95-97, lib/src/host/remote/service.ts:814-826,1008-1013
  • PASS: [remote/Trust boundary] FAIL IF pending maps unbounded: BurrowRuntime pairings capped MAX_PENDING_PAIRINGS (#evictOldestPairingIfFull), service #pairings capped oldest-first, connection handshakes MAX_PENDING_CONNECTION_HANDSHAKES, invitations MAX_TOKENS_PER_BURROW — evidence: lib/src/remote/burrow/burrow-runtime.ts:1260-1268,1524-1530,507; lib/src/host/remote/service.ts:1281
  • PASS: [remote/Trust boundary] FAIL IF clientId unbounded at frame boundary / failed handshake allocates: isBoundedString(MAX_CLIENT_ID_LENGTH) on client-gone and isE2eRelayToBurrowFrame before enqueue; #onPairingInit catch returns before #clientState — evidence: lib/src/remote/burrow/burrow-runtime.ts:908,921,1017-1021
  • PASS: [remote/Credentials at rest] AES-GCM confined to the Pocket at-rest wrapper — evidence: grep -rli gcm over remote-lib-common/src, lib/src, relay/src (non-test) returns only lib/src/remote/client/pocket-private-key.ts
  • PASS: [remote/Credentials at rest] relay state dir created 0o700 — evidence: relay/src/state.ts:175 mkdir(stateDir,{recursive:true,mode:0o700}) in writeAtomic
  • PASS: [remote/Credentials at rest] every relay state file written via writeAtomic at 0o600 — evidence: relay/src/state.ts:177 writeFile(tmp,...,mode:0o600)+rename; grep writeFile/appendFile/createWriteStream over relay/src finds only state.ts:177 and runtime-file.ts:39 (runtime file is DORMOUSE_RUNTIME_FILE under run/, not state dir, also 0o600 atomic)
  • PASS: [remote/Credentials at rest] relay retired hosts.json deleted unread at boot — evidence: relay/src/state.ts:404-406 rm(hosts.json,{force:true}); called relay/src/start.ts void forgetRetiredState(stateDir); pinned relay/test/state-records.test.mjs:139
  • PASS: [remote/Credentials at rest] start.ts obtains setup password from SetupPasswordStore.loadOrCreate(generateSetupPassword) — evidence: relay/src/start.ts:48
  • PASS: [remote/Credentials at rest] generateSetupPassword uses crypto.randomBytes(32) — evidence: relay/src/setup-password.ts:7 randomBytes(32).toString(hex)
  • PASS: [remote/Credentials at rest] readConfig reads no DORMOUSE_SETUP_PASSWORD or other setup-password input — evidence: grep SETUP_PASSWORD/setupPassword in relay/src/config.ts returns nothing; env reads at config.ts:96-139 are PORT, BIND_HOST, ORIGIN, STATE_DIR, POCKET_DIR, VAPID_*, RUNTIME_FILE, ENROLL_TOKEN_FILE, RELEASE_ID, REQUIRE_USER_VERIFICATION
  • PASS: [remote/Credentials at rest] SetupPasswordStore refuses persisted or generated values outside 64 lowercase hex — evidence: relay/src/state.ts:286-295 isStoredSetupPassword uses isSetupPassword (HEX_ENCODED_32_BYTES_PATTERN /^[0-9a-f]{64}$/, remote-lib-common/src/remote/enroll-offer.ts:29); loadRecord throws CorruptStateError (state.ts:224); generated mint validated before write (state.ts:245-247); start.ts exits 1 on CorruptStateError
  • PASS: [remote/Credentials at rest] createApp refuses a non-64-lowercase-hex setup password — evidence: relay/src/app.ts:490-495 throws unless isSetupPassword(config.setupPassword)
  • PASS: [remote/What crosses the boundary] push sealed per recipient to that ACL record own Client static — evidence: lib/src/remote/burrow/push-delivery.ts:172-175 loop over records, deps.seal(record.clientStaticPublicKey, plaintext); one envelope per deliveryId
  • PASS: [remote/What crosses the boundary] fresh salt per message, key minted per message (HKDF over X25519), zero nonce spent once; no CipherState/group key — evidence: remote-lib-common/src/security/push-seal.ts sealPush getRandomValues(32) -> sealKey -> chacha20poly1305(key, ZERO_NONCE); test remote-lib-common/test/push-seal.test.mjs:80-105,204
  • PASS: [remote/What crosses the boundary] BurrowRuntime.sealPushForClient hands push-delivery a seal capability, not the private key — evidence: lib/src/remote/burrow/burrow-runtime.ts:451-465; lib/src/host/remote/service.ts:1350 seal closure; AlertPushDeps.enrollment typed Pick<relayUrl|burrowToken> push-delivery.ts:89 (runtime object is the full enrollment, see qual INFO); key imported nonextractable remote-lib-common/src/security/noise.ts:321-326
  • PASS: [remote/What crosses the boundary] the Pocket service worker is the only openPush caller — evidence: grep openPush( in lib/src and remote-lib-common/src (non-test) finds only lib/src/remote/pocket-app/sw.ts:89 and the definition push-seal.ts:144
  • PASS: [remote/What crosses the boundary] push text bounded with boundedPushText on the Burrow before sealing — evidence: lib/src/remote/burrow/push-delivery.ts:84 toPushText, :166-170 title/tag bounded before utf8Encode and seal
  • PASS: [remote/What crosses the boundary] push text re-bounded with boundedPushText in sw.ts before showNotification — evidence: lib/src/remote/pocket-app/sw.ts:102-110 title/body/tag via boundedPushText; showNotification at :148 receives only that output or GENERIC
  • PASS: [local/Terminal output] FAIL IF UI improvements #5 clause (a) OSC 367 open never reaches surface.tool from replay — evidence: parseReplay lib/src/lib/platform/replay-parse.ts:16-21 calls only recordToolEvents (tool-events.ts:23-29 handles announce/state/commandStart, never toolOpen); dispatchToolOpens reached only from applyLiveToolEvents tool-events.ts:33-36, called from live terminal:toolEvents handlers vscode-adapter.ts:126, tauri-adapter.ts:157, browser-sidecar-adapter.ts:332, fake-adapter.ts:477

Truncated to fit: the full body is 121381 characters. The untruncated audit-report.md is in this run's audit-transcript artifact (download).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions