I did this
When libcurl answers a SASL DIGEST-MD5 challenge, Curl_auth_create_digest_md5_message builds a comma-separated list of key=value directives. Three of those values (username, realm, and nonce) are escaped with auth_digest_string_quoted before they are wrapped in quotes. digest-uri is not. It is filled with the raw SPN from Curl_auth_build_spn(service, hostname, NULL), and service is CURLOPT_SERVICE_NAME when that option is set. A quote and comma in the service string close the quoted digest-uri field early and add extra directives to the message. That buffer is then base64-encoded and written by the protocol continuation callback.
The same response path already had quoting bugs in the other fields: realm and nonce in #20482 / #20545, and username in #20940 (HackerOne 3608522). On that HackerOne report the maintainer said "curl makes no guarantees for this. Users should pass in correct data or there might be errors.", and that "We do not consider this security problems." Still, given that other parameters received the quotation, I thought it might still worth file an issue for the digest-uri parameter, too.
Demonstration
A local SMTP listener offers DIGEST-MD5, returns a fixed md5-sess / qop=auth challenge, and prints the decoded client response. Run the listener server, then the curl command against the printed PORT.
`server.py`
#!/usr/bin/env python3
from __future__ import annotations
import base64
import socket
import threading
HOST = "127.0.0.1"
CHALLENGE = b'nonce="n",realm="r",algorithm=md5-sess,qop="auth"'
CH_B64 = base64.b64encode(CHALLENGE).decode("ascii")
class SmtpDigest:
def __init__(self) -> None:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind((HOST, 0))
self.sock.listen(8)
self.port = self.sock.getsockname()[1]
self._stop = threading.Event()
def serve(self) -> None:
self.sock.settimeout(0.5)
while not self._stop.is_set():
try:
conn, _addr = self.sock.accept()
except socket.timeout:
continue
except OSError:
break
threading.Thread(target=self.handle, args=(conn,), daemon=True).start()
def stop(self) -> None:
self._stop.set()
try:
self.sock.close()
except OSError:
pass
def handle(self, conn: socket.socket) -> None:
conn.settimeout(10)
try:
conn.sendall(b"220 scan2-candidate8 smtp\r\n")
rest = b""
while not self._stop.is_set():
try:
chunk = conn.recv(4096)
except socket.timeout:
break
if not chunk:
break
rest += chunk
while b"\r\n" in rest:
raw, rest = rest.split(b"\r\n", 1)
line = raw.decode("ascii", "replace").strip()
if not line:
conn.sendall(b"535 empty\r\n")
continue
print(f"> {line}", flush=True)
cmd = line.split(" ", 1)[0].upper()
if cmd in ("EHLO", "HELO"):
conn.sendall(b"250-localhost\r\n250 AUTH DIGEST-MD5\r\n")
elif cmd == "AUTH":
conn.sendall(f"334 {CH_B64}\r\n".encode("ascii"))
elif cmd == "QUIT":
conn.sendall(b"221 bye\r\n")
return
else:
decoded = base64.b64decode(line).decode("utf-8", "replace")
print(f"DECODED: {decoded}", flush=True)
conn.sendall(b"535 authentication failed\r\n")
finally:
try:
conn.close()
except OSError:
pass
def main() -> None:
srv = SmtpDigest()
print(f"PORT={srv.port}", flush=True)
try:
srv.serve()
except KeyboardInterrupt:
pass
finally:
srv.stop()
if __name__ == "__main__":
main()
python3 server.py
# PORT=40627
printf 'hi\n' | curl --max-time 8 --user 'user:secret' \
--service-name 'smtp",injected=1,x="' \
--mail-from sender@example.com --mail-rcpt recipient@example.com \
--login-options 'AUTH=DIGEST-MD5' -T - smtp://127.0.0.1:40627/
Output
--service-name 'smtp",injected=1,x="' terminates digest-uri and adds injected=1:
DECODED: username="user",realm="r",nonce="n",cnonce="9a4367aa915d8c454f0cebf54aeeefd2",nc="00000001",digest-uri="smtp",injected=1,x="/127.0.0.1",response=d8aafa956dd9d15f98fa61480eeec606,qop=auth
Code walkthrough
1. CURLOPT_SERVICE_NAME is stored unfiltered
Curl_setstropt copies a C string up to CURL_MAX_INPUT_LENGTH with no character filtering:
lib/setopt.c:81-90
CURLOPT_SERVICE_NAME use that helper:
lib/setopt.c:2307-2308
url_set_data_creds then copies STRING_SERVICE_NAME unchanged into the connection credentials:
lib/url.c:1365-1376
2. DIGEST-MD5 continuation uses that service string
With a username configured and DIGEST-MD5 enabled, sasl_choose_digest selects the mechanism and sets SASL_DIGESTMD5 as the first continuation state:
lib/curl_sasl.c:387-393
On the challenge continuation, Curl_sasl_continue calls Curl_auth_create_digest_md5_message. That function prefers the credentials' SASL service over the protocol default ("smtp" / "imap" / "pop"):
lib/curl_sasl.c:646-654, lib/vauth/digest.c:350-351
3. SPN is concatenated and interpolated without quoting
Curl_auth_build_spn concatenates service and the connection hostname with no escaping:
lib/vauth/vauth.c:49-58, lib/vauth/digest.c:435
The response formatter quote-escapes username, realm, and nonce, then drops the raw SPN into digest-uri="%s":
lib/vauth/digest.c:487-498
The HTTP Digest path in the same file does quote the URI (uri_quoted = auth_digest_string_quoted(...)).
Curl_sasl_continue then base64-encodes the buffer (SASL_FLAG_BASE64) and passes it to the protocol contauth callback with no structured-field check. SMTP writes it with Curl_pp_sendf(..., "%s", ...):
lib/curl_sasl.c:248-266, lib/curl_sasl.c:793-796, lib/smtp.c:763-773
For --service-name 'smtp",injected=1,x="' the SPN is smtp",injected=1,x="/127.0.0.1, so the emitted field list contains digest-uri="smtp",injected=1,x="/127.0.0.1" instead of one quoted digest-uri.
Reporting on behalf of Microsoft FORGE Lab. This bug was initially discovered with an agentic code scanner; the author verified the finding and prepared this report.
I expected the following
digest-uri escaped with auth_digest_string_quoted the same way as username, realm, and nonce.
curl/libcurl version
curl 8.23.0-DEV (Linux) libcurl/8.23.0-DEV zlib/1.3.1
Release-Date: [unreleased]
Protocols: dict file ftp gopher http imap ipfs ipns mqtt pop3 rtsp smtp telnet tftp ws
Features: alt-svc AsynchDNS IPv6 Largefile libz threadsafe UnixSockets
Built from 39088cd295592ca6d4c5e687c75b400a67a87906 (2026-09-16). gcc 15.2.0, Python 3.14.4.
Note: This does not trigger on Ubuntu's packaged curl (8.18.0-1ubuntu2.5). That package's CVE-2026-8458 patch is a partial backport of upstream 5e99b73cf441d9c369768b9cd48b5389b9a2503d (creds: add sasl service name, first in 8.21.0). Upstream also stopped reading STRING_SERVICE_NAME in Curl_sasl_continue, but moved the lookup into Curl_auth_create_digest_md5_message via Curl_creds. Ubuntu applied only the curl_sasl.c side, so DIGEST-MD5 always gets the protocol default ("smtp") and --service-name never reaches digest-uri.
operating system
Linux Abraxas 6.6.114.1-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Mon Dec 1 20:46:23 UTC 2025 x86_64 GNU/Linux
I did this
When libcurl answers a SASL DIGEST-MD5 challenge,
Curl_auth_create_digest_md5_messagebuilds a comma-separated list ofkey=valuedirectives. Three of those values (username,realm, andnonce) are escaped withauth_digest_string_quotedbefore they are wrapped in quotes.digest-uriis not. It is filled with the raw SPN fromCurl_auth_build_spn(service, hostname, NULL), andserviceisCURLOPT_SERVICE_NAMEwhen that option is set. A quote and comma in the service string close the quoteddigest-urifield early and add extra directives to the message. That buffer is then base64-encoded and written by the protocol continuation callback.The same response path already had quoting bugs in the other fields: realm and nonce in #20482 / #20545, and username in #20940 (HackerOne 3608522). On that HackerOne report the maintainer said "curl makes no guarantees for this. Users should pass in correct data or there might be errors.", and that "We do not consider this security problems." Still, given that other parameters received the quotation, I thought it might still worth file an issue for the
digest-uriparameter, too.Demonstration
A local SMTP listener offers DIGEST-MD5, returns a fixed
md5-sess/qop=authchallenge, and prints the decoded client response. Run the listener server, then the curl command against the printedPORT.`server.py`
Output
--service-name 'smtp",injected=1,x="'terminatesdigest-uriand addsinjected=1:Code walkthrough
1.
CURLOPT_SERVICE_NAMEis stored unfilteredCurl_setstroptcopies a C string up toCURL_MAX_INPUT_LENGTHwith no character filtering:lib/setopt.c:81-90CURLOPT_SERVICE_NAMEuse that helper:lib/setopt.c:2307-2308url_set_data_credsthen copiesSTRING_SERVICE_NAMEunchanged into the connection credentials:lib/url.c:1365-13762. DIGEST-MD5 continuation uses that service string
With a username configured and DIGEST-MD5 enabled,
sasl_choose_digestselects the mechanism and setsSASL_DIGESTMD5as the first continuation state:lib/curl_sasl.c:387-393On the challenge continuation,
Curl_sasl_continuecallsCurl_auth_create_digest_md5_message. That function prefers the credentials' SASL service over the protocol default ("smtp"/"imap"/"pop"):lib/curl_sasl.c:646-654,lib/vauth/digest.c:350-3513. SPN is concatenated and interpolated without quoting
Curl_auth_build_spnconcatenatesserviceand the connection hostname with no escaping:lib/vauth/vauth.c:49-58,lib/vauth/digest.c:435The response formatter quote-escapes username, realm, and nonce, then drops the raw SPN into
digest-uri="%s":lib/vauth/digest.c:487-498The HTTP Digest path in the same file does quote the URI (
uri_quoted = auth_digest_string_quoted(...)).Curl_sasl_continuethen base64-encodes the buffer (SASL_FLAG_BASE64) and passes it to the protocolcontauthcallback with no structured-field check. SMTP writes it withCurl_pp_sendf(..., "%s", ...):lib/curl_sasl.c:248-266,lib/curl_sasl.c:793-796,lib/smtp.c:763-773For
--service-name 'smtp",injected=1,x="'the SPN issmtp",injected=1,x="/127.0.0.1, so the emitted field list containsdigest-uri="smtp",injected=1,x="/127.0.0.1"instead of one quoteddigest-uri.Reporting on behalf of Microsoft FORGE Lab. This bug was initially discovered with an agentic code scanner; the author verified the finding and prepared this report.
I expected the following
digest-uriescaped withauth_digest_string_quotedthe same way asusername,realm, andnonce.curl/libcurl version
Built from
39088cd295592ca6d4c5e687c75b400a67a87906(2026-09-16). gcc 15.2.0, Python 3.14.4.Note: This does not trigger on Ubuntu's packaged curl (
8.18.0-1ubuntu2.5). That package'sCVE-2026-8458patch is a partial backport of upstream5e99b73cf441d9c369768b9cd48b5389b9a2503d(creds: add sasl service name, first in 8.21.0). Upstream also stopped readingSTRING_SERVICE_NAMEinCurl_sasl_continue, but moved the lookup intoCurl_auth_create_digest_md5_messageviaCurl_creds. Ubuntu applied only thecurl_sasl.cside, so DIGEST-MD5 always gets the protocol default ("smtp") and--service-namenever reachesdigest-uri.operating system