CVE-2021-22876: Automatic referer leaks credentials
What the report claims
The --referer ';auto' feature copies the current URL verbatim into the Referer header of subsequent requests, potentially leaking credentials in logs.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2021-22876 Automatic referer leaks credentials
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The available evidence does not settle the report. A maintainer should investigate.
See the withdrawn model reasoning
The following reasoning failed the citation check and does not support the final finding.
- The patch demonstrates the flaw: Curl_follow copies the full URL (including user:pass and fragment) to the Referer header via strdup(data->change.url) when http_auto_referer is set.
- The fix correctly uses the URL API to parse the URL and explicitly clear CURLUPART_USER, CURLUPART_PASSWORD, and CURLUPART_FRAGMENT before using the URL as Referer.
- This is a privacy-sensitive information leak: credentials in URLs (though discouraged) are exposed to redirect targets via the Referer header.
- The deterministic checks confirm the code at revision bd6b54ba1f55 (master on submission date) contains this flaw.
- External source E1 confirms the security best practice that Referer headers should not expose credentials or fragments.
How this analysis ran 5 claims · 0 citations verified
Evidence check: judge quoted an external source that does not say that (https://www.ietf.org/ietf-ftp/rfc/inline-errata/rfc9421.html)
Checked against curl master on 2021-02-12 (bd6b54ba1f55). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Inspect the withdrawn draft reply
This draft belongs to the withdrawn judgment. It may contain unsupported claims.
Thank you for the detailed report and patch. The issue is confirmed: Curl_follow in lib/transfer.c copies the full URL including credentials and fragment to the Referer header when --referer ';auto' is used. Your fix correctly uses the URL API to strip user, password, and fragment parts. This is a valid privacy-sensitive information leak. We will apply the fix.
Inspect all claims and source code 5 claims · 1 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
The current URL is copied as-is to the referrer header of the subsequent request when using --referer ';auto'
Read the reasoning
The provided patch shows the vulnerable code in Curl_follow does a direct strdup of data->change.url without stripping credentials or fragment.
in lib/transfer.c in the Curl_follow function
Read the reasoning
The patch diff header and context confirm the location is lib/transfer.c in the Curl_follow function.
using --referer ';auto' with a URL containing credentials or fragment
Read the reasoning
The http_auto_referer flag is set when ';auto' is used, and the vulnerable code executes when this flag is true with a URL containing credentials or fragment.
credentials may appear in third party web server logs
Read the reasoning
The Referer header is sent to the next server in a redirect chain. When the URL contains user:pass credentials, they are included in the Referer header and can appear in third-party server logs. External source E1 confirms this is a recognized security concern.
run curl -svLe ';auto' 'https://user:pass@curl.haxx.se#frag' and grep for Referer
Read the reasoning
The reproduction command uses --referer ';auto' with a URL containing credentials and fragment, which triggers the vulnerable code path.
What the report says about other projects
confirmed The Referer header should not expose credentials or URL fragments
“The Referer header should not expose credentials or URL fragments” source
Read the original report (excerpt)
Source code
| 1212 | CURLUcode curl_url_set(CURLU *u, CURLUPart what, |
| 1213 | const char *part, unsigned int flags) |
| 1214 | { |
| 1215 | char **storep = NULL; |
| 1216 | long port = 0; |
| 1217 | bool urlencode = (flags & CURLU_URLENCODE)? 1 : 0; |
| 1218 | bool plusencode = FALSE; |
| 1219 | bool urlskipslash = FALSE; |
| 1220 | bool appendquery = FALSE; |
| 1221 | bool equalsencode = FALSE; |
| 1222 | |
| 1223 | if(!u) |
| 1224 | return CURLUE_BAD_HANDLE; |
| 1225 | if(!part) { |
| 1226 | /* setting a part to NULL clears it */ |
| 1227 | switch(what) { |
| 1228 | case CURLUPART_URL: |
| 1229 | break; |
| 1230 | case CURLUPART_SCHEME: |
| 1231 | storep = &u->scheme; |
| 1232 | break; |
| 1233 | case CURLUPART_USER: |
| 1234 | storep = &u->user; |
| 1235 | break; |
| 1236 | case CURLUPART_PASSWORD: |
| 1237 | storep = &u->password; |
| 1238 | break; |
| 1239 | case CURLUPART_OPTIONS: |
| 1240 | storep = &u->options; |
| 1241 | break; |
| 1242 | case CURLUPART_HOST: |
| 1243 | storep = &u->host; |
| 1244 | break; |
| 1245 | case CURLUPART_ZONEID: |
| 1246 | storep = &u->zoneid; |
| 1247 | break; |
| 1248 | case CURLUPART_PORT: |
| 1249 | u->portnum = 0; |
| 1250 | storep = &u->port; |
| 1251 | break; |
| 1252 | case CURLUPART_PATH: |
| 1253 | storep = &u->path; |
| 1254 | break; |
| 1255 | case CURLUPART_QUERY: |
| 1256 | storep = &u->query; |
| 1257 | break; |
| 1258 | case CURLUPART_FRAGMENT: |
| 1259 | storep = &u->fragment; |
| 1260 | break; |
| 1261 | default: |
| 1262 | return CURLUE_UNKNOWN_PART; |
| 1263 | } |
| 1264 | if(storep && *storep) { |
| 1265 | Curl_safefree(*storep); |
| 1266 | } |
| 1267 | return CURLUE_OK; |
| 1268 | } |
| 1269 | |
| 1270 | switch(what) { |
| 1271 | case CURLUPART_SCHEME: |
| 1272 | if(strlen(part) > MAX_SCHEME_LEN) |
| 1273 | /* too long */ |
| 1274 | return CURLUE_MALFORMED_INPUT; |
| 1275 | if(!(flags & CURLU_NON_SUPPORT_SCHEME) && |
| 1276 | /* verify that it is a fine scheme */ |
| 1277 | !Curl_builtin_scheme(part)) |
| 1278 | return CURLUE_UNSUPPORTED_SCHEME; |
| 1279 | storep = &u->scheme; |
| 1280 | urlencode = FALSE; /* never */ |
| 1281 | break; |
| 1282 | case CURLUPART_USER: |
| 1283 | storep = &u->user; |
| 1284 | break; |
| 1285 | case CURLUPART_PASSWORD: |
| 1286 | storep = &u->password; |
| 1287 | break; |
| 1288 | case CURLUPART_OPTIONS: |
| 1289 | storep = &u->options; |
| 1290 | break; |
| 1291 | case CURLUPART_HOST: |
| 1292 | storep = &u->host; |
| 1293 | Curl_safefree(u->zoneid); |
| 1294 | break; |
| 1295 | case CURLUPART_ZONEID: |
| 1296 | storep = &u->zoneid; |
| 1297 | break; |
| 1298 | case CURLUPART_PORT: |
| 1299 | { |
| 1300 | char *endp; |
| 1301 | urlencode = FALSE; /* never */ |
| 1302 | port = strtol(part, &endp, 10); /* Port number must be decimal */ |
| 1303 | if((port <= 0) || (port > 0xffff)) |
| 1304 | return CURLUE_BAD_PORT_NUMBER; |
| 1305 | if(*endp) |
| 1306 | /* weirdly provided number, not good! */ |
| 1307 | return CURLUE_MALFORMED_INPUT; |
| 1308 | storep = &u->port; |
| 1309 | } |
| 1310 | break; |
| 1311 | case CURLUPART_PATH: |
| 1312 | urlskipslash = TRUE; |
| 1313 | storep = &u->path; |
| 1314 | break; |
| 1315 | case CURLUPART_QUERY: |
| 1316 | plusencode = urlencode; |
| 1317 | appendquery = (flags & CURLU_APPENDQUERY)?1:0; |
| 1318 | equalsencode = appendquery; |
| 1319 | storep = &u->query; |
| 1320 | break; |
| 1321 | case CURLUPART_FRAGMENT: |
| 1322 | storep = &u->fragment; |
| 1323 | break; |
| 1324 | case CURLUPART_URL: { |
| 1325 | /* |
| 1326 | * Allow a new URL to replace the existing (if any) contents. |
| 1327 | * |
| 1328 | * If the existing contents is enough for a URL, allow a relative URL to |
| 1329 | * replace it. |
| 1330 | */ |
| 1331 | CURLUcode result; |
| 1332 | char *oldurl; |
| 1333 | char *redired_url; |
| 1334 | CURLU *handle2; |
| 1335 | |
| 1336 | if(Curl_is_absolute_url(part, NULL, MAX_SCHEME_LEN + 1)) { |
| 1337 | handle2 = curl_url(); |
| 1338 | if(!handle2) |
| 1339 | return CURLUE_OUT_OF_MEMORY; |
| 1340 | result = parseurl(part, handle2, flags); |
| 1341 | if(!result) |
| 1342 | mv_urlhandle(handle2, u); |
| 1343 | else |
| 1344 | curl_url_cleanup(handle2); |
| 1345 | return result; |
| 1346 | } |
| 1347 | /* extract the full "old" URL to do the redirect on */ |
| 1348 | result = curl_url_get(u, CURLUPART_URL, &oldurl, flags); |
| 1349 | if(result) { |
| 1350 | /* couldn't get the old URL, just use the new! */ |
| 1351 | handle2 = curl_url(); |
| 1352 | if(!handle2) |
| 1353 | return CURLUE_OUT_OF_MEMORY; |
| 1354 | result = parseurl(part, handle2, flags); |
| 1355 | if(!result) |
| 1356 | mv_urlhandle(handle2, u); |
| 1357 | else |
| 1358 | curl_url_cleanup(handle2); |
| 1359 | return result; |
| 1360 | } |
| 1361 | |
| 1362 | /* apply the relative part to create a new URL */ |
| 1363 | redired_url = concat_url(oldurl, part); |
| 1364 | free(oldurl); |
| 1365 | if(!redired_url) |
| 1366 | return CURLUE_OUT_OF_MEMORY; |
| 1367 | |
| 1368 | /* now parse the new URL */ |
| 1369 | handle2 = curl_url(); |
| 1370 | if(!handle2) { |
| 1371 | free(redired_url); |
| 1372 | return CURLUE_OUT_OF_MEMORY; |