HexBrain

curl · Report #3970639

--etag-save - truncates append-redirected stdout

Informativestatus
lowseverity
4upvotes
28 Aug 2026disclosed

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>
  1. Write PRESERVE\n to a log file
  2. Run curl --etag-save - http://server/ >> logfile where server returns ETag: "remote"
  3. Log file now contains only "remote"\n — PRESERVE is 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

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original