Skip to content

parse_field() matches header names anywhere in request → X-Forwarded-For spoofing + header smuggling #94

@kugland

Description

@kugland

Summary

parse_field() in darkhttpd.c uses strcasestr(conn->request, field) to locate HTTP header values. That scans the entire raw request buffer and returns the first substring match — including bytes inside the request-target or another header's value. A client can therefore smuggle arbitrary "headers" (Authorization, Range, Host, If-Modified-Since, X-Forwarded-For, ...) into request handling and logging.

The most impactful case is --trusted-ip: any client whose connection matches the trusted proxy address can spoof its logged IP by hiding X-Forwarded-For: <ip> inside a header it controls (e.g. Referer), without ever sending a real X-Forwarded-For header.

Reproduction

Start the server with a trusted proxy equal to the client address:

./darkhttpd ./wwwroot --port 18080 --addr 127.0.0.1 \
                     --trusted-ip 127.0.0.1 --log access.log

Send a request with no real X-Forwarded-For, but smuggle one through Referer:

printf 'GET / HTTP/1.0\r\nHost: x\r\nReferer: X-Forwarded-For: 99.99.99.99\r\n\r\n' \
  | nc 127.0.0.1 18080

access.log:

99.99.99.99 - - [...] "GET / HTTP/1.1" 200 233 "X-Forwarded-For: 99.99.99.99" ""

The logged client IP is fully attacker-controlled.

Root cause

darkhttpd.c (current master):

static char *parse_field(const struct connection *conn, const char *field) {
    size_t bound1, bound2;
    char *pos;

    /* find start */
    pos = strcasestr(conn->request, field);
    ...
}

strcasestr does not respect header-line boundaries.

Fix

Walk header lines and only match field at the start of a line. Patch is straightforward (strncasecmp after each \n). Happy to send a PR — I have a patch + regression test ready.

Impact

  • --trusted-ip log spoofing (the main one — security-relevant if logs feed fail2ban / rate limiting / audit).
  • Spoofing of Authorization, Range, If-Modified-Since, Host, Connection, X-Forwarded-Proto through any header value or request-target byte the client can place.
  • Hidden behind a Referer or User-Agent header, so trivial to do through a normal browser-side request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions