CVE-2022-27778: curl removes wrong file on error
What the report claims
Curl removes a wrong file when using --no-clobber and --remove-on-error together, causing unintended data loss.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2022-27778 curl removes wrong file on error
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?
- tool_operate.c:598-600 unlinks per->outfile (original name) on error with --remove-on-error
- tool_cb_wrt.c:88 (per deterministic checks) generates alternative name for --no-clobber but does not update per->outfile
- Result: original file deleted (violates --no-clobber), partial file remains (violates --remove-on-error)
- CVE-2022-27778 assigned, confirming security relevance
How this analysis ran 5 claims · 7 citations verified
Checked against curl permalink 3fd1d8df3a24 (3fd1d8df3a24). 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: tool_operate.c:598-600 removes per->outfile (the original filename) on error, but --no-clobber writes to a generated name (e.g., foo.1) without updating per->outfile. This deletes the protected file and leaves the partial file. We'll fix the remove-on-error logic to unlink the actual written file.
Inspect all claims and source code 5 claims · 1 code excerpts
Report claims
curl removes the wrong file when combining --no-clobber and --remove-on-error if the target file name exists and an error occurs
Read the reasoning
The remove-on-error logic at lines 598-600 unlinks per->outfile, which is the original user-specified output filename. The no-clobber logic (tool_cb_wrt.c line 88 per deterministic checks) generates an alternative filename when the target exists but does not update per->outfile, so the wrong file is removed.
the bug occurs in the remove-on-error unlink logic
Read the reasoning
The bug is in the remove-on-error unlink logic at lines 598-600 of tool_operate.c, which uses per->outfile (the original name) instead of the actual file written to when no-clobber renamed it.
when a file with the same name already exists and an error happens during transfer
Read the reasoning
The trigger is an error during transfer (result != CURLE_OK) combined with --remove-on-error (config->rm_partial). The no-clobber condition (file exists) is handled in tool_cb_wrt.c line 88 per deterministic checks.
removal of a file that was supposed not to be overwritten, leading to data loss
Read the reasoning
The original file (foo) is deleted despite --no-clobber, and the partial file (foo.1) is left behind despite --remove-on-error, causing both data loss and incomplete file retention.
steps to reproduce involve creating a file, starting a server, and running curl with --no-clobber --remove-on-error
Read the reasoning
Reproduction steps are concrete and match the code path: create existing file, start server, run curl with --no-clobber --remove-on-error -m 3 to trigger timeout/error, observe wrong file removed.
Deterministic checks that failed
- line none of unlink appear near line 88 at 3fd1d8df3a24; it reads: 'if(config->file_clobber_mode == CLOBBER_NEVER && fd == -1) {'
Read the original report
Source code
| 573 | } |
| 574 | outs->bytes = 0; /* clear for next round */ |
| 575 | } |
| 576 | *retryp = TRUE; |
| 577 | *delay = sleeptime; |
| 578 | return CURLE_OK; |
| 579 | } |
| 580 | } /* if retry_numretries */ |
| 581 | noretry: |
| 582 | |
| 583 | if((global->progressmode == CURL_PROGRESS_BAR) && |
| 584 | per->progressbar.calls) |
| 585 | /* if the custom progress bar has been displayed, we output a |
| 586 | newline here */ |
| 587 | fputs("\n", per->progressbar.out); |
| 588 | |
| 589 | /* Close the outs file */ |
| 590 | if(outs->fopened && outs->stream) { |
| 591 | int rc = fclose(outs->stream); |
| 592 | if(!result && rc) { |
| 593 | /* something went wrong in the writing process */ |
| 594 | result = CURLE_WRITE_ERROR; |
| 595 | if(global->showerror) |
| 596 | fprintf(global->errors, "curl: (%d) Failed writing body\n", result); |
| 597 | } |
| 598 | if(result && config->rm_partial) { |
| C1: unlink(per->outfile) called on error with rm_partial set | |
| C2: unlink(per->outfile) in remove-on-error block | |
| C3: condition: result && config->rm_partial (error + remove-on-error) | |
| C4: unlink removes per->outfile (original file protected by --no-clobber) | |
| 599 | notef(global, "Removing output file: %s", per->outfile); |
| C1: logs 'Removing output file: %s' using per->outfile | |
| C2: uses per->outfile for the filename to remove | |
| 600 | unlink(per->outfile); |
| C4: partial file (e.g., foo.1) remains on disk | |
| 601 | } |
| 602 | } |
| 603 | |
| 604 | /* File time can only be set _after_ the file has been closed */ |
| 605 | if(!result && config->remote_time && outs->s_isreg && outs->filename) { |
| 606 | /* Ask libcurl if we got a remote file time */ |
| 607 | curl_off_t filetime = -1; |
| 608 | curl_easy_getinfo(curl, CURLINFO_FILETIME_T, &filetime); |
| 609 | setfiletime(filetime, outs->filename, global); |
| 610 | } |
| 611 | |
| 612 | /* Write the --write-out data before cleanup but after result is final */ |
| 613 | if(config->writeout) |
| 614 | ourWriteOut(config->writeout, per, result); |
| 615 | |
| 616 | /* Close function-local opened file descriptors */ |
| 617 | if(per->heads.fopened && per->heads.stream) |
| 618 | fclose(per->heads.stream); |
| 619 | |
| 620 | if(per->heads.alloc_filename) |
| 621 | Curl_safefree(per->heads.filename); |
| 622 | |
| 623 | if(per->etag_save.fopened && per->etag_save.stream) |