Hello everyone, first of all I would like to wish you a happy New Year. I have been spending some time thoroughly reviewing the latest version of Pi-hole, and throughout today I will be reporting a few interesting findings. Greetings to all!
Summary
Security Researcher Julio Ángel Ferrari (aka T0X1CX) discovered that the Pi-hole web interface contains a stored HTML injection vulnerability (Stored HTML Injection) in the active sessions table located on the API settings page (/admin/settings/api). This vulnerability allows an attacker with valid credentials to inject arbitrary HTML code that will be rendered in the browser of any administrator who visits the active sessions page.
Details
When a user authenticates in Pi-hole, the FTL server (Faster Than Light, Pi-hole’s core engine) records information about the session, including HTTP request metadata such as the remote IP address and, if present, the value of the X-Forwarded-For header. This header is commonly used in environments with reverse proxies (nginx, Apache, Cloudflare, etc.) to preserve the original client’s IP address.
The file scripts/js/settings-api.js contains the logic used to render the active sessions table using the DataTables library. In the rowCallback function, which is executed for each row of the table, the following vulnerable code exists on lines 114–120:
// If x_forwarded_for is != null, the session is using a proxy
// Show x-forwarded-for instead of the remote address in italics
// and show the remote address in the title attribute
if (data.x_forwarded_for !== null) {
$("td:eq(8)", row).html("<em>" + data.x_forwarded_for + "</em>");
$("td:eq(8)", row).attr("title", "Original remote address: " + data.remote_addr);
}
The issue lies in line 118, where the value data.x_forwarded_for is directly concatenated into an HTML string and inserted into the DOM using jQuery’s .html() method. This method interprets the content as HTML, which means that any HTML tags present in the value will be parsed and rendered by the browser.
The HTTP X-Forwarded-For header is a standard header that any HTTP client can send arbitrarily. It is not necessary to be behind a real proxy in order to include this header in a request. An attacker can use common tools such as curl, wget, Python requests, Burp Suite, or even JavaScript fetch() to send an authentication request with an X-Forwarded-For header that contains malicious HTML code instead of a legitimate IP address.
For example, using curl:
curl -X POST "http://pi.hole/api/auth" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: <b style='color:red'>MALICIOUS PAYLOAD</b>" \
-d '{"password":"password"}'
Or using Python:
import requests
headers = {
"Content-Type": "application/json",
"X-Forwarded-For": "<b style='color:red'>PAYLOAD</b>"
}
requests.post("http://pi.hole/api/auth", json={"password": "..."}, headers=headers)
Or using wget:
wget --header="X-Forwarded-For: <b>PAYLOAD</b>" \
--post-data='{"password":"..."}' \
http://pi.hole/api/auth
The attacker sends a POST request to the /api/auth endpoint to authenticate, including an X-Forwarded-For header with malicious HTML content.
Pi-hole FTL receives the request, validates the credentials, and if they are correct, creates a new session. The value of the X-Forwarded-For header is stored in the session data structure without any validation or sanitization.
Later, when any administrator (including a user other than the attacker) visits the /admin/settings/api page, the JavaScript code issues a GET request to the /api/auth/sessions endpoint to retrieve the list of active sessions.
The API returns a JSON object that includes the x_forwarded_for field with the exact value sent by the attacker, unmodified.
The DataTables rowCallback function processes each session. When it finds that x_forwarded_for is not null, it executes:
$("td:eq(8)", row).html("<em>" + data.x_forwarded_for + "</em>");
jQuery interprets the resulting string as HTML. If data.x_forwarded_for contains HACKED, the final HTML will be:
<em><b style='color:red'>HACKED</b></em>
The browser renders this HTML, displaying “HACKED” in red and bold in the table cell.
The DataTables configuration includes a general protection in columnDefs (lines 54–66):
columnDefs: [
{
targets: 0,
orderable: false,
className: "select-checkbox",
render() {
return "";
},
},
{
targets: "_all",
render: $.fn.dataTable.render.text(),
},
],
The setting render: $.fn.dataTable.render.text() applied to targets: "_all" should automatically escape the content of all cells, converting special HTML characters into their corresponding entities (< → <, > → >, etc.).
However, this protection is overridden by the code in rowCallback. When $("td:eq(8)", row).html(...) is executed, the cell content is directly overwritten with raw HTML, completely bypassing DataTables’ automatic escaping.
Additionally, the Pi-hole project includes a utility function utils.escapeHtml() defined in scripts/js/utils.js (lines 23–36), which is specifically designed to prevent this type of vulnerability:
function escapeHtml(text) {
if (text === null || text === undefined) {
return "";
}
return text
.toString()
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
This function is globally available as utils.escapeHtml() and is correctly used in other parts of the codebase to sanitize data before inserting it into the DOM. However, in the vulnerable code in settings-api.js, this function is not used to sanitize data.x_forwarded_for before inserting it.
The issue is further aggravated because the backend (Pi-hole FTL, written in C) also does not validate the contents of the X-Forwarded-For header. Ideally, the server should:
-
Validate that the value of X-Forwarded-For is a valid IP address (IPv4 or IPv6) or a comma-separated list of IPs.
-
Reject or sanitize values that contain characters that are not valid for an IP address.
-
At a minimum, escape special HTML characters before storing the value.
Since none of these validations are performed, the malicious value is stored as-is in the session and returned unmodified through the API.
PoC
Run the following command to log in to Pi-hole while adding the X-Forwarded-For header.
curl -X POST "http://pi.hole/api/auth" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: <b style='color:red'>MALICIOUS PAYLOAD</b>" \
-d '{"password":"password"}'
Once logged in, navigate to Settings > Web Interface / API, and you will be able to observe the injected code in the active sessions section.
Impact
Successful exploitation of this vulnerability allows an attacker with valid credentials to inject arbitrary HTML code that will be rendered in the browser of any administrator who visits the active sessions page. This enables multiple attack vectors: UI spoofing to display false or misleading information in the sessions table; concealment of malicious activity through CSS injection that hides the attacker’s session from the administrator’s view; internal phishing by creating fake HTML elements such as re-authentication forms to capture credentials; and psychological manipulation by making the administrator believe that legitimate sessions are malicious or vice versa.
Since Pi-hole implements a Content Security Policy (CSP) that blocks inline JavaScript, the impact is limited to pure HTML injection without the ability to execute scripts. However, in configurations where CSP is less restrictive or disabled, this vulnerability could escalate to full Cross-Site Scripting (XSS), allowing session hijacking, exfiltration of DNS configuration data, or modification of Pi-hole configuration without the administrator’s consent.
Hello everyone, first of all I would like to wish you a happy New Year. I have been spending some time thoroughly reviewing the latest version of Pi-hole, and throughout today I will be reporting a few interesting findings. Greetings to all!
Summary
Security Researcher Julio Ángel Ferrari (aka T0X1CX) discovered that the Pi-hole web interface contains a stored HTML injection vulnerability (Stored HTML Injection) in the active sessions table located on the API settings page (/admin/settings/api). This vulnerability allows an attacker with valid credentials to inject arbitrary HTML code that will be rendered in the browser of any administrator who visits the active sessions page.
Details
When a user authenticates in Pi-hole, the FTL server (Faster Than Light, Pi-hole’s core engine) records information about the session, including HTTP request metadata such as the remote IP address and, if present, the value of the X-Forwarded-For header. This header is commonly used in environments with reverse proxies (nginx, Apache, Cloudflare, etc.) to preserve the original client’s IP address.
The file scripts/js/settings-api.js contains the logic used to render the active sessions table using the DataTables library. In the rowCallback function, which is executed for each row of the table, the following vulnerable code exists on lines 114–120:
The issue lies in line 118, where the value data.x_forwarded_for is directly concatenated into an HTML string and inserted into the DOM using jQuery’s .html() method. This method interprets the content as HTML, which means that any HTML tags present in the value will be parsed and rendered by the browser.
The HTTP X-Forwarded-For header is a standard header that any HTTP client can send arbitrarily. It is not necessary to be behind a real proxy in order to include this header in a request. An attacker can use common tools such as curl, wget, Python requests, Burp Suite, or even JavaScript fetch() to send an authentication request with an X-Forwarded-For header that contains malicious HTML code instead of a legitimate IP address.
For example, using curl:
Or using Python:
Or using wget:
The attacker sends a POST request to the /api/auth endpoint to authenticate, including an X-Forwarded-For header with malicious HTML content.
Pi-hole FTL receives the request, validates the credentials, and if they are correct, creates a new session. The value of the X-Forwarded-For header is stored in the session data structure without any validation or sanitization.
Later, when any administrator (including a user other than the attacker) visits the /admin/settings/api page, the JavaScript code issues a GET request to the /api/auth/sessions endpoint to retrieve the list of active sessions.
The API returns a JSON object that includes the x_forwarded_for field with the exact value sent by the attacker, unmodified.
The DataTables rowCallback function processes each session. When it finds that x_forwarded_for is not null, it executes:
jQuery interprets the resulting string as HTML. If data.x_forwarded_for contains HACKED, the final HTML will be:
The browser renders this HTML, displaying “HACKED” in red and bold in the table cell.
The DataTables configuration includes a general protection in columnDefs (lines 54–66):
The setting render: $.fn.dataTable.render.text() applied to targets: "_all" should automatically escape the content of all cells, converting special HTML characters into their corresponding entities (< → <, > → >, etc.).
However, this protection is overridden by the code in rowCallback. When $("td:eq(8)", row).html(...) is executed, the cell content is directly overwritten with raw HTML, completely bypassing DataTables’ automatic escaping.
Additionally, the Pi-hole project includes a utility function utils.escapeHtml() defined in scripts/js/utils.js (lines 23–36), which is specifically designed to prevent this type of vulnerability:
This function is globally available as utils.escapeHtml() and is correctly used in other parts of the codebase to sanitize data before inserting it into the DOM. However, in the vulnerable code in settings-api.js, this function is not used to sanitize data.x_forwarded_for before inserting it.
The issue is further aggravated because the backend (Pi-hole FTL, written in C) also does not validate the contents of the X-Forwarded-For header. Ideally, the server should:
Validate that the value of X-Forwarded-For is a valid IP address (IPv4 or IPv6) or a comma-separated list of IPs.
Reject or sanitize values that contain characters that are not valid for an IP address.
At a minimum, escape special HTML characters before storing the value.
Since none of these validations are performed, the malicious value is stored as-is in the session and returned unmodified through the API.
PoC
Run the following command to log in to Pi-hole while adding the X-Forwarded-For header.
Once logged in, navigate to Settings > Web Interface / API, and you will be able to observe the injected code in the active sessions section.
Impact
Successful exploitation of this vulnerability allows an attacker with valid credentials to inject arbitrary HTML code that will be rendered in the browser of any administrator who visits the active sessions page. This enables multiple attack vectors: UI spoofing to display false or misleading information in the sessions table; concealment of malicious activity through CSS injection that hides the attacker’s session from the administrator’s view; internal phishing by creating fake HTML elements such as re-authentication forms to capture credentials; and psychological manipulation by making the administrator believe that legitimate sessions are malicious or vice versa.
Since Pi-hole implements a Content Security Policy (CSP) that blocks inline JavaScript, the impact is limited to pure HTML injection without the ability to execute scripts. However, in configurations where CSP is less restrictive or disabled, this vulnerability could escalate to full Cross-Site Scripting (XSS), allowing session hijacking, exfiltration of DNS configuration data, or modification of Pi-hole configuration without the administrator’s consent.