Cookie Replacement Use-After-Free Vulnerability
What the report claims
The cookie replacement logic in lib/cookie.c contains a use-after-free vulnerability in the replace_existing() function due to modifying a linked list while iterating over it.
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 code at lib/cookie.c:915 accesses repl->creationtime BEFORE freecookie(repl, TRUE) at line 921 - this is use-before-free, not use-after-free
- The linked list iteration (lines 833-910) completes fully before any node removal (line 918) or free (line 921) occurs - no modification during iteration
- The report's central claim of use-after-free is directly contradicted by the provided source code
How this analysis ran 5 claims · 7 citations verified
Checked against curl master on 2026-01-19 (9703dabd77b5). 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. However, the code in lib/cookie.c shows that replace_existing() accesses the old cookie's creationtime (line 915) BEFORE calling freecookie() (line 921), and the list iteration completes entirely before any modification. This is not a use-after-free. The PoC file was not provided for verification. Could you clarify if you're seeing a different code path or version?
Inspect all claims and source code 5 claims · 6 code excerpts
Report claims
replace_existing() modifies a linked list while iterating over it, creating potential for memory corruption
Read the reasoning
The report claims a use-after-free where 'co->creationtime = repl->creationtime' accesses freed memory. However, the code shows the access at line 915 occurs before freecookie() at line 921. The linked list iteration (lines 833-910) completes entirely before any modification (lines 911-922). No use-after-free exists in the provided code.
lib/cookie.c:822-925
Read the reasoning
The location claim is accurate - the function spans lines 822-925 in the provided source.
race condition server triggers the vulnerability
Read the reasoning
The PoC server code (poc_cookie_uaf.py) was not provided in the source material, so the trigger claim cannot be verified against the codebase.
potential for arbitrary code execution
Read the reasoning
Since the use-after-free claim (C1) is contradicted by the code, the claimed impact of arbitrary code execution from that flaw is also unsupported.
run curl with AddressSanitizer against exploit1, exploit2, or exploit3 endpoints
Read the reasoning
The reproduction steps reference a PoC file and exploit endpoints that were not provided in the source material.
What the report says about other projects
not addressed AddressSanitizer is required to detect the vulnerability reliably
not addressed The vulnerability is classified as a use-after-free error (CWE-416)
Read the original report (excerpt)
Source code
| 797 | Curl_strntolower(lcase, domain, dlen + 1); |
| 798 | Curl_strntolower(lcookie, co->domain, clen + 1); |
| 799 | acceptable = psl_is_cookie_domain_acceptable(psl, lcase, lcookie); |
| 800 | Curl_psl_release(data); |
| 801 | } |
| 802 | else |
| 803 | infof(data, "libpsl problem, rejecting cookie for safety"); |
| 804 | } |
| 805 | |
| 806 | if(!acceptable) { |
| 807 | infof(data, "cookie '%s' dropped, domain '%s' must not " |
| 808 | "set cookies for '%s'", co->name, domain, co->domain); |
| 809 | return TRUE; |
| 810 | } |
| 811 | } |
| 812 | #else |
| 813 | (void)data; |
| 814 | (void)co; |
| 815 | (void)domain; |
| 816 | DEBUGF(infof(data, "NO PSL to check set-cookie '%s' for domain=%s in %s", |
| 817 | co->name, co->domain, domain)); |
| 818 | #endif |
| 819 | return FALSE; |
| 820 | } |
| 821 | |
| 822 | /* returns TRUE when replaced */ |
| C2: replace_existing function starts at line 822 | |
| 823 | static bool replace_existing(struct Curl_easy *data, |
| 824 | struct Cookie *co, |
| 825 | struct CookieInfo *ci, |
| 826 | bool secure, |
| 827 | bool *replacep) |
| 828 | { |
| 829 | bool replace_old = FALSE; |
| 830 | struct Curl_llist_node *replace_n = NULL; |
| 831 | struct Curl_llist_node *n; |
| 832 | size_t myhash = cookiehash(co->domain); |
| 833 | for(n = Curl_llist_head(&ci->cookielist[myhash]); n; n = Curl_node_next(n)) { |
| 834 | struct Cookie *clist = Curl_node_elem(n); |
| 835 | if(!strcmp(clist->name, co->name)) { |
| 836 | /* the names are identical */ |
| 837 | bool matching_domains = FALSE; |
| 838 | |
| 839 | if(clist->domain && co->domain) { |
| 840 | if(curl_strequal(clist->domain, co->domain)) |
| 841 | /* The domains are identical */ |
| 842 | matching_domains = TRUE; |
| 843 | } |
| 844 | else if(!clist->domain && !co->domain) |
| 845 | matching_domains = TRUE; |
| 846 | |
| 847 | if(matching_domains && /* the domains were identical */ |
| 848 | clist->path && co->path && /* both have paths */ |
| 849 | clist->secure && !co->secure && !secure) { |
| 850 | size_t cllen; |
| 851 | const char *sep = NULL; |
| 852 | |
| 853 | /* |
| 854 | * A non-secure cookie may not overlay an existing secure cookie. |
| 855 | * For an existing cookie "a" with path "/login", refuse a new |
| 856 | * cookie "a" with for example path "/login/en", while the path |
| 857 | * "/loginhelper" is ok. |
| 858 | */ |
| 859 | |
| 860 | DEBUGASSERT(clist->path[0]); |
| 861 | if(clist->path[0]) |
| 862 | sep = strchr(clist->path + 1, '/'); |
| 863 | if(sep) |
| 864 | cllen = sep - clist->path; |
| 865 | else |
| 866 | cllen = strlen(clist->path); |
| 867 | |
| 868 | if(curl_strnequal(clist->path, co->path, cllen)) { |
| 869 | infof(data, "cookie '%s' for domain '%s' dropped, would " |
| 870 | "overlay an existing cookie", co->name, co->domain); |
| 871 | return FALSE; |
| 872 | } |
| 873 | } |
| 874 | } |
| 875 | |
| 876 | if(!replace_n && !strcmp(clist->name, co->name)) { |
| 877 | /* the names are identical */ |
| 878 | |
| 879 | if(clist->domain && co->domain) { |
| 880 | if(curl_strequal(clist->domain, co->domain) && |
| 881 | (clist->tailmatch == co->tailmatch)) |
| 882 | /* The domains are identical */ |
| 883 | replace_old = TRUE; |
| 884 | } |
| 885 | else if(!clist->domain && !co->domain) |
| 886 | replace_old = TRUE; |
| 887 | |
| 888 | if(replace_old) { |
| 889 | /* the domains were identical */ |
| 890 | |
| 891 | if(clist->path && co->path && |
| 892 | !curl_strequal(clist->path, co->path)) |
| 893 | replace_old = FALSE; |
| 894 | else if(!clist->path != !co->path) |
| 895 | replace_old = FALSE; |
| 896 | } |
| 897 | |
| 898 | if(replace_old && !co->livecookie && clist->livecookie) { |
| 899 | /* |
| 900 | * Both cookies matched fine, except that the already present cookie |
| 901 | * is "live", which means it was set from a header, while the new one |
| 902 | * was read from a file and thus is not "live". "live" cookies are |
| 903 | * preferred so the new cookie is freed. |
| 904 | */ |
| 905 | return FALSE; |
| 906 | } |
| 907 | if(replace_old) |
| 908 | replace_n = n; |
| 909 | } |
| 910 | } |
| 911 | if(replace_n) { |
| C1: The if(replace_n) block starts after the for loop completes at line 910 | |
| 912 | struct Cookie *repl = Curl_node_elem(replace_n); |
| 913 | |
| 914 | /* when replacing, creationtime is kept from old */ |
| 915 | co->creationtime = repl->creationtime; |
| C1: co->creationtime = repl->creationtime; accesses repl BEFORE freecookie is called | |
| C4: Access to repl->creationtime happens before free | |
| 916 | |
| 917 | /* unlink the old */ |
| 918 | Curl_node_remove(replace_n); |
| 919 | |
| 920 | /* free the old cookie */ |
| 921 | freecookie(repl, TRUE); |
| C1: freecookie(repl, TRUE); is called AFTER the creationtime copy, not before | |
| C4: freecookie called after access | |
| 922 | } |
| 923 | *replacep = replace_old; |
| 924 | return TRUE; |
| 925 | } |
| C2: replace_existing function ends at line 925 | |
| 926 | |
| 927 | /* |
| 928 | * Curl_cookie_add |
| 929 | * |
| 930 | * Add a single cookie line to the cookie keeping object. Be aware that |
| 931 | * sometimes we get an IP-only hostname, and that might also be a numerical |
| 932 | * IPv6 address. |
| 933 | * |
| 934 | */ |
| 935 | CURLcode |
| 936 | Curl_cookie_add(struct Curl_easy *data, |
| 937 | struct CookieInfo *ci, |
| 938 | bool httpheader, /* TRUE if HTTP header-style line */ |
| 939 | bool noexpire, /* if TRUE, skip remove_expired() */ |
| 940 | const char *lineptr, /* first character of the line */ |
| 941 | const char *domain, /* default domain */ |
| 942 | const char *path, /* full path used when this cookie is set, |
| 943 | used to get default path for the cookie |
| 944 | unless set */ |
| 945 | bool secure) /* TRUE if connection is over secure origin */ |
| 946 | { |
| 947 | struct Cookie comem; |
| 948 | struct Cookie *co; |
| 949 | size_t myhash; |
| 950 | CURLcode result; |
| 207 | struct Curl_llist_node *Curl_llist_head(struct Curl_llist *list) |
| 208 | { |
| 209 | DEBUGASSERT(list); |
| 210 | DEBUGASSERT(list->_init == LLISTINIT); |
| 211 | return VERIFYNODE(list->_head); |
| 212 | } |
Show 6 lines (not cited by the judge)
| 234 | void *Curl_node_elem(struct Curl_llist_node *n) |
| 235 | { |
| 236 | DEBUGASSERT(n); |
| 237 | DEBUGASSERT(n->_init == NODEINIT); |
| 238 | return n->_ptr; |
| 239 | } |
Show 6 lines (not cited by the judge)
| 243 | struct Curl_llist_node *Curl_node_next(struct Curl_llist_node *n) |
| 244 | { |
| 245 | DEBUGASSERT(n); |
| 246 | DEBUGASSERT(n->_init == NODEINIT); |
| 247 | return VERIFYNODE(n->_next); |
| 248 | } |
Show 4 lines (not cited by the judge)
| 191 | void Curl_node_remove(struct Curl_llist_node *e) |
| 192 | { |
| 193 | Curl_node_uremove(e, NULL); |
| 194 | } |
Show 9 lines (not cited by the judge)
| 64 | static void freecookie(struct Cookie *co, bool maintoo) |
| 65 | { |
| 66 | curlx_free(co->domain); |
| 67 | curlx_free(co->path); |
| 68 | curlx_free(co->name); |
| 69 | curlx_free(co->value); |
| 70 | if(maintoo) |
| 71 | curlx_free(co); |
| 72 | } |