Commit Graph
2 Commits
Author SHA1 Message Date
Bernd RodlerandClaude Opus 4.8 a4155aa342 security(smime): fix finding 5 (parser DoS) and harden finding 4
Finding 5 — the MIME parser runs on attacker-controlled input: the inner
content recovered after decrypt/verify is whatever the sender put there.
Upstream had no depth limit on nested multiparts and no size cap anywhere.

Verified against the unpatched upstream parser with the same input:

  UPSTREAM CRASHED: RangeError - Maximum call stack size exceeded
  UPSTREAM: 65MB accepted (no size cap)

So this was a live decrypt-time DoS reachable by anyone who can send mail.

Caps added: depth 20, parts 500, bytes 64 MB — generous enough that no
legitimate message comes close (real mail nests 3-4 levels). Past a limit a
subtree degrades to a leaf rather than throwing, so one pathological branch
doesn't discard the legitimate parts above it. Oversize input is refused
outright rather than truncated: half a MIME tree parses into misleading
nonsense, and showing part of a message is worse than saying no. Both
bodyStructure walkers in smime-detect.js are capped too — those run on
server-supplied structure BEFORE any decrypt/verify gate.

Finding 4 — hardened, not eliminated, per the agreed scope. Unlocked
CryptoKeys still live in durable IndexedDB rather than memory; moving them
would mean refactoring how the plugin shares state across iframes and
risking the unlock->decrypt path just verified.

What changed instead:

- Removed the lockOnLogout opt-out from the logout/account-switch wipes. A
  non-extractable key cannot be exported but can still be USED, so a handle
  outliving the session lets anyone with the browser profile decrypt mail
  without knowing the passphrase. That is not a preference to toggle off.
- Added a best-effort wipe on pagehide and beforeunload to narrow the window
  in which a usable handle exists on disk. Best-effort by nature: an
  IndexedDB write may not complete during teardown and neither event fires
  on a crash — which is precisely why the boot wipe in activate() remains
  the load-bearing control.
- Deliberately NOT wiping on visibilitychange: tabbing away would drop the
  unlock and force a passphrase re-entry every time, which trains users into
  turning S/MIME off entirely.
- Dropped the now-dead lockOnLogout setting from the manifest. A toggle that
  silently does nothing is worse than no toggle.

Tests: 49 unit assertions + 28 round trip. The round trip now feeds genuinely
hostile MIME through the real parser (5000-level nesting, 5000 siblings,
65 MB) and still confirms a normal multipart/alternative parses correctly.
Full crypto round trip unchanged and passing, so neither fix broke S/MIME.

Findings 6, 7, 8 and 9 remain open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 11:57:18 +02:00
Bernd RodlerandClaude Opus 4.8 d047891ded test(smime): real crypto round trip against the patched plugin
Adds roundtrip.mjs, which drives the plugin's own modules directly — no
browser, no DOM — and proves the three audit fixes did not break S/MIME.
24 assertions, all passing, against the self-signed spike certificates:

  PKCS#12 import (both identities, RSA-2048, kdf=600000)
  key encrypted at rest (32-byte salt, 12-byte IV)
  unlock yields NON-EXTRACTABLE keys; wrong passphrase rejected
  sign -> verify: signature valid, signer email matches From
  encrypt -> decrypt by the intended recipient, plaintext matches
  sender can read their own Sent copy
  downgraded message produces no plaintext

Two results worth recording.

Finding 1 is confirmed against a genuine CMS structure, not just a mock:
the spike certs are self-signed, smimeVerify reports signatureValid AND
signerEmailMatch true AND selfSigned true, and the gate refuses the
auto-import. That is exactly the cert-substitution attack, blocked. The
same status with selfSigned:false passes, so the gate is not simply
refusing everything.

Finding 2 is confirmed end to end: our own encrypt path produces
AES-256-GCM, decrypt reports contentAuthenticated:true, so HTML renders
without suppression. Only legacy inbound CBC degrades to text.

The section-8 assertion is deliberately loose. Swapping the 9-byte
AES-GCM OID for the 8-byte 3DES OID also invalidates the enclosing DER
lengths, so ASN.1 validation rejects the message before the allowlist is
reached — either way no plaintext is produced, and the assertion says
which path fired rather than pretending it tested the allowlist. The
allowlist itself is asserted precisely in verify-fixes.mjs, which now
carries 36 assertions including checks that fail if a legacy CBC OID
reappears or the mail path stops using the native engine.

Browser-side spike result: the patched plugin installs through the admin
channel, resolves to the privileged tier, and activates with
"hooks=5, slots=3" and no refusals — so the B-04 gate does not block it.
Its S/MIME settings section renders and survives SPA navigation. Key
import via the UI could not be automated (native file picker), which is a
harness limit rather than a product defect; roundtrip.mjs covers that
path directly instead.

Findings 4, 5 and 6 remain open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 11:41:07 +02:00