Guides · security
CVE-2022-35583: the wkhtmltopdf SSRF that will never be patched — and how to contain it if you cannot migrate this quarter
Published 2026-08-17 · every number below was pulled from its primary source on the day of writing, including the disagreement between them. Sources linked inline.
Somebody filed a ticket with a 9.8 on it and your name next to it. Before you write either the migration plan or the risk acceptance, three things are worth knowing, and only the first one is widely reported:
- The score is real, the vulnerability is real, and no patch will ever exist — the repository has been read-only since 2022.
- Two of the largest distribution security teams looked at the same behaviour and rated it far lower than NVD did. Ubuntu calls it medium and deferred it; Debian marked it unimportant and unfixed. You need to know why before you quote either number in a document.
- Migrating does not remove the SSRF. We measured the replacement everyone recommends: Chromium fetched every internal address we injected, and the response body came back as extractable text inside the PDF. Migration removes the unpatchable part. It does not remove this class of bug.
This page is written for whoever has to close the ticket, including the case where the honest answer is "not this quarter". Containment first, migration second.
What the vulnerability is
CVE-2022-35583
is server-side request forgery — CWE-918, as classified by NVD. The mechanism is one line long: HTML that
reaches the renderer can contain an <iframe> whose src is an
address inside your network. wkhtmltopdf fetches it, renders whatever comes back, and
places it in the PDF. The PDF then goes wherever your PDFs go — usually to the person who
supplied the HTML.
So the attacker gets to make requests from inside your perimeter, with your renderer's
network position and credentials-by-location, and read the answers. The classic targets are
cloud instance metadata (169.254.169.254), internal admin panels, health and
debug endpoints, and anything else that trusts callers because they are on the right
network.
| Field | Value (pulled 2026-08-17) |
|---|---|
| CVSS 3.1 base | 9.8 Critical — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Weakness | CWE-918, server-side request forgery |
| Affected | NVD's CPE names 0.12.6 — the last release, published 2020-06-10 |
| Published / last modified | 2022-08-22 / 2026-06-17 |
| NVD status | Analyzed |
| Fix available | None. Repository archived and read-only since 2022-11-22 |
Why two distributions rated it far lower than 9.8
This is the part your risk acceptance stands or falls on, and it is almost never mentioned in the write-ups. Three security teams have published a position on this CVE and they do not agree:
| Source | Rating | Status |
|---|---|---|
| NVD | 9.8 Critical | Analyzed |
| Ubuntu | medium | "Vulnerable, fix deferred" on its supported releases including 24.04 LTS, since 2023-07-18 |
| Debian | unimportant | unfixed; bullseye and bookworm marked vulnerable |
Both distributions explain themselves, and their reasoning is the same. Ubuntu's tracker note, in full:
"As of 2023-07-18, no fix for this vulnerability was made available by upstream. That is because this is not being considered a wkhtmltopdf issue, but instead an issue with applications using wkhtmltopdf without first sanitizing input that is user-supplied HTML/JS. In the related GitHub issue page it is possible to see that this issue is possibly being disputed."
Debian's note reaches the same place in fewer words: "By design, wkhtmltopdf retrieves external resources. If it is employed inside a protected network in an automated way, a malicious actor may access internal resources. A user of wkhtmltopdf should restrict such access."
In other words: fetching the resources your HTML references is the entire job. A tool
that refused would be a broken tool. The vulnerability is not a coding mistake inside
wkhtmltopdf — it is what happens when a fetching engine is pointed at attacker-controlled
input with no egress boundary around it. Ubuntu cites
commit
5426e55 as the maintainers' acknowledgement; that commit, from 2020-06-10,
is the one that created the project's
status page — the page that
says, in the maintainer's own words:
"Do not use wkhtmltopdf with any untrusted HTML — be sure to sanitize any user-supplied HTML/JS, otherwise it can lead to complete takeover of the server it is running on! Please consider using a Mandatory Access Control system like AppArmor or SELinux."
The other numbers a risk acceptance needs
CVSS describes a worst case. Two other public numbers describe the current situation, and both belong in the document because a reviewer will ask:
| Signal | Value | What it means here |
|---|---|---|
| EPSS | 0.117 — 95.7th percentile model date 2026-08-16 |
An 11.7% modelled probability of observed exploitation attempts in the next 30 days. High relative to most CVEs, and consistent with public exploit write-ups existing. It is a population-level forecast, not a statement about your host. |
| CISA KEV | Not listed catalog 2026.08.14, 1,665 entries |
No confirmed in-the-wild exploitation on CISA's record, so no BOD 22-01 remediation date attaches to it. Absence from KEV is not evidence of safety — it means the deadline is yours to set rather than the government's. |
| Exploit maturity | Public write-up and PoC | NVD's reference list tags a
Packet Storm entry and a third-party write-up as
Exploit. This is not theoretical work. |
Why no patch can arrive, stated precisely
Pulled from the GitHub API on 2026-08-17: archived: true, last push
2022-11-22, 1,352 open issues, 14,568 stars, project started 2009-08-07.
The ticket filed against the project itself — issue #5249, opened 2022-08-26 and titled "[Security 9.8 CRITICAL] CVE-2022-35583" — is still open today, with seven comments. It cannot be closed and it cannot be fixed: the repository is read-only, so there is no one to accept a pull request and no release to put it in. If your process wants a link showing the upstream state, that issue is a better citation than any advisory.
The deeper reason is structural rather than administrative. wkhtmltopdf is built on Qt 4, unsupported since 2015, containing a QtWebKit whose engine has not been meaningfully updated since 2012. QtWebKit was deprecated in 2015 and removed from Qt in 2016. Even a willing maintainer would have no upstream to send a browser-engine fix to.
Your package manager has an opinion too
Worth checking before you plan anything, because it decides whether you are on a clock:
| Distribution | State (pulled 2026-08-17) |
|---|---|
| Debian | 0.12.6-2 in oldstable (bookworm), 0.12.6-1 in
oldoldstable (bullseye). Not present in stable, testing or unstable —
source: ftp-master madison. |
| Ubuntu | Last published in noble 24.04 LTS as 0.12.6-2build2.
Marked DNE — does not exist — in every release after it, per the
Ubuntu CVE
tracker. |
Two consequences. First, apt install wkhtmltopdf still succeeds today on the
LTS a great many people run, and installs 0.12.6 with this CVE open — so a base image
rebuild will not save you, and a fresh install is not a fresh start. Second, the package is
already gone from newer releases, which means your next distribution upgrade breaks the build
rather than merely warning you. That is your real deadline, and it is usually sooner than the
security ticket's.
Am I actually exposed? A triage that takes ten minutes
Two conditions have to hold simultaneously. Answer both with evidence, not with recollection — the answers go in the document.
1. Can attacker-influenced HTML reach the renderer?
Not "do we accept HTML uploads" — almost nobody does. The question is whether any value that a user can influence ends up interpolated into the template. That includes: names and addresses on an invoice, a free-text note or comment, a company logo URL, a rich-text description, a filename, a support ticket body, a product title from a marketplace feed, a webhook payload field. Any of those is an injection point if it is not escaped, and template engines that emit HTML unescaped by default are common.
Grep for where your template interpolates, then check the escaping at each site. If every byte reaching the renderer is generated by your own code from your own data, this condition is false and your exposure genuinely is low. Write down how you verified it.
2. Can the renderer reach anything worth reaching?
Run the check rather than reasoning about it — from the renderer's own network position, not from your laptop:
# 1. Put a listener somewhere the renderer can reach.
python3 -m http.server 8099 &
# 2. Feed it HTML that tries to reach that listener, plus the
# cloud metadata address, the way an attacker's payload would.
cat > probe.html <<'HTML'
<iframe src="http://127.0.0.1:8099/probe"></iframe>
<iframe src="http://169.254.169.254/latest/meta-data/"></iframe>
<img src="http://127.0.0.1:8099/pixel.png">
HTML
# 3. Render it through your real pipeline, then check both sides:
# - nothing in the listener's access log
# - nothing fetched visible in the PDF: pdftotext probe.pdf -
# If either shows a hit, your egress controls are not on.
If the listener logs a hit, or if the metadata iframe renders anything other than an error, your egress controls are not doing what the architecture diagram says. That is the single most common finding here, and it is usually a surprise.
Containment, in order of how much risk it removes
If migration is a month of work and you have a week, this is the order that actually buys you something. The first item is worth more than all the others combined.
- Stop attacker-influenced HTML from reaching the renderer. Escape at
the interpolation site — every one of them. Where a template genuinely needs user-supplied
markup, allow-list the tags through a real sanitiser and drop
<iframe>,<object>,<embed>,<link>, and any attribute that takes a URL. This closes the vulnerability rather than reducing its blast radius, and it is the fix both distribution security teams say is yours to make. - Take away the network. The maintainer's own
AppArmor page contains
the most useful sentence in this entire subject and buries it near the bottom: remove
#include <abstractions/nameservice>from the profile and "any attempt to make a network connection by wkhtmltopdf will get denied". That is a kernel-enforced egress kill switch for an SSRF, on Ubuntu, Debian and SUSE, in one commented-out line. Red Hat and Fedora do not ship AppArmor — use SELinux, or a network namespace with no route out.#include <tunables/global> /usr/local/bin/wkhtmltopdf { #include <abstractions/base> #include <abstractions/fonts> #include <abstractions/openssl> # THE LINE THAT MATTERS. Present = the process may open sockets. # Removed = the kernel denies every network connection it attempts, # which is CVE-2022-35583 with nowhere to go. # #include <abstractions/nameservice> deny capability sys_ptrace, /proc/*/maps r, /usr/local/bin/wkhtmltopdf mr, /var/cache/fontconfig/* r, /tmp/** rwlk, # Only the directories your own templates and output need: /opt/pdf-workdir/** rwk, }Adapted from the maintainer's published policy; the working-paths section has to be customised for your application. Load it with
systemctl reload apparmorand confirm withsudo aa-statusthat the profile is listed in enforce mode. If your templates do load remote assets, keep the socket access but put an egress allow-list in front of it, and block link-local addresses so169.254.169.254is unreachable. - Leave local file access disabled. In 0.12.6,
--disable-local-file-accessis the documented default, and--allow <path>exists to open specific directories. Search your codebase for--enable-local-file-access— wrappers and Stack Overflow answers hand it out freely to make a local logo or stylesheet load, and it re-opens the file-read surface that the default was closing. Note the maintainer's caveat: that flag "will block access to the local filesystem", but "an attacker able to exploit a CVE in the prebuilt binaries may be able to bypass this setting", which is exactly why the AppArmor layer is not redundant. - Turn off what your templates do not use.
-n/--disable-javascriptif the document is static — it removes a large class of payload from a 2012 engine.--no-imagesif you have no images.--disable-external-linksand--disable-internal-linkswhere link annotations are pointless. Every flag verified against the shipped usage documentation. - Assume the process will be taken over and make that boring. Unprivileged user, its own container, read-only root filesystem, no cloud credentials on the instance role it can borrow, no shared secrets in its environment, a short timeout and a memory cap. Rendering untrusted input is a job for a machine that owns nothing.
- Put an expiry date on the acceptance. A risk acceptance with a date is a plan; one without is a decision never to migrate, taken quietly. Pin it to the distribution deadline from the table above — that one is not negotiable with you.
Check your wrappers, not just the binary
An NVD keyword search for "wkhtmltopdf" returns eight CVEs today. Only two are against the engine, and five of the remaining six are in code that calls it — which is where the recent activity is, and where a scanner that only fingerprints the binary will miss things.
| CVE | Score | Where | Note |
|---|---|---|---|
CVE-2022-35583 | 9.8 | the engine | this page — unfixed, unfixable |
CVE-2020-21365 | 7.5 | the engine | fixed. Directory traversal through 0.12.5; Ubuntu's tracker records
it as released upstream in 0.12.6-1. If you are on 0.12.6 this one is
closed — do not carry it in your ticket. |
CVE-2026-16770 | 9.8 | Perl PDF::WebKit ≤ 1.2 |
2026-08-13. Argument injection: the library turns
<meta name="pdf-webkit-KEY"> tags in your document into command-line
options. Content becomes flags. |
CVE-2026-16766 | 9.8 | Perl Catalyst::View::Wkhtmltopdf < 0.6.1 |
2026-07-25. Render options passed to the shell unsanitised; 0.6.0 was an incomplete fix. |
CVE-2024-13285 | 9.8 | Drupal wkhtmltopdf module | 2025-01-09, advisory SA-CONTRIB-2024-049 |
CVE-2020-10390 | 7.2 | PHPKB | The configurable wkhtmltopdf path was the injection point — an admin setting became code execution. |
CVE-2018-14865 | 6.5 | Odoo | Insecure options when handing documents to wkhtmltopdf → local file read |
The pattern in the wrapper bugs is one thing repeated: a CLI with dozens of flags, called through string concatenation, with user data somewhere near the flags. If you keep wkhtmltopdf for now, audit the layer that builds the command line — that is where the 2026 criticals are, and unlike the engine, those have fixes you can install.
CVE-2025-51591 (2025-07-11) is the identical injected-iframe
SSRF in Pandoc, scored 3.7 Low, with the mitigation being Pandoc's
--sandbox option. Any HTML converter that fetches what its input references
can be pointed inward. What differs between tools is whether a sandbox switch exists and
whether anyone is still shipping fixes.Measured: migrating does not remove the SSRF
This is the part worth taking away even if you close the ticket by migrating, and we have not seen it stated anywhere, so we tested it rather than asserting it. The standard recommendation is Puppeteer or Playwright — modern engine, actively patched, all true. The question nobody asks is whether the successor fetches injected internal addresses too.
It does. Same three carriers, one page, Chromium 151.0.7922.34 driven by Playwright 1.62.1, on 2026-08-17:
import http from 'node:http';
import fs from 'node:fs';
import { chromium } from 'playwright';
// Stand-in for an internal asset: reachable from the renderer,
// not from the internet. Your metadata endpoint, your admin panel.
const hits = [];
const srv = http.createServer((req, res) => {
hits.push(req.url);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<body>INTERNAL-SECRET-8f3a</body>');
});
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
const I = `http://127.0.0.1:${srv.address().port}`;
// The shape CVE-2022-35583 describes -- an injected iframe pointing at an
// internal address -- plus the two carriers people forget about.
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(`<h2>Customer note</h2>
<iframe src="${I}/admin?probe=iframe" width="440" height="90"></iframe>
<img src="${I}/pixel.png?probe=img">
<link rel=stylesheet href="${I}/theme.css?probe=css">`,
{ waitUntil: 'load' });
await page.waitForTimeout(1000);
fs.writeFileSync('/tmp/out.pdf', await page.pdf({ format: 'A4' }));
await browser.close(); srv.close();
console.log('requests that reached the internal server:', hits.length);
// then: pdftotext /tmp/out.pdf - | grep INTERNAL-SECRET-8f3a
requests that reached the internal server: 3
/admin?probe=iframe /theme.css?probe=css /pixel.png?probe=img
$ pdftotext /tmp/out.pdf - | grep -c INTERNAL-SECRET-8f3a
1
All three requests arrived: the iframe, the stylesheet and the image. And the fetched
body is not merely requested — it is rendered into the document and extractable as
text, which pdftotext confirms. So this is not blind SSRF. Whoever
receives the PDF reads the internal response.
Nothing about that is a Chromium defect, and it is not an argument against migrating. It is the same by-design fetching behaviour Debian described, in a browser that is still maintained. The honest conclusion:
On the new stack you at least get an in-process lever. Playwright and Puppeteer can refuse the request themselves, which is a reasonable belt to wear with the braces of network policy — in our run this took the count from 3 to 0 while still producing the PDF:
const ALLOW = ['https://cdn.example.com/', 'https://fonts.example.com/'];
await page.route('**/*', (route) => {
const url = route.request().url();
if (url.startsWith('data:')) return route.continue();
if (ALLOW.some((prefix) => url.startsWith(prefix))) return route.continue();
return route.abort(); // everything else, including 169.254.169.254
});
Use it as a second layer, not the only one. It runs inside the same process an attacker is trying to influence, so a network-level boundary still does the load-bearing work.
When you do migrate
The mechanics of the move — which replacement, how the flags map, and the one feature most
replacements silently drop — are covered separately:
wkhtmltopdf is archived: the 2026 migration
guide. If your PDFs contain form fields a human is supposed to type into, read
what replaces
--enable-forms before you choose, because that requirement is invisible in
your template code and every Chromium-based replacement drops it without a word.
One structural option belongs in the security conversation specifically: if the renderer lives outside your network entirely — a hosted API, ours (snapdok.io) or any of the others — then the SSRF target list shrinks to whatever that vendor's network can reach, and your internal addresses stop being reachable from the rendering step at all. That is a real reduction and worth naming, with the trade-off named just as plainly: your HTML now leaves your network, so you have moved a confidentiality decision rather than deleted a risk. For a document assembled from customer data, that trade deserves an actual decision, not a default.
What to write in the ticket
A shape that tends to survive review, with the blanks being the work:
- Finding. CVE-2022-35583, CVSS 3.1 base 9.8, SSRF (CWE-918) in wkhtmltopdf 0.12.6. No patch exists or will exist; upstream archived 2022-11-22 and issue #5249 remains open.
- Contested severity, with sources. Ubuntu rates it medium and has it deferred since 2023-07-18; Debian marks it unimportant and unfixed. Both state the same reason: fetching referenced resources is intended behaviour, so the control belongs to the caller.
- Exploitability today. EPSS 0.117, 95.7th percentile (2026-08-16). Not in CISA KEV as of catalog 2026.08.14, so no BOD 22-01 date applies. Public PoC exists.
- Our exposure. Whether attacker-influenced values reach the template, and how that was verified. Whether the renderer has egress, and the result of the probe above — with the log excerpt.
- Compensating controls in place. Which of the six above, actually deployed, with the config change linked.
- Hard deadline. The distribution one: gone from Debian stable already, and absent from every Ubuntu release after noble 24.04 LTS. The migration is scheduled for date because the package stops existing, not because a scanner complained.
The short version
The CVE is real, unpatchable, and conditional on your deployment — which is why two distribution security teams scored it far below NVD and published their reasoning. If attacker-influenced HTML reaches your renderer and that renderer can reach anything interesting, treat the 9.8 as accurate and fix the input path today. Then take away the renderer's network, keep local file access off, and put a date on the acceptance. Migrate on the distribution's schedule rather than the scanner's — and do not expect the migration itself to close the SSRF, because we measured the recommended replacement doing exactly the same thing.
Sources pulled 2026-08-17: NVD 2.0 API · Ubuntu CVE tracker · Debian security tracker · FIRST EPSS API · CISA KEV · Debian madison · GitHub API · wkhtmltopdf status page · AppArmor policy · usage docs. The Chromium SSRF measurement is our own and reproducible with the script above.