Files
Kordant/piolium/findings/p8-002-puppeteer-ssrf/draft.md
2026-05-29 09:03:47 -04:00

56 lines
3.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.ts` lines 141150 (generatePDF function)
- `web/src/server/services/reports/generator.ts` lines 53137 (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
```typescript
// 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
1. Admin (or attacker with SQL injection) controls report data or template files
2. Data contains `<img src="file:///etc/passwd">` or `<img src="http://169.254.169.254/latest/meta-data/">`
3. `generatePDF()` renders the report via Puppeteer
4. Puppeteer resolves the URL, reading local files or accessing cloud metadata
5. Attack succeeds because `--no-sandbox` disables Chrome sandboxing
## Defense Search Results
- `--no-sandbox` flag 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