Double Free Vulnerability in `libcurl` Cookie Management (`cookie.c`)
What the report claims
Double free vulnerabilities in libcurl's cookie management code can lead to program crashes, arbitrary code execution, data integrity issues, and denial of service.
Compare with curl's recorded outcome
Labeled AI slop
curl's maintainer lists this report as AI slop
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code provides evidence against the report's central claim. Review the cited lines before deciding.
Why this finding?
- The replace_existing function (lib/cookie.c:1026–1036) correctly unlinks the node before freeing the cookie exactly once; no double-free is present in the provided code.
- The Curl_cookie_add function body is not included in the provided source, so the second claim cannot be verified.
- Since the primary claimed flaw is contradicted by the code and the secondary is unverifiable, the report does not demonstrate a valid security vulnerability.
What still needs checking
- The complete Curl_cookie_add function implementation (including the fail: error path) from lib/cookie.c at the target revision.
How this analysis ran 6 claims · 5 citations verified
Checked against curl master on 2025-04-29 (2fa3d528aea1). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the report. In the provided code, replace_existing (lib/cookie.c:1026–1036) removes the node from the list with Curl_node_remove and then calls freecookie exactly once — there is no double-free. The Curl_cookie_add function body was not included in the source excerpt; please provide the full function (especially the fail: label and surrounding logic) so we can assess the second claim.
Inspect all claims and source code 6 claims · 4 code excerpts
Report claims
Double free occurs when a cookie object is freed without ensuring it has not already been removed from the list, leading to double-free in the replace_existing function.
Read the reasoning
The code shows a correct remove-then-free pattern: the node is unlinked from the list at line 1033, then the cookie is freed once at line 1036. There is no second free of the same pointer in this function, and the node was in the list during iteration (line 949–1025), so it had not been removed already.
Memory allocated for a cookie object is freed again on errors even if it was previously released, causing double-free in the Curl_cookie_add function.
Read the reasoning
The actual implementation of Curl_cookie_add (including any fail: label and freecookie calls) is not present in the provided source. The deterministic checks confirm the symbol exists, but the code needed to evaluate the claim is missing.
Create multiple cookies with identical names, domains, and paths to trigger replacement scenario in replace_existing function.
Read the reasoning
Trigger claim depends on C1 being a real flaw; since C1 is contradicted, this trigger does not demonstrate a vulnerability.
Add a malformed or invalid cookie header line to trigger error path in Curl_cookie_add function.
Read the reasoning
Trigger claim depends on C2; without the Curl_cookie_add function body, it cannot be assessed.
Program crash or segmentation fault due to double-free leading to undefined behavior.
Read the reasoning
Impact claim depends on the existence of the double-free flaws, which are not substantiated.
Potential arbitrary code execution and data corruption from memory corruption caused by double-free.
Read the reasoning
Impact claim depends on the existence of the double-free flaws, which are not substantiated.
Read the original report (excerpt)
Source code
| 938 | static int |
| 939 | replace_existing(struct Curl_easy *data, |
| 940 | struct Cookie *co, |
| 941 | struct CookieInfo *ci, |
| 942 | bool secure, |
| 943 | bool *replacep) |
| 944 | { |
| 945 | bool replace_old = FALSE; |
| 946 | struct Curl_llist_node *replace_n = NULL; |
| 947 | struct Curl_llist_node *n; |
| 948 | size_t myhash = cookiehash(co->domain); |
| 949 | for(n = Curl_llist_head(&ci->cookielist[myhash]); n; n = Curl_node_next(n)) { |
| 950 | struct Cookie *clist = Curl_node_elem(n); |
| 951 | if(!strcmp(clist->name, co->name)) { |
| 952 | /* the names are identical */ |
| 953 | bool matching_domains = FALSE; |
| 954 | |
| 955 | if(clist->domain && co->domain) { |
| 956 | if(strcasecompare(clist->domain, co->domain)) |
| 957 | /* The domains are identical */ |
| 958 | matching_domains = TRUE; |
| 959 | } |
| 960 | else if(!clist->domain && !co->domain) |
| 961 | matching_domains = TRUE; |
| 962 | |
| 963 | if(matching_domains && /* the domains were identical */ |
| 964 | clist->spath && co->spath && /* both have paths */ |
| 965 | clist->secure && !co->secure && !secure) { |
| 966 | size_t cllen; |
| 967 | const char *sep; |
| 968 | |
| 969 | /* |
| 970 | * A non-secure cookie may not overlay an existing secure cookie. |
| 971 | * For an existing cookie "a" with path "/login", refuse a new |
| 972 | * cookie "a" with for example path "/login/en", while the path |
| 973 | * "/loginhelper" is ok. |
| 974 | */ |
| 975 | |
| 976 | sep = strchr(clist->spath + 1, '/'); |
| 977 | |
| 978 | if(sep) |
| 979 | cllen = sep - clist->spath; |
| 980 | else |
| 981 | cllen = strlen(clist->spath); |
| 982 | |
| 983 | if(strncasecompare(clist->spath, co->spath, cllen)) { |
| 984 | infof(data, "cookie '%s' for domain '%s' dropped, would " |
| 985 | "overlay an existing cookie", co->name, co->domain); |
| 986 | return CERR_BAD_SECURE; |
| 987 | } |
| 988 | } |
| 989 | } |
| 990 | |
| 991 | if(!replace_n && !strcmp(clist->name, co->name)) { |
| 992 | /* the names are identical */ |
| 993 | |
| 994 | if(clist->domain && co->domain) { |
| 995 | if(strcasecompare(clist->domain, co->domain) && |
| 996 | (clist->tailmatch == co->tailmatch)) |
| 997 | /* The domains are identical */ |
| 998 | replace_old = TRUE; |
| 999 | } |
| 1000 | else if(!clist->domain && !co->domain) |
| 1001 | replace_old = TRUE; |
| 1002 | |
| 1003 | if(replace_old) { |
| 1004 | /* the domains were identical */ |
| 1005 | |
| 1006 | if(clist->spath && co->spath && |
| 1007 | !strcasecompare(clist->spath, co->spath)) |
| 1008 | replace_old = FALSE; |
| 1009 | else if(!clist->spath != !co->spath) |
| 1010 | replace_old = FALSE; |
| 1011 | } |
| 1012 | |
| 1013 | if(replace_old && !co->livecookie && clist->livecookie) { |
| 1014 | /* |
| 1015 | * Both cookies matched fine, except that the already present cookie |
| 1016 | * is "live", which means it was set from a header, while the new one |
| 1017 | * was read from a file and thus is not "live". "live" cookies are |
| 1018 | * preferred so the new cookie is freed. |
| 1019 | */ |
| 1020 | return CERR_LIVE_WINS; |
| 1021 | } |
| 1022 | if(replace_old) |
| 1023 | replace_n = n; |
| 1024 | } |
| 1025 | } |
| 1026 | if(replace_n) { |
| C1: replace_existing checks if replace_n is set, then gets the cookie element from the node | |
| 1027 | struct Cookie *repl = Curl_node_elem(replace_n); |
| 1028 | |
| 1029 | /* when replacing, creationtime is kept from old */ |
| 1030 | co->creationtime = repl->creationtime; |
| 1031 | |
| 1032 | /* unlink the old */ |
| 1033 | Curl_node_remove(replace_n); |
| C1: Curl_node_remove(replace_n) unlinks the node from the list | |
| 1034 | |
| 1035 | /* free the old cookie */ |
| 1036 | freecookie(repl); |
| C1: freecookie(repl) frees the cookie after removal — single free, not double | |
| 1037 | } |
| 1038 | *replacep = replace_old; |
| 1039 | return CERR_OK; |
| 1040 | } |
| 201 | void Curl_node_remove(struct Curl_llist_node *e) |
| C1: Curl_node_remove only unlinks; it does not free the element | |
| 202 | { |
| 203 | Curl_node_uremove(e, NULL); |
| 204 | } |
| 36 | Curl_cookie_add() |
| C2: Provided source lines 36–117 contain only comments and the cap_expires helper; the Curl_cookie_add function body is not included | |
| 37 | |
| 38 | Adds a cookie to the in-memory cookie jar. |
| 39 | |
| 40 | |
| 41 | SENDING COOKIE INFORMATION |
| 42 | ========================== |
| 43 | |
| 44 | Curl_cookie_getlist() |
| 45 | |
| 46 | For a given host and path, return a linked list of cookies that |
| 47 | the client should send to the server if used now. The secure |
| 48 | boolean informs the cookie if a secure connection is achieved or |
| 49 | not. |
| 50 | |
| 51 | It shall only return cookies that have not expired. |
| 52 | |
| 53 | Example set of cookies: |
| 54 | |
| 55 | Set-cookie: PRODUCTINFO=webxpress; domain=.fidelity.com; path=/; secure |
| 56 | Set-cookie: PERSONALIZE=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 57 | domain=.fidelity.com; path=/ftgw; secure |
| 58 | Set-cookie: FidHist=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 59 | domain=.fidelity.com; path=/; secure |
| 60 | Set-cookie: FidOrder=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 61 | domain=.fidelity.com; path=/; secure |
| 62 | Set-cookie: DisPend=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 63 | domain=.fidelity.com; path=/; secure |
| 64 | Set-cookie: FidDis=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 65 | domain=.fidelity.com; path=/; secure |
| 66 | Set-cookie: |
| 67 | Session_Key@6791a9e0-901a-11d0-a1c8-9b012c88aa77=none;expires=Monday, |
| 68 | 13-Jun-1988 03:04:55 GMT; domain=.fidelity.com; path=/; secure |
| 69 | ****/ |
| 70 | |
| 71 | |
| 72 | #include "curl_setup.h" |
| 73 | |
| 74 | #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_COOKIES) |
| 75 | |
| 76 | #include "urldata.h" |
| 77 | #include "cookie.h" |
| 78 | #include "psl.h" |
| 79 | #include "sendf.h" |
| 80 | #include "slist.h" |
| 81 | #include "share.h" |
| 82 | #include "strcase.h" |
| 83 | #include "curl_get_line.h" |
| 84 | #include "curl_memrchr.h" |
| 85 | #include "parsedate.h" |
| 86 | #include "rename.h" |
| 87 | #include "fopen.h" |
| 88 | #include "strdup.h" |
| 89 | #include "llist.h" |
| 90 | #include "strparse.h" |
| 91 | |
| 92 | /* The last 3 #include files should be in this order */ |
| 93 | #include "curl_printf.h" |
| 94 | #include "curl_memory.h" |
| 95 | #include "memdebug.h" |
| 96 | |
| 97 | static void strstore(char **str, const char *newstr, size_t len); |
| 98 | |
| 99 | /* number of seconds in 400 days */ |
| 100 | #define COOKIES_MAXAGE (400*24*3600) |
| 101 | |
| 102 | /* Make sure cookies never expire further away in time than 400 days into the |
| 103 | future. (from RFC6265bis draft-19) |
| 104 | |
| 105 | For the sake of easier testing, align the capped time to an even 60 second |
| 106 | boundary. |
| 107 | */ |
| 108 | static void cap_expires(time_t now, struct Cookie *co) |
| 109 | { |
| 110 | if((TIME_T_MAX - COOKIES_MAXAGE - 30) > now) { |
| 111 | timediff_t cap = now + COOKIES_MAXAGE; |
| 112 | if(co->expires > cap) { |
| 113 | cap += 30; |
| 114 | co->expires = (cap/60)*60; |
| 115 | } |
| 116 | } |
| 117 | } |
Show 9 lines (not cited by the judge)
| 119 | static void freecookie(struct Cookie *co) |
| 120 | { |
| 121 | free(co->domain); |
| 122 | free(co->path); |
| 123 | free(co->spath); |
| 124 | free(co->name); |
| 125 | free(co->value); |
| 126 | free(co); |
| 127 | } |