The pdfkit v0.8.6 exploit is CVE-2022-25765, a command injection vulnerability in the Ruby pdfkit gem where an unsanitized URL passed to the PDF renderer allows shell commands to execute on the server. It affects pdfkit from version 0.0.0 through 0.8.6, and the practical fix is upgrading to 0.8.7.2 or later — the initial 0.8.7 patch was incomplete. This is a textbook example of a whole vulnerability class: user input flowing into a shell command line. Below is how the flaw arises, how to tell whether your application is reachable, and how to remediate it properly, framed for defenders rather than attackers.
What pdfkit does and where the flaw lives
The pdfkit gem is a Ruby wrapper around wkhtmltopdf, the command-line tool that renders HTML into PDF. To do its job, pdfkit builds a wkhtmltopdf command line and shells out to it. The vulnerability is in how it assembled that command line: pdfkit constructed the invocation using Ruby's %x{} / backtick shell execution rather than a safe argument array, and it did not neutralize shell metacharacters in the URL or options it was handed.
That is the entire root cause. Whenever a program builds a shell command by concatenating strings and one of those strings is influenced by an untrusted source, the shell's own metacharacters — semicolons, backticks, $(), pipes — become available to whoever controls the input. The URL argument was the tainted string here.
How the injection happens, conceptually
Consider a Rails action that turns a user-supplied address into a PDF:
# Vulnerable pattern
url = params[:url] # attacker-influenced
kit = PDFKit.new(url) # url flows into a shell command line
pdf = kit.to_pdf
Because the URL is placed on a shell command line without sanitization, a value carrying shell metacharacters is interpreted by the shell rather than treated as a plain URL. The affected component is described in the advisories as the URL not being properly sanitized before it reaches command execution. Conceptually, an attacker supplies a "URL" whose trailing portion is a shell fragment; the shell runs that fragment with the privileges of the Ruby process.
We are deliberately not publishing a working payload string. The defensive point stands without one: any code path where request data — query parameters, form fields, JSON body values, even a stored value that originated from a user — reaches PDFKit.new or pdfkit options on a version at or below 0.8.6 is a live command-injection exposure. That is what you are looking for.
Are you actually reachable?
Three conditions have to hold for CVE-2022-25765 to be exploitable in your app:
-
You depend on
pdfkitat 0.8.6 or below. Check the lockfile, not just the Gemfile:bundle list | grep pdfkit grep -A1 "pdfkit" Gemfile.lock -
Untrusted input reaches the URL or options. Grep your codebase for the sink and trace backward to the source:
grep -rn "PDFKit.new\|PDFKit.from" app/ lib/For each hit, ask whether the argument derives from
params, a request header, or stored user data. A hardcoded internal URL is not exploitable; anything user-influenced is. -
The gem is loaded server-side with meaningful privileges — which for a Rails app it is.
If all three hold, this is a high-severity finding: unauthenticated remote code execution when the endpoint is public. Prioritize accordingly. An SCA scan that resolves the transitive graph will tell you condition 1 across every service at once, including apps that pulled pdfkit in through another gem rather than declaring it directly.
The fix: upgrade, and don't stop at 0.8.7
The remediation history is genuinely confusing, so be precise:
- 0.8.7 attempted the fix but it was incomplete — the sanitization could still be bypassed.
- 0.8.7.2 is the version the advisories converge on as the complete fix.
Upgrade to 0.8.7.2 or higher:
# Gemfile
gem "pdfkit", ">= 0.8.7.2"
bundle update pdfkit
bundle list | grep pdfkit # confirm >= 0.8.7.2
Defense in depth, because one gem fix is not a strategy
Upgrading closes this CVE. The following make the next URL-to-shell bug a non-event, and they apply to any renderer, not just pdfkit:
- Validate URLs before they reach the renderer. Parse with
URI.parse, allow onlyhttp/httpsschemes, and reject anything with characters outside a strict URL grammar. Whitelisting beats blacklisting metacharacters. - Prefer passing HTML, not URLs. If you can render from a trusted HTML string you assembled server-side rather than fetching an arbitrary user URL, you remove the tainted-argument problem and a server-side request forgery vector at the same time.
- Run the renderer sandboxed and unprivileged. wkhtmltopdf should execute as a low-privilege user, ideally in a container with no outbound network beyond what rendering needs, so that even a successful injection lands somewhere contained.
- Argument arrays over shell strings everywhere in your own code. When you shell out, use the array form (
system(["cmd", arg1, arg2])) so the OS never invokes a shell to re-parse your arguments. This is the single habit that prevents most command injection.
The generalizable lesson is that command injection is a data-flow problem: untrusted source, dangerous sink, no sanitization between. Tooling that traces that flow — and the manual review patterns behind it — is covered in our Academy material on injection classes.
FAQ
What versions of pdfkit are affected by CVE-2022-25765?
pdfkit from 0.0.0 through 0.8.6. The complete fix is in 0.8.7.2 — note that the earlier 0.8.7 patch was incomplete and could be bypassed.
Is my app vulnerable if I only render trusted internal URLs?
If the URL passed to pdfkit is always a hardcoded or fully server-controlled value with no user influence, the injection is not reachable. The risk is specifically untrusted input flowing into the URL or options argument. Confirm by tracing every PDFKit.new call back to its source.
Is this the same pdfkit as the Node.js/npm PDFKit?
No. CVE-2022-25765 is the Ruby pdfkit gem (a wkhtmltopdf wrapper). The npm pdfkit package is an unrelated JavaScript PDF-generation library. Make sure your finding is against the Ruby gem before acting.
Why isn't upgrading to 0.8.7 enough?
The 0.8.7 fix was found to be incomplete and bypassable. Upgrade to 0.8.7.2 or later for the complete remediation, and pair it with URL validation as defense in depth.