security(smime): fork upstream plugin and fix two audit findings
S-01 audited bulwarkmail/plugins/smime @ 91085a3 (2,935 lines). Nine findings, two HIGH. No backdoor and no exfiltration path anywhere in the bundle — the problems are trust-model and input-validation gaps. Full report in vnc/audits/SMIME-PLUGIN-AUDIT-2026-08-04.md. Fork is source-only. The upstream smime.zip is a 1.77 MB prebuilt bundle whose manifest reads 1.0.1 while the source reads 1.0.2, so auditing src/ would not audit what that zip installs. We build from source. Finding 1 (HIGH) — certificate substitution. maybeAutoImportSigner gated on signatureValid alone, but smimeVerify runs checkChain:false, so that only proves "signed by whoever holds this key", not that the claimed identity is real. Self-sign a cert asserting victim@example.com, send one signed message, and it was stored as the encryption target for that address — the user's next Encrypt to the victim went to the attacker. Now requires signerEmailMatch === true and !selfSigned. Both values were already computed and displayed as untrusted in the banner; only the import path ignored them. Tests for `true` explicitly so an undefined match (missing From header) fails closed. Finding 3 (MED-HIGH) — CRLF header injection. Escaping reached only Subject and attachment filename; display names, raw addresses, Message-ID, In-Reply-To, References and attachment Content-Type were emitted verbatim, and formatAddress escapes only backslash and quote. In-Reply-To/References/display names are copied from inbound mail when replying or forwarding, so the value is attacker-supplied. Sanitising inside formatHeader covers all 17 call sites by construction; the three headers assembled directly get stripCrlf explicitly. Also adds auth:observe to the manifest. The plugin registers onAfterLogout/onAccountSwitch — real hooks (lib/plugin-hooks.ts:362-363) — without declaring the permission, so under B-09 the session-key wipe would silently stop running. verify-fixes.mjs carries 19 assertions including source checks that fail if either guard is removed or a new unsanitised interpolated header appears. That last one immediately caught the interpolated smime-type Content-Type header, which manual review had dismissed as static. Finding 2 (unauthenticated CBC accepted on decrypt) is NOT fixed. This is not safe for real mail yet — sandbox accounts only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e9746fcf78
commit
f7e487171c
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"id": "smime",
|
||||
"name": "S/MIME",
|
||||
"version": "1.0.2",
|
||||
"author": "Bulwark Mail Community",
|
||||
"description": "End-to-end S/MIME for webmail: sign and encrypt outgoing messages, and automatically verify signatures and decrypt incoming CMS (PKCS#7) mail. Private keys are imported from a PKCS#12 (.p12/.pfx) file, encrypted at rest with a passphrase, and unlocked into non-extractable WebCrypto keys that never leave your browser. Runs in the privileged (same-origin) plugin tier so all cryptography happens locally with bundled pkijs/asn1js.",
|
||||
"type": "ui-extension",
|
||||
"tier": "privileged",
|
||||
"permissions": [
|
||||
"crypto:full",
|
||||
"email:blob-read",
|
||||
"email:raw-send",
|
||||
"email:render-takeover",
|
||||
"email:read",
|
||||
"email:send",
|
||||
"smime:read",
|
||||
"auth:observe",
|
||||
"ui:composer-toolbar",
|
||||
"ui:email-banner",
|
||||
"ui:settings-section",
|
||||
"app:lifecycle"
|
||||
],
|
||||
"entrypoint": "index.js",
|
||||
"minAppVersion": "1.7.6",
|
||||
"icon": "media/icon.svg",
|
||||
"banner": "media/banner.svg",
|
||||
"settingsSchema": {
|
||||
"encryptionStrength": {
|
||||
"type": "select",
|
||||
"label": "Content encryption algorithm",
|
||||
"description": "Symmetric cipher used to encrypt the message body. AES-256-GCM is recommended; AES-128-GCM is slightly smaller and still strong.",
|
||||
"default": "aes-256",
|
||||
"options": ["aes-256", "aes-128"]
|
||||
},
|
||||
"autoImportSignerCerts": {
|
||||
"type": "boolean",
|
||||
"label": "Auto-save signer certificates",
|
||||
"description": "When a validly signed message is opened, remember the signer's certificate so you can later send them encrypted mail without importing it manually.",
|
||||
"default": true
|
||||
},
|
||||
"lockOnLogout": {
|
||||
"type": "boolean",
|
||||
"label": "Lock keys on logout",
|
||||
"description": "Wipe all unlocked private keys from memory when you sign out or switch accounts. Leave on unless you have a specific reason not to.",
|
||||
"default": true
|
||||
},
|
||||
"warnOnSelfSigned": {
|
||||
"type": "boolean",
|
||||
"label": "Warn on self-signed signer",
|
||||
"description": "Show a caution banner when an incoming signature validates against a self-signed certificate (not chained to a trusted CA).",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"locales": {
|
||||
"en": {
|
||||
"banner.signed_valid": "Signature valid",
|
||||
"banner.signed_invalid": "Signature invalid",
|
||||
"banner.encrypted": "Encrypted message",
|
||||
"banner.decrypted": "Decrypted",
|
||||
"banner.locked": "Encrypted — unlock your key to read",
|
||||
"toolbar.sign": "Sign",
|
||||
"toolbar.encrypt": "Encrypt",
|
||||
"settings.title": "S/MIME keys & certificates"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user