3.1 KiB
Phase: 8 Sequence: 002 Slug: puppeteer-ssrf Verdict: VALID Rationale: Puppeteer launched with --no-sandbox and page.setContent() accepting arbitrary HTML; report data from database can contain URLs that Puppeteer resolves Severity-Original: high Severity: medium PoC-Status: pending Pre-FP-Flag: none Debate: piolium/attack-surface/balanced-chamber-summary.md
Summary
The report PDF generator in web/src/server/services/reports/generator.ts uses Puppeteer in headless mode with --no-sandbox flag and page.setContent() to render HTML templates to PDF. The compileData() function populates the report with data from the database (alert breakdowns, threat scores, recommendations) that are rendered as HTML strings. If any data contains URLs (e.g., file:// or http:// schemes), Puppeteer will resolve them, enabling SSRF.
Location
web/src/server/services/reports/generator.tslines 141–150 (generatePDF function)web/src/server/services/reports/generator.tslines 53–137 (compileData function)
Attacker Control
An attacker with admin access can control report template files in web/src/server/services/reports/templates/, or an attacker with SQL injection access (DFD-1) can inject URLs into the normalizedAlerts table that gets rendered in reports. The compileData() function uses source values from the database and generates HTML with these values.
Trust Boundary Crossed
Database-stored data → Browser rendering context (Puppeteer). This crosses the server-to-browser trust boundary within the server process, allowing controlled data to trigger network requests to arbitrary URLs.
Impact
SSRF to internal services (metadata endpoints, internal APIs), local file read via file:// URLs. The --no-sandbox flag disables Chrome sandboxing, significantly expanding the attack surface.
Evidence
// generatePDF() — no-sandbox + arbitrary HTML
export async function generatePDF(html: string): Promise<Buffer> {
const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "load" }); // Arbitrary HTML
// ...
}
// compileData() — populates report with database data
// alertBreakdownRows contains source values from normalizedAlerts table
// recommendations generates HTML with emoji and markdown-like content
Reproduction Steps
- Admin (or attacker with SQL injection) controls report data or template files
- Data contains
<img src="file:///etc/passwd">or<img src="http://169.254.169.254/latest/meta-data/"> generatePDF()renders the report via Puppeteer- Puppeteer resolves the URL, reading local files or accessing cloud metadata
- Attack succeeds because
--no-sandboxdisables Chrome sandboxing
Defense Search Results
--no-sandboxflag is present — disables Chrome sandboxing- No URL allowlisting or blocking in Puppeteer
- No
page.setRequestInterception(true)to block non-allowed URLs - CSP is not effective for Puppeteer headless browser
- HTML template system uses
{{key}}substitution without escaping