curl · Report #3970639
--etag-save - truncates append-redirected stdout
How it was found
Summary
save_etag() (line 267–273) calls fileno(), fstat(), ftruncate(fd, 0) whenever the underlying fd is a regular file. It does not check OutStruct.regular_file, which is FALSE for stdout even when shell-redirected to a file. When --etag-save - is used with append redirection (>>), an HTTP 2xx/3xx response containing an ETag header causes ftruncate to destroy all pre-existing content in the append target before writing the ETag value. The peer controls the trigger via the response status code and ETag header.
Reproduction
python3 poc_etag_truncate.py <sha>
- Write
PRESERVE\nto a log file - Run
curl --etag-save - http://server/ >> logfilewhere server returnsETag: "remote" - Log file now contains only
"remote"\n—PRESERVEis gone
Control: Same setup but server returns no ETag header → PRESERVE retained.
Root cause
// src/tool_cb_hdr.c:267-273
int fd = fileno(etag_save->stream);
if((fd != -1) &&
!curlx_fstat(fd, &file) &&
(S_ISREG(file.st_mode) && // ← only checks physical file type
toolx_ftruncate(fd, 0))) // ← truncates regardless of ownership
S_ISREG alone cannot distinguish a curl-owned ETag output file from stdout redirected to a regular file.
Remediation
Guard ftruncate with etag_save->regular_file (the semantic stream flag) in addition to S_ISREG.
Impact
A remote server can destroy arbitrary pre-existing content in any regular file the user's shell has opened for append on stdout. The attacker controls the trigger (any 2xx/3xx response with an ETag header) and the replacement content (the ETag value). The user must have chosen --etag-save - and append redirection, but the target file, its prior contents, and the decision to append are entirely the user's — curl silently violates the append contract. In scripting contexts where multiple curl invocations append ETags to a shared log, a single malicious server response wipes the entire log history.v