CVE-2023-38546: cookie injection with none file
What the report claims
A cookie injection vulnerability exists when duplicating a curl easy handle if a file named 'none' exists in the current working directory, allowing cookies to be injected into the duplicated handle.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2023-38546 cookie injection with none file
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 behavior described is the intended cookie persistence feature: curl_easy_duphandle() duplicates the parent handle's cookie configuration including its filename (lib/easy.c:925-934).
- The default cookie filename 'none' is a documented design choice (lib/cookie.c:31). Applications that enable cookies without setting a custom filename will use 'none'; this is not a vulnerability.
- Loading cookies from the configured cookie file and sending them with requests is the documented purpose of libcurl's cookie support, not 'injection'.
- Exploitation requires the attacker to already have write access to the application's current working directory and the application to have explicitly enabled cookies with the default filename — a configuration issue, not a curl vulnerability.
How this analysis ran 5 claims · 5 citations verified
Checked against curl master on 2023-09-14 (267e14f1bab6). Test 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.
This report describes intended behavior, not a vulnerability. curl_easy_duphandle() duplicates the parent handle's cookie configuration including its filename (lib/easy.c:925-934). The default cookie filename is 'none' (lib/cookie.c:31), so if the application enables cookies without setting CURLOPT_COOKIEFILE, both handles will load cookies from a file named 'none' in the cwd. This is the documented cookie persistence feature. Applications that do not want this should set an explicit cookie filename or disable cookies. An attacker who can write files to the application's cwd already has significant control; this does not constitute a security flaw in curl.
Inspect all claims and source code 5 claims · 2 code excerpts
Report claims
A file named 'none' in the current working directory causes cookies to be injected into a duplicated handle during curl_easy_duphandle()
Read the reasoning
The report frames loading cookies from the configured cookie file as 'injection'. In reality, curl_easy_duphandle() duplicates the parent handle's cookie configuration including its filename (line 929). If the parent handle uses the default filename 'none', the duplicate will also use 'none' and load cookies from that file. This is the intended cookie persistence feature, not a vulnerability. The default filename 'none' is a design choice; applications that enable cookies should set an explicit filename via CURLOPT_COOKIEFILE if they do not want to use the default.
The flaw occurs in curl_easy_duphandle() when copying cookies from the parent handle to the duplicated handle
Read the reasoning
The code at lines 925-934 explicitly documents this as intended behavior: 'If cookies are enabled in the parent handle, we enable them in the clone as well!' Duplicating the cookie configuration (including the filename) is the correct semantics for a handle duplication function.
The presence of a file named 'none' in the current working directory triggers the injection
Read the reasoning
The presence of a file named 'none' triggers cookie loading only because 'none' is the default cookie filename when the application enables cookies without specifying a custom filename. This is configuration-dependent behavior, not an unconditional trigger. An application that sets CURLOPT_COOKIEFILE to a different path would not be affected.
Cookies can be injected into an HTTP request via the duplicated handle
Read the reasoning
Sending cookies from the cookie jar with HTTP requests is the documented purpose of libcurl's cookie support. The duplicated handle inherits the parent's cookie configuration, so it will send the same cookies. This is not 'injection' — it is the feature working as designed.
The proof of concept uses a file named 'none' and a compiled poc_cookieinj program to inject cookies
Read the reasoning
The PoC is not provided in the source code. The deterministic checks only confirm the quoted lines exist in the repository. Whether the PoC demonstrates a vulnerability or merely the intended cookie-loading behavior cannot be verified from the provided code alone.
Read the original report
Source code
| 31 | struct CookieInfo *Curl_cookie_init(struct Curl_easy *data, |
| C1: Curl_cookie_init declaration shows it takes a file parameter; the quoted implementation uses file?file:"none" as the default when no filename is provided | |
| C3: The default filename 'none' is used only when no explicit filename is provided to Curl_cookie_init | |
| 32 | const char *file, struct CookieInfo *inc, bool newsession); |
| 33 | |
| 34 | Inits a cookie struct to store data in a local file. This is always |
| 35 | called before any cookies are set. |
| 36 | |
| 37 | struct Cookie *Curl_cookie_add(struct Curl_easy *data, |
| 38 | struct CookieInfo *c, bool httpheader, bool noexpire, |
| 39 | char *lineptr, const char *domain, const char *path, |
| 40 | bool secure); |
| 41 | |
| 42 | The 'lineptr' parameter is a full "Set-cookie:" line as |
| 43 | received from a server. |
| 44 | |
| 45 | The function need to replace previously stored lines that this new |
| 46 | line supersedes. |
| 47 | |
| 48 | It may remove lines that are expired. |
| 49 | |
| 50 | It should return an indication of success/error. |
| 51 | |
| 52 | |
| 53 | SENDING COOKIE INFORMATION |
| 54 | ========================== |
| 55 | |
| 56 | struct Cookies *Curl_cookie_getlist(struct CookieInfo *cookie, |
| 57 | char *host, char *path, bool secure); |
| 58 | |
| 59 | For a given host and path, return a linked list of cookies that |
| 60 | the client should send to the server if used now. The secure |
| 61 | boolean informs the cookie if a secure connection is achieved or |
| 62 | not. |
| 63 | |
| 64 | It shall only return cookies that haven't expired. |
| 65 | |
| 66 | |
| 67 | Example set of cookies: |
| 68 | |
| 69 | Set-cookie: PRODUCTINFO=webxpress; domain=.fidelity.com; path=/; secure |
| 70 | Set-cookie: PERSONALIZE=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 71 | domain=.fidelity.com; path=/ftgw; secure |
| 72 | Set-cookie: FidHist=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 73 | domain=.fidelity.com; path=/; secure |
| 74 | Set-cookie: FidOrder=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 75 | domain=.fidelity.com; path=/; secure |
| 76 | Set-cookie: DisPend=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 77 | domain=.fidelity.com; path=/; secure |
| 78 | Set-cookie: FidDis=none;expires=Monday, 13-Jun-1988 03:04:55 GMT; |
| 79 | domain=.fidelity.com; path=/; secure |
| 80 | Set-cookie: |
| 81 | Session_Key@6791a9e0-901a-11d0-a1c8-9b012c88aa77=none;expires=Monday, |
| 82 | 13-Jun-1988 03:04:55 GMT; domain=.fidelity.com; path=/; secure |
| 83 | ****/ |
| 84 | |
| 85 | |
| 86 | #include "curl_setup.h" |
| 87 | |
| 88 | #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_COOKIES) |
| 89 | |
| 90 | #include "urldata.h" |
| 91 | #include "cookie.h" |
| 92 | #include "psl.h" |
| 93 | #include "strtok.h" |
| 94 | #include "sendf.h" |
| 95 | #include "slist.h" |
| 96 | #include "share.h" |
| 97 | #include "strtoofft.h" |
| 98 | #include "strcase.h" |
| 99 | #include "curl_get_line.h" |
| 100 | #include "curl_memrchr.h" |
| 101 | #include "parsedate.h" |
| 102 | #include "rename.h" |
| 103 | #include "fopen.h" |
| 104 | #include "strdup.h" |
| 105 | |
| 106 | /* The last 3 #include files should be in this order */ |
| 107 | #include "curl_printf.h" |
| 108 | #include "curl_memory.h" |
| 109 | #include "memdebug.h" |
| 110 | |
| 111 | static void strstore(char **str, const char *newstr, size_t len); |
| 112 | |
| 113 | static void freecookie(struct Cookie *co) |
| 114 | { |
| 115 | free(co->expirestr); |
| 116 | free(co->domain); |
| 117 | free(co->path); |
| 118 | free(co->spath); |
| 119 | free(co->name); |
| 120 | free(co->value); |
| 121 | free(co->maxage); |
| 122 | free(co->version); |
| 123 | free(co); |
| 124 | } |
| 896 | struct Curl_easy *curl_easy_duphandle(struct Curl_easy *data) |
| 897 | { |
| 898 | struct Curl_easy *outcurl = calloc(1, sizeof(struct Curl_easy)); |
| 899 | if(!outcurl) |
| 900 | goto fail; |
| 901 | |
| 902 | /* |
| 903 | * We setup a few buffers we need. We should probably make them |
| 904 | * get setup on-demand in the code, as that would probably decrease |
| 905 | * the likeliness of us forgetting to init a buffer here in the future. |
| 906 | */ |
| 907 | outcurl->set.buffer_size = data->set.buffer_size; |
| 908 | |
| 909 | /* copy all userdefined values */ |
| 910 | if(dupset(outcurl, data)) |
| 911 | goto fail; |
| 912 | |
| 913 | Curl_dyn_init(&outcurl->state.headerb, CURL_MAX_HTTP_HEADER); |
| 914 | |
| 915 | /* the connection cache is setup on demand */ |
| 916 | outcurl->state.conn_cache = NULL; |
| 917 | outcurl->state.lastconnect_id = -1; |
| 918 | outcurl->state.recent_conn_id = -1; |
| 919 | outcurl->id = -1; |
| 920 | |
| 921 | outcurl->progress.flags = data->progress.flags; |
| 922 | outcurl->progress.callback = data->progress.callback; |
| 923 | |
| 924 | #ifndef CURL_DISABLE_COOKIES |
| 925 | if(data->cookies) { |
| C2: The code block at lines 925-934 shows curl_easy_duphandle enabling cookies in the clone when the parent has cookies enabled — this is documented as intended behavior ('we enable them in the clone as well!') | |
| 926 | /* If cookies are enabled in the parent handle, we enable them |
| 927 | in the clone as well! */ |
| 928 | outcurl->cookies = Curl_cookie_init(data, |
| C1: curl_easy_duphandle passes data->cookies->filename to Curl_cookie_init, which is the parent handle's configured cookie filename | |
| C4: Cookies from the configured file are loaded into the duplicated handle as part of normal cookie support | |
| 929 | data->cookies->filename, |
| 930 | outcurl->cookies, |
| 931 | data->set.cookiesession); |
| 932 | if(!outcurl->cookies) |
| 933 | goto fail; |
| 934 | } |
| 935 | |
| 936 | if(data->set.cookielist) { |
| 937 | outcurl->set.cookielist = Curl_slist_duplicate(data->set.cookielist); |
| 938 | if(!outcurl->set.cookielist) |
| 939 | goto fail; |
| 940 | } |
| 941 | #endif |
| 942 | |
| 943 | if(data->state.url) { |
| 944 | outcurl->state.url = strdup(data->state.url); |
| 945 | if(!outcurl->state.url) |
| 946 | goto fail; |
| 947 | outcurl->state.url_alloc = TRUE; |
| 948 | } |
| 949 | |
| 950 | if(data->state.referer) { |
| 951 | outcurl->state.referer = strdup(data->state.referer); |
| 952 | if(!outcurl->state.referer) |
| 953 | goto fail; |
| 954 | outcurl->state.referer_alloc = TRUE; |
| 955 | } |
| 956 | |
| 957 | /* Reinitialize an SSL engine for the new handle |
| 958 | * note: the engine name has already been copied by dupset */ |
| 959 | if(outcurl->set.str[STRING_SSL_ENGINE]) { |
| 960 | if(Curl_ssl_set_engine(outcurl, outcurl->set.str[STRING_SSL_ENGINE])) |
| 961 | goto fail; |
| 962 | } |
| 963 | |
| 964 | #ifndef CURL_DISABLE_ALTSVC |
| 965 | if(data->asi) { |
| 966 | outcurl->asi = Curl_altsvc_init(); |
| 967 | if(!outcurl->asi) |
| 968 | goto fail; |
| 969 | if(outcurl->set.str[STRING_ALTSVC]) |
| 970 | (void)Curl_altsvc_load(outcurl->asi, outcurl->set.str[STRING_ALTSVC]); |
| 971 | } |
| 972 | #endif |
| 973 | #ifndef CURL_DISABLE_HSTS |
| 974 | if(data->hsts) { |
| 975 | outcurl->hsts = Curl_hsts_init(); |
| 976 | if(!outcurl->hsts) |
| 977 | goto fail; |
| 978 | if(outcurl->set.str[STRING_HSTS]) |
| 979 | (void)Curl_hsts_loadfile(outcurl, |
| 980 | outcurl->hsts, outcurl->set.str[STRING_HSTS]); |
| 981 | (void)Curl_hsts_loadcb(outcurl, outcurl->hsts); |
| 982 | } |
| 983 | #endif |
| 984 | /* Clone the resolver handle, if present, for the new handle */ |
| 985 | if(Curl_resolver_duphandle(outcurl, |
| 986 | &outcurl->state.async.resolver, |
| 987 | data->state.async.resolver)) |
| 988 | goto fail; |
| 989 | |
| 990 | #ifdef USE_ARES |
| 991 | { |
| 992 | CURLcode rc; |
| 993 | |
| 994 | rc = Curl_set_dns_servers(outcurl, data->set.str[STRING_DNS_SERVERS]); |
| 995 | if(rc && rc != CURLE_NOT_BUILT_IN) |
| 996 | goto fail; |
| 997 | |
| 998 | rc = Curl_set_dns_interface(outcurl, data->set.str[STRING_DNS_INTERFACE]); |
| 999 | if(rc && rc != CURLE_NOT_BUILT_IN) |
| 1000 | goto fail; |
| 1001 | |
| 1002 | rc = Curl_set_dns_local_ip4(outcurl, data->set.str[STRING_DNS_LOCAL_IP4]); |
| 1003 | if(rc && rc != CURLE_NOT_BUILT_IN) |
| 1004 | goto fail; |
| 1005 | |
| 1006 | rc = Curl_set_dns_local_ip6(outcurl, data->set.str[STRING_DNS_LOCAL_IP6]); |
| 1007 | if(rc && rc != CURLE_NOT_BUILT_IN) |
| 1008 | goto fail; |
| 1009 | } |
| 1010 | #endif /* USE_ARES */ |
| 1011 | |
| 1012 | Curl_initinfo(outcurl); |
| 1013 | |
| 1014 | outcurl->magic = CURLEASY_MAGIC_NUMBER; |
| 1015 | |
| 1016 | /* we reach this point and thus we are OK */ |
| 1017 | |
| 1018 | return outcurl; |
| 1019 | |
| 1020 | fail: |
| 1021 | |
| 1022 | if(outcurl) { |
| 1023 | #ifndef CURL_DISABLE_COOKIES |
| 1024 | curl_slist_free_all(outcurl->set.cookielist); |
| 1025 | outcurl->set.cookielist = NULL; |
| 1026 | #endif |
| 1027 | Curl_safefree(outcurl->state.buffer); |
| 1028 | Curl_dyn_free(&outcurl->state.headerb); |
| 1029 | Curl_safefree(outcurl->state.url); |
| 1030 | Curl_safefree(outcurl->state.referer); |
| 1031 | Curl_altsvc_cleanup(&outcurl->asi); |
| 1032 | Curl_hsts_cleanup(&outcurl->hsts); |
| 1033 | Curl_freeset(outcurl); |
| 1034 | free(outcurl); |
| 1035 | } |
| 1036 | |
| 1037 | return NULL; |
| 1038 | } |