Repository navigation
Integrate reviewed open PRs, patch Dependabot alerts, block browser attacks on bridge - #25
Merged
Merged
Conversation
Supersedes dependabot PRs #1, #6, #7, #13, #14, #17 and #19, which were opened against versions that have since received further advisories (fast-uri, hono, qs, ip-address, adm-zip). Bumps each vulnerable package to its current patched release instead: - ws 8.20.0 -> 8.21.3 (direct; bridge range raised to ^8.21.3 so npm consumers of tb-bridge also get the fix) - adm-zip 0.5.17 -> 0.6.1 (dev; XPI build output verified byte-identical) - hono 4.12.12 -> 4.13.7, @hono/node-server 1.19.13 -> 1.19.17 - fast-uri 3.1.0 -> 3.1.7, qs 6.15.0 -> 6.16.0, body-parser 2.2.2 -> 2.3.0 - ip-address 10.1.0 -> 10.7.0, express-rate-limit 8.3.2 -> 8.7.0 npm audit: 9 vulnerabilities (5 high) -> 0. All tarball integrity hashes resolve from registry.npmjs.org; none of the updated packages declare install scripts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019uoUWTHVsDJpMJCVNFZz5L
The bridge binds to 127.0.0.1, which prevents remote access but is not access control: every process running as the same OS user can call it and read the whole mailbox, whether or not it was wired up to the CLI. SPEC.md already documented TB_AUTH_TOKEN and both clients already send it as `Authorization: Bearer <token>`. The daemon never checked it, so the roadmap item "Auth token support (bridge + CLI)" was only half done. The bridge now compares the header against TB_AUTH_TOKEN in constant time and answers 401 when it is missing or wrong. Behaviour is unchanged when the variable is unset, so the default zero-config setup still works. Two deliberate choices: - An empty TB_AUTH_TOKEN aborts startup rather than disabling auth. Failing open would leave the bridge unauthenticated with nothing in the log to say so. - The startup banner reports whether auth is enabled, so the state of the control is visible without probing for it. The 401 body carries code AUTH_REQUIRED because clients map an unrecognised 4xx to THUNDERBIRD_ERROR, which would misreport a missing token as a Thunderbird fault. Scope is the HTTP listener only. The WebSocket listener on :7701 that the extension connects to still accepts any local connection; closing that needs a change on the extension side and is left for separate work. Tests spawn the real bridge process rather than a mock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…; add heartbeat
A web page in the user's browser could drive the localhost bridge:
- Blind cross-origin POSTs executed even though CORS hid the response
(verified: a text/plain POST /compose {send:true} from
Origin: https://evil.example reached the extension).
- DNS rebinding let a hostile domain read responses same-origin.
- Any page could open ws://127.0.0.1:7701 and replace the extension
socket, receiving every CLI request and answering with forged data.
The bridge now rejects HTTP requests with an Origin outside
TB_BRIDGE_CORS_ORIGINS, Host headers that are not an IP literal,
localhost, *.localhost, *.internal (host.docker.internal) or listed in
TB_BRIDGE_ALLOWED_HOSTS, and WebSocket handshakes from http(s)/file/null
origins. tb, tb-mcp and curl send no Origin header and are unaffected;
the extension connects from moz-extension://.
The CORS allowlist and WebSocket ping/pong heartbeat (drops sockets that
stop answering, e.g. after sleep) come from PR #20.
test/bridge-security.mjs runs the real bridge: 27 checks, 16 of which
fail against the previous bridge.
Co-authored-by: le-dawg <3172733+le-dawg@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uoUWTHVsDJpMJCVNFZz5L
… bounded parallelism Integrates the extension parts of PR #4 and PR #20, with fixes: Thread (from #4) - Parse References / In-Reply-To / Message-ID from getRaw() (getFull() doesn't expose them) and strip angle brackets before messages.query({ headerMessageId }) — previously `tb thread` always returned an empty list. getFull() fallback now strips brackets too. - Downstream replies found by subject are limited to exact normalized subject matches (Thunderbird's query is substring), exclude junk, and are labelled threadMatch: "subject" vs "references". - thread-utils.js is loaded by the manifest and is the single source for the parsing helpers instead of a test-only copy. Recent / search (from #4) - /recent honours accountId, and now unreadOnly as well; the CLI sent both but the extension ignored them. - /messages/search accepts headerMessageId. Connection (from #20, reworked) - Reconnect backoff 3s → 6s → 12s → 15s cap (PR used 60s, which would leave a restarted bridge waiting up to a minute), reset on connect, immediate retry on idle → active ("idle" permission). - Socket handlers are bound to their own socket, so a late close from a replaced socket can't null the live one (main crashed on ws.send). Throughput (from #20) - read-batch, fetch, bulk tag/fetch and thread lookups run through a shared queue capped at 8 concurrent operations, preserving order. - Attachment base64 encoded in 32 KB chunks instead of byte by byte. Not taken from #20: the 30s folder-info cache (stale unread counts right after mark-read; Thunderbird is the source of truth), the /debug/compose-capabilities endpoint, and the 15s resume-poll interval. test/extension.test.mjs loads the real background scripts in a vm with a mocked messenger API (39 checks); against the previous extension the thread, recent, search and reconnect checks fail. Co-authored-by: Daniel Böckenhoff <dboe@ipp.mpg.de> Co-authored-by: le-dawg <3172733+le-dawg@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019uoUWTHVsDJpMJCVNFZz5L
…egration email_search no longer requires `query` when at least one filter is given (from PR #20); with neither it fails with INVALID_ARGS instead of running an unbounded search. Not taken from PR #20's client changes: the per-call /bridge/status preflight (the bridge already answers 503 immediately when the extension is disconnected), the new SEARCH_UNHEALTHY / LIST_UNHEALTHY codes and `timeout !== 30000` override heuristic (error-code contract change whose docs disagreed with the code), and error mapping that dropped the bridge's own `code` (it would have hidden AUTH_REQUIRED from #21). Co-authored-by: le-dawg <3172733+le-dawg@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019uoUWTHVsDJpMJCVNFZz5L
7700 is commonly taken on developer machines — e.g. VS Code auto-forwards it from dev containers, which shadowed the real bridge on the host with EADDRINUSE. Defaults now live on an uncommon pair below the macOS/Linux ephemeral range. Updated: bridge defaults and usage text, extension WS_URL, CLI and MCP client defaults, Dockerfile, .env.example, cli/config.example.json, server.json, docs, skill, architecture.svg. CHANGELOG history entries and the raster assets (architecture.png, social-preview.png, demo.gif) still show the old ports. BREAKING CHANGE: the extension in this release connects to 18771 only. Pair a new bridge with the released 2.0.0 extension via `tb-bridge --port 7700 --ws-port 7701` and TB_BRIDGE_PORT=7700. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmV9wYm7QYGHPZ4YdS66Rw
EADDRINUSE on the HTTP or WebSocket port crashed with an unhandled 'error' event stack trace. The bridge now names the port, prints the lsof command to find the holder, and exits 1. For the WebSocket port it says to free the port rather than move it, since the extension always connects to 18771. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmV9wYm7QYGHPZ4YdS66Rw
Back to the original 7700 (HTTP) / 7701 (WebSocket) defaults, keeping compatibility with the released 2.0.0 extension. The actual cause of the EADDRINUSE was VS Code auto port forwarding, now disabled in the dev container config; the bridge's port-in-use message (24ccb67) stays. This reverts commit 0cf71f2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmV9wYm7QYGHPZ4YdS66Rw
- thunderbird-cli, thunderbird-cli-bridge, thunderbird-cli-mcp and server.json → 1.1.0; extension manifest → 2.1.0 - `tb --version` and the MCP serverInfo read the version from package.json (were stuck at 1.0.0); GET /health reports the loaded extension's manifest version instead of a fixed "2.0.0", so an old extension is visible at a glance - server.json lists TB_AUTH_TOKEN (secret) for the MCP Registry - CHANGELOG [1.1.0]; skill tracks thunderbird-cli-mcp@1.1.0 - release notes template no longer names the XPI after the npm version Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmV9wYm7QYGHPZ4YdS66Rw
This was referenced Sep 14, 2026
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.
Integrates the reviewed parts of all open PRs, patches every open Dependabot alert, and fixes browser-reachable vulnerabilities in the bridge that none of the PRs addressed.
Security
Browser attacks on the localhost bridge (new). Verified against
main:POST /composewithsend: true, and it reached Thunderbird.ws://127.0.0.1:7701and replace the extension socket, then receive every request and return forged results.The bridge now rejects HTTP
Origins outsideTB_BRIDGE_CORS_ORIGINS, non-localHostheaders (extend withTB_BRIDGE_ALLOWED_HOSTS), and WebSocket handshakes from web origins.tb,tb-mcp,curlandhost.docker.internalare unaffected.Auth token (Enforce TB_AUTH_TOKEN on the bridge HTTP listener #21). Optional
TB_AUTH_TOKENon the bridge, taken as-is.Dependencies. One update to the current patched releases: ws, adm-zip, hono, @hono/node-server, fast-uri, qs, body-parser, ip-address, express-rate-limit.
npm audit: 9 findings (5 high) → 0.Fixes and improvements
tb thread(from fix(extension): tb thread always returns empty — getRaw() headers, angle brackets, downstream search #4). No longer returns an empty thread: headers are parsed from the raw message and IDs queried without angle brackets.threadMatch: "subject".thread-utils.jsis loaded by the manifest instead of being a copy only the tests used.tb recent(from fix(extension): tb thread always returns empty — getRaw() headers, angle brackets, downstream search #4). The extension now honours--account, and--unread, which was also ignored.idlepermission.ws.sendcrash onmain.email_searchaccepts filters without a text query.Not taken from #20
/bridge/statuspreflight: the bridge already answers 503 immediately when the extension is disconnected.SEARCH_UNHEALTHY/LIST_UNHEALTHYcodes: they change the error contract, and the docs contradicted the code.code: it would hideAUTH_REQUIRED./debug/compose-capabilitiesendpoint, the 15s resume-poll timer, anddocs/ENERGY_AUDIT_MACOS25.md(AI-generated and already out of date).Supersedes #1, #4, #6, #7, #13, #14, #17, #19, #20, #21. Contributors are credited via
Co-authored-by.Testing
npm test(46),test:mcp(37),test:bridge-auth(15), newtest:bridge-security(27, real bridge process), newtest:extension(34 + 39, real background scripts in a vm with a mockedmessengerAPI). All pass; the new security and extension suites fail againstmain. CI runs the new suites.tbCLI → real bridge → realbackground.jsover a WebSocket, with and withoutTB_AUTH_TOKEN.🤖 Generated with Claude Code
https://claude.ai/code/session_01RmV9wYm7QYGHPZ4YdS66Rw