CakePHP 5.4.2, SmtpTransport lone LF leftover of GHSA-2qh5
GHSA-2qh5 deleted the two-byte CRLF pair from custom mail headers. A lone LF or CR is left in place. SmtpTransport SMTP DATA treats it as a new Bcc line. Labbed 5.4.2. No CVE yet.
- Name
- CakePHP 5.4.2, SmtpTransport lone LF leftover of GHSA-2qh5
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- CakePHP
- Affected
- CakePHP 5.4.2 (887b641) SmtpTransport / MailTransport. Same preconditions as GHSA-2qh5: app copies user data into Message::setHeaders. Stock cakephp/app does not do that by itself.
- Published
- 12 Oct 2026
- Updated
- 12 Oct 2026
- Tags
- 0-day, cakephp, crlf, header-injection, cwe-93, smtp
The pair-only strip was the leftover
I am @abraxas_null. The proof of concept is on GitHub: abraxas/cakephp-smtp-crlf (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, send.php, smtp_sink.py, composer.json. Authorized lab only. It talks to loopback.
This is CakePHP 5.4.2 (887b641de9f7aba06071f98f17edcbc573cbcf63). No CVE yet. CWE-93. 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N). Remote mail header injection. Same preconditions as GHSA-2qh5.
GHSA-2qh5 / CVE-2026-77634 added str_replace("\r\n", '', $val) on SmtpTransport and MailTransport. That callback is a no-op for a bare LF or CR. Header names are still concatenated unsanitized. PHP mail() and the SMTP DATA we captured treat a bare LF as a new header line.
The transport tests only send "line one\r\nline two" and expect line oneline two. The announced fix holds for the pair. It does not hold for the byte.
Stock cakephp/app does not put request data into custom headers by itself. This is a framework leftover. Same victim model as the GHSA.
Why CakePHP
I had just finished unpublished Crit/High hunts of larger trees. CakePHP 5.4.2 is a framework pin after a 2026 GHSA wave: FunctionsBuilder SQLi, jsonValue SQLi, SmtpTransport CRLF, View element path, Paginator XSS, Query limit/offset, RateLimit ipHeader, multipart filename=. That is a good day to sit on the tag anyway and look for pair-only strips.
I only wanted Critical or High. Local is in-scope. Medium stays on the floor unless it chains. This is a framework: each finding needs an app that uses the default or documented API. Skip "pass user SQL into query()".
Pinning 5.4.2
Latest calendar tag. 887b641. VERSION.txt = 5.4.2. Composer cakephp/cakephp:5.4.2 in PHP 8.3 for this lab.
Intake is security@cakephp.org. SECURITY.md on the tag.
The skip list is half the hunt
On this pin the interesting ones were already closed:
- GHSA-vjqc / CVE-2026-79752 FunctionsBuilder
cast/extract/datePart/dateAddSQLi.ensureSimpleString(). - GHSA-fxf7 / CVE-2026-77635
jsonValue()Postgres SQLi. Path bound, not interpolated. - GHSA-2qh5 / CVE-2026-77634 SmtpTransport CRLF in custom headers. Incomplete. Pair only. This leftover.
- GHSA-wpvj / CVE-2026-48820
View::element()path._checkFilePath(). - GHSA-qh8m / CVE-2026-23643
PaginatorHelper::limitControl()XSS.h($fieldName). - GHSA-6g8q Query
limit/offsetSQLi. Typedint. - 5.4.1 bakery RateLimitMiddleware default
ipHeaderx-forwarded-for. Default is nowremote_addr. - 5.4.2 notes multipart
Content-Dispositionfilename=injection.FormDataPart::_headerParameterToStringstrips"/\r/\n.
cakephp/authentication GHSA-hhpq (open redirect) is a different repo. Core Routing has no matching backslash bypass.
Hunts that returned NONE in this bar: CSRF / FormProtection / _method override; View LFI siblings; BodyParser XXE (XML parser off by default, loadEntities false); mass assignment (documented $_accessible = ['*' => true]); helper XSS leftovers; clientIp() HTTP_CLIENT_IP without trustProxy (LegalHackers, already gated).
I did not re-file any of that.
Ten surfaces, then the pair
I split the tree. Unserialize/cache/cookies, CSRF/FormProtection, View/LFI/files, remaining SQLi, mail CRLF siblings, IP/Host/proxy/SSRF, XXE/BodyParser, mass-assignment/crypto, XSS/helpers/routing, Http Client/cookie jar.
No unpublished unauthenticated RCE on a stock cakephp/app skeleton. The leftovers that survived need an app that uses the documented API.
This pack is leftover 1: Smtp/Mail transport CRLF strip is pair-only. Sibling packs on the same pin: cookie-suffix, trusted-proxies. Attachment Content-Type / Content-ID CRLF is Medium leftover 6 after the filename= fix. Cache unserialize is a different class.
SmtpTransport 5.4.2:
$headers = $message->getHeadersString(
[...],
"\r\n",
function (string $val): string {
return str_replace("\r\n", '', $val);
},
);MailTransport uses the same callback. Message::getHeadersString maps the callback over values, then does $key . ': ' . $val.
If an app copies user data into a custom header, an attacker can add Bcc, rewrite Subject, or break into the body by sending LF instead of CRLF.
What I almost filed instead
Treating GHSA-2qh5 as closed and walking away. The tests cover the pair. The leftover is the byte.
Calling PHP mail() in the lab. The oracle is SMTP DATA plus getHeadersString. MailTransport is proven on the header string, not on a real sendmail.
Inflating this to unauth RCE on a skeleton app. Stock app does not put request data into custom headers.
Multipart filename= as this leftover. 5.4.2 already strips that. Attachment Content-Type / Content-ID is leftover 6, Medium, a different pack.
Cake's RCPT TO using only setTo / setCc / setBcc on the Message object. Injected Bcc inside DATA is inferred extra envelope on some MTAs, not a change to Cake's envelope.
The lab
Composer cakephp/cakephp:5.4.2 in PHP 8.3. Loopback SMTP sink at 127.0.0.1:19410.
- Control:
X-Meta=ok\r\nBcc: lab@localhost.invalid\r\nX-Oracle: CAKEPHP-SMTP-CRLF-WITNESS. Result: one headerX-Meta: okBcc: lab@localhost.invalidX-Oracle: CAKEPHP-SMTP-CRLF-WITNESS. - Case LF: same payload with
\nonly. Result: newBcc:andX-Oracle:lines in SMTP DATA, SmtpgetHeadersString, and MailgetHeadersString. - Case CR: same with
\ronly. Same extra lines.
Replay:
git clone https://github.com/abraxas/cakephp-smtp-crlf
cd cakephp-smtp-crlf/lab
./run.shBind it to loopback. The loopback client is lab/run.sh with a banner on it.
SUCCESS cakephp-smtp-crlf pin=5.4.2 control-crlf-stripped=yes lf-bcc=yes cr-leftover=yes how=smtp-data-bcc-line CAKEPHP-SMTP-CRLF-WITNESSWrong turns, in the order I made them
Pair-only tests as coverage. "line one\r\nline two" is the GHSA. Lone LF is the leftover.
mail() as the oracle. SMTP DATA is enough. MailTransport getHeadersString is the sibling.
Skeleton app as the victim. Framework leftover. Same as GHSA-2qh5.
A reverse shell. Theatre. The oracle is a new Bcc: line.
What an attacker can do
Needs an application that puts request data into custom mail headers.
| Privilege | Method / sink | Lab-proven or inferred |
|---|---|---|
Inject an extra Bcc: (or Cc: / To:) line into the SMTP DATA header block |
setHeaders(['X-Meta' => "ok\nBcc: lab@localhost.invalid"]) then SmtpTransport::send |
Lab-proven. SMTP DATA and getHeadersString both show a new Bcc: line. Witness X-Oracle: CAKEPHP-SMTP-CRLF-WITNESS |
Same injection through MailTransport |
Same callback in MailTransport::send |
Lab-proven on getHeadersString. This lab does not call PHP mail() |
| Lone CR as well as lone LF | ok\rBcc: ... |
Lab-proven. Pair-only strip leaves \r |
Rewrite Subject or break into the body |
Value contains \nSubject: or \n\n |
Inferred from the same concatenation. Lab injected Bcc / X-Oracle, not Subject |
| Extra envelope recipient | Injected Bcc inside DATA |
Inferred. Some MTAs re-parse DATA. Cake's RCPT TO still uses the Message object |
Two-byte \r\n pair |
GHSA-2qh5 strip | Lab-proven closed. Control concatenates okBcc:... |
| Unauthenticated RCE on a skeleton app | not this leftover | Closed |
Multipart filename= CRLF |
5.4.2 already strips "/\r/\n |
Closed |
The fix
Strip or reject any \r or \n in header names and values (same as the 5.4.2 filename= sanitizer). Cover lone LF and lone CR in the transport tests, not only the \r\n pair.
Until then, do not copy request data into Message::setHeaders / addHeaders.
I am not going to print a header value you can paste at someone else's contact form. The missing byte strip is the useful part. If you own the app, run the proof of concept against loopback.
References
- Proof of concept: abraxas/cakephp-smtp-crlf · cakephp-smtp-crlf-Abraxas-Labs.py
- Lab:
lab/· Dockerfile · docker-compose.yml · run.sh · poc.py · send.php · smtp_sink.py - @abraxas_null · github.com/abraxas · abraxaslabs.tech · abraxas.null@proton.me
- CWE-93
- Tree: cakephp/cakephp 5.4.2 (
887b641de9f7aba06071f98f17edcbc573cbcf63) ·SmtpTransport.php·MailTransport.php·Message.php - Nearby patched (pair only): GHSA-2qh5-382h-3jpc / CVE-2026-77634
- Same pin: cakephp-cookie-suffix · cakephp-trusted-proxies