CVE-2023-46219: HSTS long file name clears contents
What the report claims
cURL's HSTS database can be overwritten when a filename exceeds 243 bytes, causing loss of stored HSTS entries.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2023-46219 HSTS long filename clears contents
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.
- Curl_fopen truncates the original file (line 59) before verifying the temporary filename can be created (line 78).
- If the temporary filename exceeds the filesystem limit (e.g., >255 bytes on ext4), open() fails but the original file is already zeroed.
- The HSTS database file is user-controlled via --hsts, so a user (or attacker controlling command line) can trigger this data loss.
- This is not listed in curl's 'Not security issues' policy sections; it causes silent data loss of security-relevant HSTS entries.
How this analysis ran 5 claims · 14 citations verified
Evidence check: judge quoted an external source that does not say that (https://docs.rs/rsext4/latest/src/rsext4/entries/dir_entry.rs.html)
Checked against curl master on 2023-11-02 (ddb95c155e18). Test 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 reproduction. The flaw is confirmed: Curl_fopen in lib/fopen.c opens the original file for writing (truncating it) at line 59 before creating the temporary file at line 71. If the temp filename exceeds the filesystem limit (e.g., 255 bytes on ext4), the open() at line 78 fails with ENAMETOOLONG, but the original HSTS file has already been emptied. This causes silent loss of all HSTS entries. We will fix this by creating the temporary file first, or by checking the filename length upfront.
Inspect all claims and source code 5 claims · 4 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
cURL creates temporary files with names potentially exceeding the filesystem's maximum filename length, leading to HSTS database overwrite when the name is longer than 243 bytes.
Read the reasoning
The code opens the original file for writing (truncating it) at line 59 before attempting to create the temporary file at line 71. If the temporary filename exceeds the filesystem's maximum name length (typically 255 bytes), the open() at line 78 fails, but the original file has already been truncated to zero bytes.
The flaw occurs in the HSTS database handling code that generates temporary filenames using a random suffix.
Read the reasoning
The temporary filename generation in Curl_fopen (lib/fopen.c) uses a 9-byte buffer for the random suffix (8 chars + NUL) and appends '.tmp' (4 chars) plus a dot separator, adding 13 bytes to the original filename.
An attacker (or user) provides a filename longer than 243 bytes for the HSTS database, causing the temporary filename to exceed the filesystem limit.
Read the reasoning
The filename comes from the caller (HSTS code in lib/hsts.c, which gets it from the user via --hsts). If the user supplies a filename >242 bytes (255 - 13), the temp name exceeds 255 bytes and open() fails with ENAMETOOLONG.
The HSTS database file is overwritten with an empty file, resulting in loss of all stored HSTS entries.
Read the reasoning
When temp file creation fails, Curl_fopen returns an error (line 109) but the original file was already truncated at line 59 and closed at line 64. The caller (hsts.c) receives the error and does not rename anything, leaving the original file as a zero-byte file.
Create an HSTS file with a name longer than 243 bytes, then use curl --hsts <thatfile> to fetch a URL; the database will be cleared.
Read the reasoning
The reproduction steps match the code path: user provides long filename via --hsts, HSTS code calls Curl_fopen, which truncates the file then fails to create temp file due to name length, leaving the database empty.
What the report says about other projects
confirmed The maximum filename length on ext4 is typically 255 bytes.
“Maximum filename length in bytes (keeps total paths well under the 255-byte component limit common to ext4/APFS/NTFS).” source
Read the original report (excerpt)
Source code
| 49 | CURLcode Curl_fopen(struct Curl_easy *data, const char *filename, |
| C3: Curl_fopen takes filename from caller | |
| 50 | FILE **fh, char **tempname) |
| 51 | { |
| 52 | CURLcode result = CURLE_WRITE_ERROR; |
| 53 | unsigned char randsuffix[9]; |
| C2: randsuffix[9] buffer for 8 alphanumeric chars + NUL | |
| 54 | char *tempstore = NULL; |
| 55 | struct_stat sb; |
| 56 | int fd = -1; |
| 57 | *tempname = NULL; |
| 58 | |
| 59 | *fh = fopen(filename, FOPEN_WRITETEXT); |
| C1: Opens original file for writing (FOPEN_WRITETEXT), which truncates it to zero length | |
| C4: fopen(filename, FOPEN_WRITETEXT) truncates original file | |
| C5: Truncation happens on any Curl_fopen call with a regular file | |
| 60 | if(!*fh) |
| 61 | goto fail; |
| 62 | if(fstat(fileno(*fh), &sb) == -1 || !S_ISREG(sb.st_mode)) |
| 63 | return CURLE_OK; |
| 64 | fclose(*fh); |
| C4: fclose(*fh) after truncation | |
| 65 | *fh = NULL; |
| 66 | |
| 67 | result = Curl_rand_alnum(data, randsuffix, sizeof(randsuffix)); |
| C2: Curl_rand_alnum fills randsuffix | |
| 68 | if(result) |
| 69 | goto fail; |
| 70 | |
| 71 | tempstore = aprintf("%s.%s.tmp", filename, randsuffix); |
| C1: Creates temp filename: filename + '.' + 8-char randsuffix + '.tmp' (13 extra chars) | |
| C2: aprintf formats temp name as "%s.%s.tmp" | |
| C3: Temp name length = strlen(filename) + 13 | |
| 72 | if(!tempstore) { |
| 73 | result = CURLE_OUT_OF_MEMORY; |
| 74 | goto fail; |
| 75 | } |
| 76 | |
| 77 | result = CURLE_WRITE_ERROR; |
| 78 | fd = open(tempstore, O_WRONLY | O_CREAT | O_EXCL, 0600); |
| C1: open() on tempstore fails with ENAMETOOLONG if total length exceeds filesystem limit (e.g., 255 bytes on ext4) | |
| C3: open() fails if temp name exceeds filesystem limit | |
| C5: Failure triggered by long filename | |
| 79 | if(fd == -1) |
| 80 | goto fail; |
| 81 | |
| 82 | #ifdef HAVE_FCHMOD |
| 83 | { |
| 84 | struct_stat nsb; |
| 85 | if((fstat(fd, &nsb) != -1) && |
| 86 | (nsb.st_uid == sb.st_uid) && (nsb.st_gid == sb.st_gid)) { |
| 87 | /* if the user and group are the same, clone the original mode */ |
| 88 | if(fchmod(fd, (mode_t)sb.st_mode) == -1) |
| 89 | goto fail; |
| 90 | } |
| 91 | } |
| 92 | #endif |
| 93 | |
| 94 | *fh = fdopen(fd, FOPEN_WRITETEXT); |
| 95 | if(!*fh) |
| 96 | goto fail; |
| 97 | |
| 98 | *tempname = tempstore; |
| 99 | return CURLE_OK; |
| 100 | |
| 101 | fail: |
| C4: fail path returns error without restoring original file | |
| 102 | if(fd != -1) { |
| 103 | close(fd); |
| 104 | unlink(tempstore); |
| 105 | } |
| 106 | |
| 107 | free(tempstore); |
| 108 | |
| 109 | return result; |
| 110 | } |
| 390 | # define fopen(fname,mode) curlx_win32_fopen(fname, mode) |
Show 31 lines (not cited by the judge)
| 41 | int Curl_rename(const char *oldpath, const char *newpath) |
| 42 | { |
| 43 | #ifdef WIN32 |
| 44 | /* rename() on Windows doesn't overwrite, so we can't use it here. |
| 45 | MoveFileEx() will overwrite and is usually atomic, however it fails |
| 46 | when there are open handles to the file. */ |
| 47 | const int max_wait_ms = 1000; |
| 48 | struct curltime start = Curl_now(); |
| 49 | TCHAR *tchar_oldpath = curlx_convert_UTF8_to_tchar((char *)oldpath); |
| 50 | TCHAR *tchar_newpath = curlx_convert_UTF8_to_tchar((char *)newpath); |
| 51 | for(;;) { |
| 52 | timediff_t diff; |
| 53 | if(MoveFileEx(tchar_oldpath, tchar_newpath, MOVEFILE_REPLACE_EXISTING)) { |
| 54 | curlx_unicodefree(tchar_oldpath); |
| 55 | curlx_unicodefree(tchar_newpath); |
| 56 | break; |
| 57 | } |
| 58 | diff = Curl_timediff(Curl_now(), start); |
| 59 | if(diff < 0 || diff > max_wait_ms) { |
| 60 | curlx_unicodefree(tchar_oldpath); |
| 61 | curlx_unicodefree(tchar_newpath); |
| 62 | return 1; |
| 63 | } |
| 64 | Sleep(1); |
| 65 | } |
| 66 | #else |
| 67 | if(rename(oldpath, newpath)) |
| 68 | return 1; |
| 69 | #endif |
| 70 | return 0; |
| 71 | } |
Show 1 lines (not cited by the judge)
| 49 | # define aprintf curl_maprintf |