diff --git a/apps/hq/src/pdf.ts b/apps/hq/src/pdf.ts index 5f210a6..76b9ae5 100644 --- a/apps/hq/src/pdf.ts +++ b/apps/hq/src/pdf.ts @@ -20,6 +20,17 @@ function getBrowser(): Promise { export async function renderPdf(html: string): Promise { const page = await (await getBrowser()).newPage() try { + // D24 (red-team): defense-in-depth for the public-share render. Block every network + // request at the browser layer — the letterhead is fully self-contained (inline CSS, + // data:-URI logo), so any http(s)/file fetch would only come from an injected payload + // that slipped past esc(). Combined with the template CSP, an escaping miss becomes a + // harmless rendering glitch instead of an SSRF/exfiltration vector. + await page.setRequestInterception(true) + page.on('request', (req) => { + const url = req.url() + if (url.startsWith('data:') || url === 'about:blank') void req.continue() + else void req.abort() + }) await page.setContent(html, { waitUntil: 'load' }) const bytes = await page.pdf({ format: 'A4', printBackground: true }) return Buffer.from(bytes) diff --git a/apps/hq/src/templates.ts b/apps/hq/src/templates.ts index 65cd124..c504049 100644 --- a/apps/hq/src/templates.ts +++ b/apps/hq/src/templates.ts @@ -105,6 +105,10 @@ export function documentHtml(doc: Doc, client: Client, company: Record + + ${esc(doc.docNo ?? 'Draft')}