Skip to content

Stored HTML Injection in Pi-hole Web Interface via X-Forwarded-For Header in Active Sessions Table

Moderate
PromoFaux published GHSA-8rw8-vjgp-rwj6 Feb 17, 2026

Package

Pi-Hole Web

Affected versions

>=6.0

Patched versions

None

Description

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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

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"}'
image

Once logged in, navigate to Settings > Web Interface / API, and you will be able to observe the injected code in the active sessions section.

image

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.

Severity

Moderate

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
Low
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
Low
Availability
Low

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:L/UI:N/S:U/C:N/I:L/A:L

CVE ID

CVE-2026-26953

Weaknesses

Improper Input Validation

The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly. Learn more on MITRE.

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. Learn more on MITRE.

Improper Encoding or Escaping of Output

The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved. Learn more on MITRE.

Credits