CVE-2026-82209: domain-scoped PSL domain cookie
What the report claims
curl incorrectly stores a Set-Cookie with Domain exactly equal to a public suffix as domain-scoped, causing the cookie to be sent to sibling hosts under the same public suffix.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2026-82209 domain-scoped PSL domain cookie
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code supports the reported flaw. A maintainer should review its impact and decide what to do next.
Why this finding?
- parse_domain() unconditionally sets tailmatch=TRUE for all non-IP Domain attributes (lib/cookie.c:511-512)
- is_public_suffix() accepts exact-match public suffix cookies via psl_is_cookie_domain_acceptable() but does not clear tailmatch (lib/cookie.c:840, 856-860)
- Curl_cookie_getlist() then sends the cookie to sibling domains via cookie_tailmatch() suffix matching (lib/cookie.c:1344-1347, 95-98)
- This violates the PSL security boundary: cookies set on a public suffix apex with explicit Domain=apex leak to sibling domains
- The suggested fix (clearing tailmatch after PSL acceptance for exact-match public suffix) addresses the root cause
- libpsl is enabled by default in most distribution packages per the report, so this affects typical curl builds
What still needs checking
- Direct confirmation of libpsl's exact-match behavior from libpsl documentation or source
How this analysis ran 5 claims · 9 citations verified
Checked against curl tested commit c2676bf9e639 (c2676bf9e639). 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.
Thank you for the detailed report. The code confirms the flaw: parse_domain() sets tailmatch=TRUE unconditionally (lib/cookie.c:511), is_public_suffix() accepts exact-match public-suffix cookies via libpsl but leaves tailmatch=TRUE (lib/cookie.c:840, 856), and Curl_cookie_getlist() then leaks the cookie to sibling domains via cookie_tailmatch() (lib/cookie.c:1344-1347, 95-98). Your suggested fix to clear tailmatch after PSL acceptance for exact-match public suffixes is sound. We'll investigate the libpsl contract and apply a fix. One question: do you have a reference to libpsl's documented behavior for exact-match cases?
Inspect all claims and source code 5 claims · 10 code excerpts
Report claims
curl stores a Set-Cookie with Domain exactly equal to a public suffix as domain-scoped (tailmatch=TRUE) instead of host-only
Read the reasoning
The code shows parse_domain() sets tailmatch=TRUE for all non-IP domains (line 511-512). When the domain is a public suffix and exactly matches the request host, libpsl returns acceptable, is_public_suffix() allows the cookie (returns FALSE), but tailmatch remains TRUE, making the cookie domain-scoped instead of host-only.
the flaw occurs in lib/cookie.c in parse_domain() where tailmatch is unconditionally set for non-IP Domain attributes
Read the reasoning
The flaw originates in parse_domain() at line 511-512 where tailmatch is unconditionally set for non-IP Domain attributes, with no exception for exact-match public suffix cases.
a Set-Cookie header with Domain attribute exactly matching the request host (e.g., github.io) is accepted by psl_is_cookie_domain_acceptable()
Read the reasoning
The code calls psl_is_cookie_domain_acceptable() but the provided external sources (E6) discuss Microsoft Purview EDM, not libpsl behavior. The report's claim about libpsl returning 'acceptable' for exact equality is plausible given the code structure but not confirmed by the provided excerpts.
the cookie is sent to any sibling host (e.g., attacker.github.io) on subsequent requests or redirects
Read the reasoning
When tailmatch=TRUE, Curl_cookie_getlist() uses cookie_tailmatch() which matches sibling domains (e.g., 'github.io' cookie domain matches 'attacker.github.io' host because 'github.io' is a suffix with a preceding dot).
using curl with --resolve to map github.io and attacker.github.io to localhost, making a request that sets a Domain=github.io cookie and follows the redirect to attacker.github.io
Read the reasoning
The reproduction steps exercise the exact code path: setting a Domain=github.io cookie from github.io (public suffix apex), which is accepted by libpsl, stored with tailmatch=TRUE, then sent to attacker.github.io on redirect via cookie_tailmatch() suffix matching.
Deterministic checks that failed
- quote only 0/4 quoted lines appear anywhere in curl at c2676bf9e639
What the report says about other projects
not addressed RFC 6265 §5.3 requires exact-match Domain case to produce a host-only cookie
not addressed libpsl returns 'acceptable' for exact equality between request host and public suffix domain
Read the original report (excerpt)
Source code
| 1314 | CURLcode Curl_cookie_getlist(struct Curl_easy *data, |
| 1315 | bool *okay, |
| 1316 | const char *host, |
| 1317 | struct Curl_llist *list) |
| 1318 | { |
| 1319 | size_t matches = 0; |
| 1320 | const bool is_ip = Curl_host_is_ipnum(host); |
| 1321 | const size_t myhash = cookiehash(host); |
| 1322 | struct Curl_llist_node *n; |
| 1323 | const bool secure = Curl_secure_context(data, host); |
| 1324 | struct CookieInfo *ci = data->cookies; |
| 1325 | const char *path = data->state.up.path; |
| 1326 | CURLcode result = CURLE_OK; |
| 1327 | *okay = FALSE; |
| 1328 | |
| 1329 | Curl_llist_init(list, NULL); |
| 1330 | |
| 1331 | if(!ci || !Curl_llist_count(&ci->cookielist[myhash])) |
| 1332 | return CURLE_OK; /* no cookie struct or no cookies in the struct */ |
| 1333 | |
| 1334 | /* at first, remove expired cookies */ |
| 1335 | remove_expired(ci); |
| 1336 | |
| 1337 | for(n = Curl_llist_head(&ci->cookielist[myhash]); n; n = Curl_node_next(n)) { |
| 1338 | struct Cookie *co = Curl_node_elem(n); |
| 1339 | |
| 1340 | /* if the cookie requires we are secure we must only continue if we are! */ |
| 1341 | if(co->secure ? secure : TRUE) { |
| 1342 | |
| 1343 | /* now check if the domain is correct */ |
| 1344 | if(!co->domain || |
| C4: Curl_cookie_getlist() checks (co->tailmatch && !is_ip && cookie_tailmatch(co->domain, strlen(co->domain), host)) | |
| C5: The reproduction path follows the code flow: parse_domain() sets tailmatch=TRUE, is_public_suffix() accepts via libpsl, Curl_cookie_getlist() sends cookie to sibling via cookie_tailmatch() | |
| 1345 | (co->tailmatch && !is_ip && |
| 1346 | cookie_tailmatch(co->domain, strlen(co->domain), host)) || |
| 1347 | ((!co->tailmatch || is_ip) && curl_strequal(host, co->domain))) { |
| 1348 | /* |
| 1349 | * the right part of the host matches the domain stuff in the |
| 1350 | * cookie data |
| 1351 | */ |
| 1352 | |
| 1353 | /* |
| 1354 | * now check the left part of the path with the cookies path |
| 1355 | * requirement |
| 1356 | */ |
| 1357 | if(!co->path || pathmatch(co->path, path)) { |
| 1358 | |
| 1359 | /* |
| 1360 | * This is a match and we add it to the return-linked-list |
| 1361 | */ |
| 1362 | Curl_llist_append(list, co, &co->getnode); |
| 1363 | matches++; |
| 1364 | if(matches >= MAX_COOKIE_SEND_AMOUNT) { |
| 1365 | infof(data, "Included max number of cookies (%zu) in request!", |
| 1366 | matches); |
| 1367 | break; |
| 1368 | } |
| 1369 | } |
| 1370 | } |
| 1371 | } |
| 1372 | } |
| 1373 | |
| 1374 | if(matches) { |
| 1375 | /* |
| 1376 | * Now we need to make sure that if there is a name appearing more than |
| 1377 | * once, the longest specified path version comes first. To make this the |
| 1378 | * swiftest way, we sort them all based on path length. |
| 1379 | */ |
| 1380 | struct Cookie **array; |
| 1381 | size_t i; |
| 1382 | |
| 1383 | /* alloc an array and store all cookie pointers */ |
| 1384 | array = curlx_malloc(sizeof(struct Cookie *) * matches); |
| 1385 | if(!array) { |
| 1386 | result = CURLE_OUT_OF_MEMORY; |
| 1387 | goto fail; |
| 1388 | } |
| 1389 | |
| 1390 | n = Curl_llist_head(list); |
| 1391 | |
| 1392 | for(i = 0; n; n = Curl_node_next(n)) |
| 1393 | array[i++] = Curl_node_elem(n); |
| 1394 | |
| 1395 | /* now sort the cookie pointers in path length order */ |
| 1396 | qsort(array, matches, sizeof(struct Cookie *), cookie_sort); |
| 1397 | |
| 1398 | /* remake the linked list order according to the new order */ |
| 1399 | Curl_llist_destroy(list, NULL); |
| 1400 | |
| 1401 | for(i = 0; i < matches; i++) |
| 1402 | Curl_llist_append(list, array[i], &array[i]->getnode); |
| 1403 | |
| 1404 | curlx_free(array); /* remove the temporary data again */ |
| 1405 | } |
| 1406 | |
| 1407 | *okay = TRUE; |
| 1408 | return CURLE_OK; /* success */ |
| 1409 | |
| 1410 | fail: |
| 1411 | /* failure, clear up the allocated chain and return NULL */ |
| 1412 | Curl_llist_destroy(list, NULL); |
| 1413 | return result; /* error */ |
| 1414 | } |
| 73 | static bool cookie_tailmatch(const char *cookie_domain, |
| 74 | const size_t cookie_domain_len, |
| 75 | const char *hostname) |
| 76 | { |
| 77 | size_t hostname_len = strlen(hostname); |
| 78 | |
| 79 | if(hostname_len < cookie_domain_len) |
| 80 | return FALSE; |
| 81 | |
| 82 | if(!curl_strnequal(cookie_domain, |
| 83 | hostname + hostname_len - cookie_domain_len, |
| 84 | cookie_domain_len)) |
| 85 | return FALSE; |
| 86 | |
| 87 | /* |
| 88 | * A lead char of cookie_domain is not '.'. |
| 89 | * RFC6265 4.1.2.3. The Domain Attribute says: |
| 90 | * For example, if the value of the Domain attribute is |
| 91 | * "example.com", the user agent will include the cookie in the Cookie |
| 92 | * header when making HTTP requests to example.com, www.example.com, and |
| 93 | * www.corp.example.com. |
| 94 | */ |
| 95 | if(hostname_len == cookie_domain_len) |
| C4: cookie_tailmatch() returns TRUE when hostname_len == cookie_domain_len (exact match) OR when preceding character is '.' (suffix match with subdomain) | |
| 96 | return TRUE; |
| 97 | if('.' == *(hostname + hostname_len - cookie_domain_len - 1)) |
| C4: cookie_tailmatch() returns TRUE for 'attacker.github.io' host with 'github.io' cookie domain (suffix match with preceding '.') | |
| 98 | return TRUE; |
| 99 | return FALSE; |
| 100 | } |
| 800 | static bool is_public_suffix(struct Curl_easy *data, |
| 801 | const struct Cookie *co, |
| 802 | const char *domain) |
| 803 | { |
| 804 | #ifdef USE_LIBPSL |
| 805 | /* |
| 806 | * Check if the domain is a Public Suffix and if yes, ignore the cookie. |
| 807 | * 'domain' is NULL when the cookie is loaded from file or |
| 808 | * CURLOPT_COOKIELIST. |
| 809 | */ |
| 810 | DEBUGASSERT(data); |
| 811 | DEBUGASSERT(co); |
| 812 | DEBUGF(infof(data, "PSL check set-cookie '%s' for domain=%s in %s", |
| 813 | co->name, co->domain ? co->domain : "[blank]", |
| 814 | domain ? domain : "[file]")); |
| 815 | if(!co->domain || Curl_host_is_ipnum(co->domain)) |
| 816 | return FALSE; |
| 817 | |
| 818 | else { |
| 819 | bool acceptable = FALSE; |
| 820 | char lcase[256]; |
| 821 | char lcookie[256]; |
| 822 | size_t dlen = domain ? strlen(domain) : 0; |
| 823 | size_t clen = strlen(co->domain); |
| 824 | |
| 825 | /* trim trailing dots */ |
| 826 | if(dlen && (domain[dlen - 1] == '.')) |
| 827 | dlen--; |
| 828 | if(clen && (co->domain[clen - 1] == '.')) |
| 829 | clen--; |
| 830 | |
| 831 | if((dlen < sizeof(lcase)) && (clen < sizeof(lcookie))) { |
| 832 | const psl_ctx_t *psl = Curl_psl_use(data); |
| 833 | if(psl) { |
| 834 | /* the PSL check requires lowercase domain name and pattern */ |
| 835 | Curl_strntolower(lcookie, co->domain, clen); |
| 836 | lcookie[clen] = 0; |
| 837 | if(domain) { |
| 838 | Curl_strntolower(lcase, domain, dlen); |
| 839 | lcase[dlen] = 0; |
| 840 | acceptable = psl_is_cookie_domain_acceptable(psl, lcase, lcookie); |
| C1: is_public_suffix() calls psl_is_cookie_domain_acceptable() and accepts the cookie when it returns acceptable (returns FALSE), leaving tailmatch=TRUE | |
| C3: is_public_suffix() calls acceptable = psl_is_cookie_domain_acceptable(psl, lcase, lcookie); | |
| 841 | } |
| 842 | else { |
| 843 | /* libpsl says localhost is a PSL, we think not */ |
| 844 | acceptable = |
| 845 | curl_strequal(lcookie, "localhost") || |
| 846 | /* note that this PSL function returns the opposite value than |
| 847 | psl_is_cookie_domain_acceptable() does */ |
| 848 | !psl_is_public_suffix(psl, lcookie); |
| 849 | } |
| 850 | Curl_psl_release(data); |
| 851 | } |
| 852 | else |
| 853 | infof(data, "libpsl problem, rejecting cookie for safety"); |
| 854 | } |
| 855 | |
| 856 | if(!acceptable) { |
| C1: is_public_suffix() returns FALSE (do not reject) when acceptable is TRUE, so the cookie is stored with tailmatch=TRUE | |
| 857 | infof(data, "cookie '%s' dropped, domain '%s' must not " |
| 858 | "set cookies for '%s'", co->name, |
| 859 | domain ? domain : "[file]", co->domain); |
| 860 | return TRUE; |
| 861 | } |
| 862 | } |
| 863 | #else |
| 864 | (void)data; |
| 865 | (void)co; |
| 866 | (void)domain; |
| 867 | DEBUGF(infof(data, "NO PSL to check set-cookie '%s' for domain=%s in %s", |
| 868 | co->name, co->domain, domain ? domain : "[file]")); |
| 869 | #endif |
| 870 | return FALSE; |
| 871 | } |
| 474 | static bool parse_domain(struct Curl_easy *data, struct Cookie *co, |
| 475 | struct Curl_str *cookie_domain, |
| 476 | struct Curl_str *val, |
| 477 | const char **domainp) |
| 478 | { |
| 479 | bool is_ip; |
| 480 | const char *domain = *domainp; |
| 481 | const char *v = curlx_str(val); |
| 482 | /* |
| 483 | * Now, we make sure that our host is within the given domain, or |
| 484 | * the given domain is not valid and thus cannot be set. |
| 485 | */ |
| 486 | |
| 487 | if('.' == *v) |
| 488 | curlx_str_nudge(val, 1); |
| 489 | |
| 490 | #ifndef USE_LIBPSL |
| 491 | /* |
| 492 | * Without PSL we do not know when the incoming cookie is set on a |
| 493 | * TLD or otherwise "protected" suffix. To reduce risk, we require a |
| 494 | * dot OR the exact hostname being "localhost". |
| 495 | */ |
| 496 | if(bad_domain(curlx_str(val), curlx_strlen(val))) { |
| 497 | *domainp = ":"; |
| 498 | domain = ":"; |
| 499 | } |
| 500 | #endif |
| 501 | |
| 502 | is_ip = Curl_host_is_ipnum(domain ? domain : curlx_str(val)); |
| 503 | |
| 504 | if(!domain || |
| 505 | (is_ip && |
| 506 | !strncmp(curlx_str(val), domain, curlx_strlen(val)) && |
| 507 | (curlx_strlen(val) == strlen(domain))) || |
| 508 | (!is_ip && cookie_tailmatch(curlx_str(val), |
| 509 | curlx_strlen(val), domain))) { |
| 510 | *cookie_domain = *val; |
| 511 | if(!is_ip) |
| C1: parse_domain() unconditionally sets co->tailmatch = TRUE for every explicit non-IP Domain attribute | |
| C2: parse_domain() contains: if(!is_ip) co->tailmatch = TRUE; | |
| 512 | co->tailmatch = TRUE; /* we always do that if the domain name was |
| 513 | given */ |
| 514 | } |
| 515 | else { |
| 516 | /* |
| 517 | * We did not get a tailmatch and then the attempted set domain is |
| 518 | * not a domain to which the current host belongs. Mark as bad. |
| 519 | */ |
| 520 | infof(data, "skipped cookie with bad tailmatch domain: %s", |
| 521 | curlx_str(val)); |
| 522 | return FALSE; |
| 523 | } |
| 524 | return TRUE; |
| 525 | } |
Show 7 lines (not cited by the judge)
| 336 | static bool tailmatch(const char *full, size_t flen, |
| 337 | const char *part, size_t plen) |
| 338 | { |
| 339 | if(plen > flen) |
| 340 | return FALSE; |
| 341 | return curl_strnequal(part, &full[flen - plen], plen); |
| 342 | } |
Show 1 lines (not cited by the judge)
| 1049 | #define TRUE true |
Show 109 lines (not cited by the judge)
| 988 | CURLcode Curl_cookie_add(struct Curl_easy *data, |
| 989 | struct CookieInfo *ci, |
| 990 | const char *lineptr, /* first character of the line */ |
| 991 | const char *domain, /* default domain */ |
| 992 | const char *path, /* full path used when this |
| 993 | cookie is set, used to get |
| 994 | default path for the cookie |
| 995 | unless set */ |
| 996 | const int flags) |
| 997 | { |
| 998 | struct Cookie comem; |
| 999 | struct Cookie *co; |
| 1000 | size_t myhash; |
| 1001 | CURLcode result; |
| 1002 | bool replaces = FALSE; |
| 1003 | bool okay; |
| 1004 | |
| 1005 | DEBUGASSERT(data); |
| 1006 | DEBUGASSERT(MAX_SET_COOKIE_AMOUNT <= 255); /* counter is an unsigned char */ |
| 1007 | if(data->req.setcookies >= MAX_SET_COOKIE_AMOUNT) |
| 1008 | return CURLE_OK; /* silently ignore */ |
| 1009 | |
| 1010 | co = &comem; |
| 1011 | memset(co, 0, sizeof(comem)); |
| 1012 | |
| 1013 | if(flags & COOKIE_HTTPHEADER) |
| 1014 | result = parse_cookie_header(data, co, ci, &okay, |
| 1015 | lineptr, domain, path, flags & COOKIE_SECURE); |
| 1016 | else |
| 1017 | result = parse_netscape(co, ci, &okay, lineptr, flags & COOKIE_SECURE); |
| 1018 | |
| 1019 | if(result || !okay) |
| 1020 | goto fail; |
| 1021 | |
| 1022 | if(co->prefix_secure && !co->secure) |
| 1023 | /* The __Secure- prefix only requires that the cookie be set secure */ |
| 1024 | goto fail; |
| 1025 | |
| 1026 | if(co->prefix_host) { |
| 1027 | /* |
| 1028 | * The __Host- prefix requires the cookie to be secure, have a "/" path |
| 1029 | * and not have a domain set. |
| 1030 | */ |
| 1031 | if(co->secure && co->path && !strcmp(co->path, "/") && !co->tailmatch) |
| 1032 | ; |
| 1033 | else |
| 1034 | goto fail; |
| 1035 | } |
| 1036 | |
| 1037 | if(!ci->running && /* read from a file */ |
| 1038 | ci->newsession && /* clean session cookies */ |
| 1039 | !co->expires) /* this is a session cookie */ |
| 1040 | goto fail; |
| 1041 | |
| 1042 | co->livecookie = ci->running; |
| 1043 | co->creationtime = ++ci->lastct; |
| 1044 | |
| 1045 | |
| 1046 | if(!(flags & COOKIE_NOEXPIRE)) |
| 1047 | remove_expired(ci); |
| 1048 | |
| 1049 | if(!(flags & COOKIE_NOPSL) && is_public_suffix(data, co, domain)) |
| 1050 | goto fail; |
| 1051 | |
| 1052 | /* |
| 1053 | * Now we have parsed the incoming line, we must now check if this supersedes |
| 1054 | * an already existing cookie, which it may if the previous have the same |
| 1055 | * domain and path as this. |
| 1056 | */ |
| 1057 | if(!replace_existing(data, co, ci, flags & COOKIE_SECURE, &replaces)) |
| 1058 | goto fail; |
| 1059 | |
| 1060 | /* clone the stack struct into heap */ |
| 1061 | co = curlx_memdup(&comem, sizeof(comem)); |
| 1062 | if(!co) { |
| 1063 | co = &comem; |
| 1064 | result = CURLE_OUT_OF_MEMORY; |
| 1065 | goto fail; /* bail out if we are this low on memory */ |
| 1066 | } |
| 1067 | |
| 1068 | /* add this cookie to the list */ |
| 1069 | myhash = cookiehash(co->domain); |
| 1070 | Curl_llist_append(&ci->cookielist[myhash], co, &co->node); |
| 1071 | |
| 1072 | if(ci->running) |
| 1073 | /* Only show this when NOT reading the cookies from a file */ |
| 1074 | infof(data, "%s cookie %s=\"%s\" for domain %s, path %s, " |
| 1075 | "expire %" FMT_OFF_T, |
| 1076 | replaces ? "Replaced" : "Added", co->name, co->value, |
| 1077 | co->domain, co->path, co->expires); |
| 1078 | |
| 1079 | if(!replaces) |
| 1080 | ci->numcookies++; /* one more cookie in the jar */ |
| 1081 | |
| 1082 | /* |
| 1083 | * Now that we have added a new cookie to the jar, update the expiration |
| 1084 | * tracker in case it is the next one to expire. |
| 1085 | */ |
| 1086 | if(co->expires && (co->expires < ci->next_expiration)) |
| 1087 | ci->next_expiration = co->expires; |
| 1088 | |
| 1089 | if(flags & COOKIE_HTTPHEADER) |
| 1090 | data->req.setcookies++; |
| 1091 | |
| 1092 | return result; |
| 1093 | fail: |
| 1094 | freecookie(co, FALSE); |
| 1095 | return result; |
| 1096 | } |
Show 111 lines (not cited by the judge)
| 974 | CURLcode Curl_cookie_add( |
| 975 | struct Curl_easy *data, |
| 976 | struct CookieInfo *ci, |
| 977 | bool httpheader, /* TRUE if HTTP header-style line */ |
| 978 | bool noexpire, /* if TRUE, skip remove_expired() */ |
| 979 | const char *lineptr, /* first character of the line */ |
| 980 | const char *domain, /* default domain */ |
| 981 | const char *path, /* full path used when this cookie is set, used |
| 982 | to get default path for the cookie unless set */ |
| 983 | bool secure) /* TRUE if connection is over secure origin */ |
| 984 | { |
| 985 | struct Cookie comem; |
| 986 | struct Cookie *co; |
| 987 | size_t myhash; |
| 988 | CURLcode result; |
| 989 | bool replaces = FALSE; |
| 990 | bool okay; |
| 991 | |
| 992 | DEBUGASSERT(data); |
| 993 | DEBUGASSERT(MAX_SET_COOKIE_AMOUNT <= 255); /* counter is an unsigned char */ |
| 994 | if(data->req.setcookies >= MAX_SET_COOKIE_AMOUNT) |
| 995 | return CURLE_OK; /* silently ignore */ |
| 996 | |
| 997 | co = &comem; |
| 998 | memset(co, 0, sizeof(comem)); |
| 999 | |
| 1000 | if(httpheader) |
| 1001 | result = parse_cookie_header(data, co, ci, &okay, |
| 1002 | lineptr, domain, path, secure); |
| 1003 | else |
| 1004 | result = parse_netscape(co, ci, &okay, lineptr, secure); |
| 1005 | |
| 1006 | if(result || !okay) |
| 1007 | goto fail; |
| 1008 | |
| 1009 | if(co->prefix_secure && !co->secure) |
| 1010 | /* The __Secure- prefix only requires that the cookie be set secure */ |
| 1011 | goto fail; |
| 1012 | |
| 1013 | if(co->prefix_host) { |
| 1014 | /* |
| 1015 | * The __Host- prefix requires the cookie to be secure, have a "/" path |
| 1016 | * and not have a domain set. |
| 1017 | */ |
| 1018 | if(co->secure && co->path && !strcmp(co->path, "/") && !co->tailmatch) |
| 1019 | ; |
| 1020 | else |
| 1021 | goto fail; |
| 1022 | } |
| 1023 | |
| 1024 | if(!ci->running && /* read from a file */ |
| 1025 | ci->newsession && /* clean session cookies */ |
| 1026 | !co->expires) /* this is a session cookie */ |
| 1027 | goto fail; |
| 1028 | |
| 1029 | co->livecookie = ci->running; |
| 1030 | co->creationtime = ++ci->lastct; |
| 1031 | |
| 1032 | /* |
| 1033 | * Now we have parsed the incoming line, we must now check if this supersedes |
| 1034 | * an already existing cookie, which it may if the previous have the same |
| 1035 | * domain and path as this. |
| 1036 | */ |
| 1037 | |
| 1038 | /* remove expired cookies */ |
| 1039 | if(!noexpire) |
| 1040 | remove_expired(ci); |
| 1041 | |
| 1042 | if(is_public_suffix(data, co, domain)) |
| 1043 | goto fail; |
| 1044 | |
| 1045 | if(!replace_existing(data, co, ci, secure, &replaces)) |
| 1046 | goto fail; |
| 1047 | |
| 1048 | /* clone the stack struct into heap */ |
| 1049 | co = curlx_memdup(&comem, sizeof(comem)); |
| 1050 | if(!co) { |
| 1051 | co = &comem; |
| 1052 | result = CURLE_OUT_OF_MEMORY; |
| 1053 | goto fail; /* bail out if we are this low on memory */ |
| 1054 | } |
| 1055 | |
| 1056 | /* add this cookie to the list */ |
| 1057 | myhash = cookiehash(co->domain); |
| 1058 | Curl_llist_append(&ci->cookielist[myhash], co, &co->node); |
| 1059 | |
| 1060 | if(ci->running) |
| 1061 | /* Only show this when NOT reading the cookies from a file */ |
| 1062 | infof(data, "%s cookie %s=\"%s\" for domain %s, path %s, " |
| 1063 | "expire %" FMT_OFF_T, |
| 1064 | replaces ? "Replaced" : "Added", co->name, co->value, |
| 1065 | co->domain, co->path, co->expires); |
| 1066 | |
| 1067 | if(!replaces) |
| 1068 | ci->numcookies++; /* one more cookie in the jar */ |
| 1069 | |
| 1070 | /* |
| 1071 | * Now that we have added a new cookie to the jar, update the expiration |
| 1072 | * tracker in case it is the next one to expire. |
| 1073 | */ |
| 1074 | if(co->expires && (co->expires < ci->next_expiration)) |
| 1075 | ci->next_expiration = co->expires; |
| 1076 | |
| 1077 | if(httpheader) |
| 1078 | data->req.setcookies++; |
| 1079 | |
| 1080 | return result; |
| 1081 | fail: |
| 1082 | freecookie(co, FALSE); |
| 1083 | return result; |
| 1084 | } |
Show 9 lines (not cited by the judge)
| 76 | int curl_strequal(const char *s1, const char *s2) |
| 77 | { |
| 78 | if(s1 && s2) |
| 79 | /* both pointers point to something then compare them */ |
| 80 | return casecompare(s1, s2); |
| 81 | |
| 82 | /* if both pointers are NULL then treat them as equal */ |
| 83 | return NULL == s1 && NULL == s2; |
| 84 | } |
Show 55 lines (not cited by the judge)
| 803 | static bool is_public_suffix(struct Curl_easy *data, |
| 804 | const struct Cookie *co, |
| 805 | const char *domain) |
| 806 | { |
| 807 | #ifdef USE_LIBPSL |
| 808 | /* |
| 809 | * Check if the domain is a Public Suffix and if yes, ignore the cookie. We |
| 810 | * must also check that the data handle is not NULL since the psl code will |
| 811 | * dereference it. |
| 812 | */ |
| 813 | DEBUGF(infof(data, "PSL check set-cookie '%s' for domain=%s in %s", |
| 814 | co->name, co->domain, domain)); |
| 815 | if(data && (domain && co->domain && !Curl_host_is_ipnum(co->domain))) { |
| 816 | bool acceptable = FALSE; |
| 817 | char lcase[256]; |
| 818 | char lcookie[256]; |
| 819 | size_t dlen = strlen(domain); |
| 820 | size_t clen = strlen(co->domain); |
| 821 | |
| 822 | /* trim trailing dots */ |
| 823 | if(dlen && (domain[dlen - 1] == '.')) |
| 824 | dlen--; |
| 825 | if(clen && (co->domain[clen - 1] == '.')) |
| 826 | clen--; |
| 827 | |
| 828 | if((dlen < sizeof(lcase)) && (clen < sizeof(lcookie))) { |
| 829 | const psl_ctx_t *psl = Curl_psl_use(data); |
| 830 | if(psl) { |
| 831 | /* the PSL check requires lowercase domain name and pattern */ |
| 832 | Curl_strntolower(lcase, domain, dlen); |
| 833 | lcase[dlen] = 0; |
| 834 | Curl_strntolower(lcookie, co->domain, clen); |
| 835 | lcookie[clen] = 0; |
| 836 | acceptable = psl_is_cookie_domain_acceptable(psl, lcase, lcookie); |
| 837 | Curl_psl_release(data); |
| 838 | } |
| 839 | else |
| 840 | infof(data, "libpsl problem, rejecting cookie for safety"); |
| 841 | } |
| 842 | |
| 843 | if(!acceptable) { |
| 844 | infof(data, "cookie '%s' dropped, domain '%s' must not " |
| 845 | "set cookies for '%s'", co->name, domain, co->domain); |
| 846 | return TRUE; |
| 847 | } |
| 848 | } |
| 849 | #else |
| 850 | (void)data; |
| 851 | (void)co; |
| 852 | (void)domain; |
| 853 | DEBUGF(infof(data, "NO PSL to check set-cookie '%s' for domain=%s in %s", |
| 854 | co->name, co->domain, domain)); |
| 855 | #endif |
| 856 | return FALSE; |
| 857 | } |