Dashboard/webhook/Slack hardening (B14, B19, B21) - #371
Merged
Conversation
- B14 (health.ts): live-log SSE 'done' branch now sets closed=true and clears the heartbeat timer (not just the poll timer), so the 15s heartbeat stops firing once a critter finishes instead of leaking forever. - B19 (webhook.ts): de-duplicate the Linear/Jira HMAC verifiers into a single verifyHmacSignature() that compares decoded bytes (not UTF-16 string length) and never throws — a multi-byte/odd-length header now yields a clean 401 instead of a RangeError -> HTTP 500. - B21 (slack.ts): add escapeSlackText() and apply it to every untrusted field (title/error/reason/repoUrl/prUrl/phase) so issue content can't inject Slack control sequences like <!channel>, <@u123>, or <url|text>. - auth.ts: compare the dashboard token with crypto.timingSafeEqual behind a byte-length guard (header and cookie) to remove the timing oracle. Adds src/__tests__/audit-web-hardening.test.ts covering all four. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- webhook.ts: drop the dead try/catch around Buffer.from(hex); in Bun it never throws (malformed/odd hex truncates), so the byte-length compare is what rejects bad input. Behavior unchanged; removes unreachable code. - slack.ts: stop escaping URL fields (prUrl, repoUrl). Escaping '&' to '&' corrupts query strings and breaks Slack auto-linking. Free-text fields (title, error, reason) are still escaped. - audit-web-hardening.test.ts: strengthen the B19 regression to reproduce the original bug with a 64-code-unit / 65-byte header that threw on the old string-.length guard; assert the verifier returns false without throwing. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
resolvePhaseMcpConfig expands ~ via node:os homedir(); the test compared it to process.env.HOME. Bun caches homedir() on first call and ignores later process.env.HOME mutations, so when a sibling test changes HOME they diverge on Linux CI and this test fails (works on macOS by execution-order luck). Assert against the same homedir() the implementation uses. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This was referenced Jul 18, 2026
Merged
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.
Summary
Resource-leak and untrusted-input hardening across the HTTP/notification surface.
timingSafeEqual→RangeError→ HTTP 500 instead of 401 on a multi-byte headerverifyHmacSignature<!channel>/<@U>/<url|text>injection&<>in free-text fields (URL fields left intact so query strings aren't corrupted)===crypto.timingSafeEqualwith a length guard (header + cookie)Tests
audit-web-hardening.test.ts— a 64-UTF-16-unit/65-byte header that threw on the old code now returnsfalsewithout throwing; valid sig accepted; Slack escaping neutralizes<!channel>while URLs pass through; timing-safe accept/reject.Verification
typecheck clean · lint exit 0 ·
bun test861 pass / 0 fail.🤖 Generated with Claude Code