From cb6ddd6fcfccfda16d05bb3b3d7e7761612149a1 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:31:24 +0200 Subject: [PATCH] fix: strip CR/LF from read-receipt header values (GHSA-w38p-hpqv-g89c) The read receipt is assembled as raw RFC 5322 text, and every header value in it comes from the message being answered. Stalwart returns subjects already RFC 2047-decoded, so a sender could hide CRLF in an encoded-word subject (or in the Message-ID or Disposition-Notification-To) and have it interpolated verbatim. That let them add headers such as Reply-To, or a whole body, to a message the user's own account files in Sent and submits. `encodeHeaderWord` passed any pure-ASCII value through untouched, CR/LF included. Port the webmail fix: free-text values go through `stripHeaderControls` (controls collapse to a space, then RFC 2047 if non-ASCII), address-like values and message-ids through `headerAtom` (controls and whitespace removed), and the From display name is quoted when it uses specials. --- .../__tests__/mdn-header-injection.test.ts | 199 ++++++++++++++++++ src/lib/mdn.ts | 63 ++++-- 2 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 src/lib/__tests__/mdn-header-injection.test.ts diff --git a/src/lib/__tests__/mdn-header-injection.test.ts b/src/lib/__tests__/mdn-header-injection.test.ts new file mode 100644 index 00000000..40f8f132 --- /dev/null +++ b/src/lib/__tests__/mdn-header-injection.test.ts @@ -0,0 +1,199 @@ +import { describe, it, expect } from 'vitest'; +import { buildMdnMessage } from '../mdn'; + +// GHSA-w38p-hpqv-g89c: every value that lands in a header of the read receipt +// originates with the sender of the message being answered, and Stalwart hands +// it back already RFC 2047-decoded. A bare CRLF smuggled inside an encoded-word +// used to be interpolated verbatim into the Subject: line, letting the sender +// append From/Reply-To/Content-Type headers and a body to a message the +// user's own account files into Sent and submits. + +const base = { + to: 'sender@other.com', + fromEmail: 'me@example.com', + originalMessageId: 'orig@other.com', +}; + +// The decoded subject from the advisory's proof of concept. +const INJECTED_SUBJECT = + 'Invoice\r\nFrom: IT Helpdesk \r\nReply-To: attacker@evil.test\r\n' + + 'Content-Type: text/html; charset=utf-8\r\n\r\n

Forged