Skip to content

Stored HTML Injection in queries.js

Low
PromoFaux published GHSA-jx8x-mj2r-62vq Apr 3, 2026

Package

No package listed

Affected versions

v6.0 - v6.4.1

Patched versions

6.5

Description

Hi Team,

please see the vulnerability details below. Please let me know if there are any questions or remarks. Thank you for your work on the pi-hole project.

Summary

The formatInfo() function in queries.js renders data.upstream, data.client.ip, and data.ede.text into HTML without escaping when a user expands a query row in the Query Log, enabling stored HTML injection. JavaScript execution is blocked by the server's CSP (script-src 'self'). The same fields are properly escaped in the table view (rowCallback), confirming the omission was an oversight.

Details

In scripts/js/queries.js, the formatInfo() detail view interpolates API data into HTML strings without escaping:

1. data.upstream (lines 160–162, rendered at line 360):

// parseQueryStatus — builds fieldtext with raw upstream value
fieldtext =
  (data.reply.type !== "UNKNOWN" ? "Forwarded, reply from " : "Forwarded to ") +
  data.upstream;                    // <-- no escapeHtml

// formatInfo — inserts fieldtext into HTML
queryStatus.fieldtext +             // <-- unescaped in HTML

2. data.client.ip (line 395):

const ipInfo =
  data.client.name !== null && data.client.name.length > 0
    ? utils.escapeHtml(data.client.name) + " (" + data.client.ip + ")"
    : data.client.ip;               // name IS escaped, IP is NOT

3. data.ede.text (line 432):

edeInfo += ">" + data.ede.text + "</strong></div>";  // <-- no escapeHtml

Inconsistency proving oversight: The table view (rowCallback) properly escapes the same data — utils.escapeHtml(querystatus.fieldtext) at line 643, .text(data.client.ip) at line 685, and utils.escapeHtml(data.cname) at line 675.

Upstream constraints: In normal operation, upstream and client.ip are set internally by FTL from network data and cannot contain HTML. However, data.ede.text (Extended DNS Error) comes from upstream DNS server responses — a malicious upstream DNS server could return arbitrary EDE text. The teleporter does not import pihole-FTL.db on restore (confirmed by testing — the file is exported but silently skipped on import), so teleporter import is not a viable vector.

PoC

Prerequisites: Pi-hole Web v6.4.1; control of an upstream DNS server (for EDE text), or a browser proxy / Playwright to intercept API responses (to demonstrate the client-side vulnerability in isolation).

Note: FTL serves query data from memory, not directly from pihole-FTL.db. Injecting rows into the database does not surface them via the API without an FTL restart, and FTL rebuilds its own ID mappings on startup. The most practical PoC uses API response interception to simulate malicious data reaching the client, isolating the client-side escaping bug from the server-side data path.

Method: API response interception (Playwright)

await page.route('**/api/queries**', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  if (json.queries && json.queries.length > 0) {
    json.queries[0].upstream = '<b style="color:red;font-size:18px">INJECTED-UPSTREAM</b>';
    json.queries[0].client = { ip: '<b style="color:red;font-size:18px">INJECTED-IP</b>', name: null };
    json.queries[0].status = 'FORWARDED';
    json.queries[0].reply = { type: 'IP', time: 25.0 };
  }
  await route.fulfill({ response, body: JSON.stringify(json) });
});
  1. Set up the route interception above, then navigate to Query Log (/admin/queries).
  2. Click the first query row to expand the detail view.

Screenshot:
image

Impact

Defense-in-depth stored HTML injection. Authenticated Pi-hole administrators who expand query rows in the Query Log are affected. JavaScript execution is blocked by CSP (script-src 'self'), limiting impact to UI defacement and potential phishing via injected forms (the CSP is missing form-action). Exploitation requires local filesystem access (database manipulation) or control of an upstream DNS server (for EDE text — the most realistic remote vector). The inconsistency between the properly-escaped table view and the unescaped detail view confirms this was an oversight, not a design choice.

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
Local
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:L/AC:L/PR:H/UI:R/S:U/C:L/I:L/A:N

CVE ID

CVE-2026-33405

Weaknesses

No CWEs

Credits