HexBrain

curl · Report #4130124

server filename with control characters printed to terminal (`-J`)

N/Astatus
lowseverity
2upvotes
26 Oct 2026disclosed

How it was found

Summary:

With -OJ, curl takes the filename from the server's Content-Disposition header. parse_filename() in src/tool_cb_hdr.c does not remove control characters such as ESC (0x1b). When a warning prints that name, the escape sequences reach the terminal.

Steps to reproduce

  1. Build curl from the source tree (cmake + make -j8 curl). The binary is build/src/curl.

  2. Save as srv.py and run python3 -I srv.py:

    import http.server
    
    class H(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.send_header('Content-Disposition',
                'attachment; filename="ok\x1b[2J\x1b[31mPWNED.txt"'
                .encode('latin-1', 'replace').decode('latin-1'))
            self.send_header('Content-Length', '4')
            self.end_headers()
            self.wfile.write(b'data')
    
        def log_message(self, *a):
            pass
    
    http.server.HTTPServer(('127.0.0.1', 8765), H).serve_forever()
    
  3. First download creates the file:

    mkdir t && cd t
    /path/to/build/src/curl -OJ http://127.0.0.1:8765/
    
  4. Second download triggers the warning:

    /path/to/build/src/curl -OJ http://127.0.0.1:8765/ 2>&1 | cat -v
    

    Expected output includes:

    Warning: Failed to open the file ok^[[2J^[[31mPWNED.txt: File exists
    

    ^[ is the raw ESC byte.

Root cause

  1. parse_filename() does not remove control characters. In src/tool_cb_hdr.c (about lines 150–213), the function takes the name from Content-Disposition and:

    • strips quotes and anything after the closing quote,
    • keeps only the part after the last / or \ (this blocks path traversal),
    • removes \r and \n.

    It does not remove other control characters, so ESC (0x1b), other bytes below 0x20, and 0x7f pass through. The Windows-only sanitizer (sanitize_file_name) would catch them, but it runs only under _WIN32 or MSDOS, so on Linux and macOS the name is used as-is.

  2. voutf() writes its output without escaping. In src/tool_msgs.c, voutf() sends the formatted message to stderr with fputs and does not check for non-printable bytes. When the warning Failed to open the file %s includes the server's name, the escape sequences reach the terminal.

The warning comes from tool_create_output_file() in src/tool_cb_wrt.c, which opens the file with O_CREAT|O_EXCL. The open fails when the name already exists, and the failure message includes the name.

Neither piece is wrong on its own. The bug is that a server-controlled name reaches a terminal-facing output without control characters being removed first.

Impact

  • A malicious server can make the terminal clear the screen, hide earlier output, or change colors.
  • No code execution, no memory corruption, no file written outside the current directory, no data exposed.
  • Needs -J (not the default), and the name must be printed (for example, a file with that name already exists).

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