DEV Community

Vladimir Kukresh
Vladimir Kukresh

Posted on

SSRF via PDF: A Silent Threat for React/Next.js Developers

πŸ›‘ SSRF via PDF: A Silent Threat for React/Next.js Developers

Have you tested your PDF exports for SSRF vulnerabilities?


πŸ“– Introduction

Many React + Next.js developers assume that if everything happens in the browser, they’re safe.

But once your project includes PDF export (invoices, reports, analytics), a headless browser comes into play on the server side.

Key question: What happens if that browser can make requests on behalf of your server?

SSRF via PDF: A Silent Threat for React/TypeScript Developers

πŸ“ˆ Fresh Stats & Sources


πŸ§ͺ Live PoC with webhook.site

  • Open https://webhook.site and copy your unique URL.
  • Send this HTML to your PDF rendering endpoint:
<iframe src="https://webhook.site/YOUR-UUID"></iframe>
Enter fullscreen mode Exit fullscreen mode

❌ What NOT to Do (Vulnerable Example)

// pages/api/generate-pdf.ts
import puppeteer from 'puppeteer'

export default async function handler(req, res) {
  const { htmlContent } = req.body
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.setContent(htmlContent, { waitUntil: 'networkidle0' }) // ⚠️ No sanitization
  const pdf = await page.pdf()
  await browser.close()
  res.setHeader('Content-Type', 'application/pdf')
  res.send(pdf)
}
Enter fullscreen mode Exit fullscreen mode

Issues:

  • Injected HTML is not sanitized.
  • No request blocking.
  • Dangerous vectors include <iframe> and CSS imports:
@import url("http://169.254.169.254/latest/meta-data/");
Enter fullscreen mode Exit fullscreen mode

βœ… How to Do It Right (Next.js + TS)


import puppeteer from 'puppeteer'
import DOMPurify from 'isomorphic-dompurify'
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { htmlContent } = req.body as { htmlContent: string }

  // 1. SANITIZE HTML
  const cleanHtml = DOMPurify.sanitize(htmlContent, {
    ALLOWED_TAGS: ['b', 'i', 'p', 'strong', 'em', 'u', 'br'],
    ALLOWED_ATTR: []
  })

  const browser = await puppeteer.launch()
  const page = await browser.newPage()

  // 2. BLOCK UNWANTED REQUESTS (FE-critical!)
  page.on('request', (request) => {
    if (isCloudMetadata(request.url())) request.abort() // πŸ›‘
    else request.continue()
  })

  await page.setRequestInterception(true)
  const isCloudMetadata = (url: string) =>
    url.includes('169.254.169.254') || url.includes('localhost') || url.startsWith('file://')

  // 3. CSP
  await page.setExtraHTTPHeaders({
    'Content-Security-Policy': "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"
  })

  await page.setContent(cleanHtml, { waitUntil: 'networkidle0' })
  const pdf = await page.pdf()
  await browser.close()

  res.setHeader('Content-Type', 'application/pdf')
  res.send(pdf)
}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ No-Browser Alternative

import { PDFDocument } from 'pdf-lib'
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const pdfDoc = await PDFDocument.create()
  const page = pdfDoc.addPage([595, 842]) // A4
  page.drawText('Hello, secure PDF!', { x: 50, y: 790, size: 16 })
  const pdfBytes = await pdfDoc.save()

  res.setHeader('Content-Type', 'application/pdf')
  res.send(Buffer.from(pdfBytes))
}
Enter fullscreen mode Exit fullscreen mode

Benefit: No headless browser β†’ no network requests.

πŸ–Ό Attack Diagram


[ User πŸ‘¨β€πŸ’» ]
     |
     | POST htmlContent with <iframe src="http://169.254.169.254/">
     v
[ Next.js API ⚑ ]
     |
     | Render HTML via Puppeteer
     v
[ Headless Chrome 🌐 ]
     |
     | HTTP/file request to internal resources:
     |  - http://169.254.169.254/latest/meta-data/
     |  - http://localhost:8080/admin
     |  - file:///etc/passwd
     v
[ AWS ☁️ / Internal Data ]
     |
     | Response embedded in PDF
     v
[ User downloads PDF πŸ“„ ]

Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Key SSRF Breaches & Reports

🏦 Capital One (2019)

πŸ“„ Pentest Case (2024)

πŸ’° HackerOne Report #2262382


πŸ›‘οΈ SSRF Prevention Matrix

Tactic Implementation Effort
Request Blocking page.setRequestInterception Low
HTML Sanitization DOMPurify Medium
Browser Elimination pdf-lib High

πŸ“‘ Monitoring with Sentry

import * as Sentry from '@sentry/node';

page.on('request', req => { 
  if (req.url().includes('169.254.169.254')) {
    Sentry.captureMessage(`SSRF ALERT: ${req.url()}`);
    req.abort();
  } else {
    req.continue();
  }
});
Enter fullscreen mode Exit fullscreen mode

πŸ† Your Task

  • Implement request blocking
  • Run webhook.site test
  • Share results in comments!

πŸ™Œ Follow for More

I'm writing more about:

  • Frontend security for React/Next.js devs
  • Real-world web exploit mitigation
  • CSP, SSRF, XSS, and modern browser defences

πŸ“Œ Follow me here on LinkedIn

Top comments (0)