<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vitalii Nemudryi</title>
    <description>The latest articles on DEV Community by Vitalii Nemudryi (@pdfik).</description>
    <link>https://dev.to/pdfik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096032%2F6550a491-03ed-44aa-a7ff-6cde173e03a6.png</url>
      <title>DEV Community: Vitalii Nemudryi</title>
      <link>https://dev.to/pdfik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pdfik"/>
    <language>en</language>
    <item>
      <title>wkhtmltopdf is archived with an open CVE — a field guide to moving off it</title>
      <dc:creator>Vitalii Nemudryi</dc:creator>
      <pubDate>Wed, 26 Aug 2026 16:25:33 +0000</pubDate>
      <link>https://dev.to/pdfik/wkhtmltopdf-is-archived-with-an-open-cve-a-field-guide-to-moving-off-it-284a</link>
      <guid>https://dev.to/pdfik/wkhtmltopdf-is-archived-with-an-open-cve-a-field-guide-to-moving-off-it-284a</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure up front: I'm Vitalii, founder of &lt;a href="https://pdfik.net" rel="noopener noreferrer"&gt;PDFik&lt;/a&gt;, a hosted&lt;br&gt;
URL/HTML-to-PDF API. It appears below as one option among several — including the ones that&lt;br&gt;
compete with it. Where a competitor is the better fit, I say so.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a decade, &lt;code&gt;wkhtmltopdf&lt;/code&gt; was the default answer to "how do I turn HTML into a PDF from my&lt;br&gt;
backend?" — one static binary, no browser to babysit, a flag for everything. Most invoice,&lt;br&gt;
report and ticket generators older than five years have it somewhere in their dependency tree.&lt;/p&gt;

&lt;p&gt;Three facts changed that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The project is archived.&lt;/strong&gt; The GitHub repository was
&lt;a href="https://github.com/wkhtmltopdf/wkhtmltopdf" rel="noopener noreferrer"&gt;archived on January 2, 2023&lt;/a&gt; and is read-only:
no maintainers, no releases, no security patches — ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is an unpatched vulnerability.&lt;/strong&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2022-35583" rel="noopener noreferrer"&gt;CVE-2022-35583&lt;/a&gt; (SSRF via rendered
content) will never be fixed upstream. If your scanner flags it, there is no "upgrade to
version X" remediation — the remediation is migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The packaging is rotting.&lt;/strong&gt; The engine is a patched Qt WebKit from another era: modern
CSS (grid, flexbox gaps, custom properties) silently misrenders, distros have been dropping
the package, official builds don't cover current Debian/Ubuntu releases or arm64 well, and
Docker images increasingly rely on third-party rebuilds of an unmaintained binary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this means your PDFs stop rendering tomorrow. It means every new deployment target,&lt;br&gt;
every security audit, and every CSS feature your designers use widens the gap. Here is the&lt;br&gt;
decision tree I'd use to close it.&lt;/p&gt;
&lt;h2&gt;
  
  
  First, inventory what wkhtmltopdf actually does for you
&lt;/h2&gt;

&lt;p&gt;Before comparing engines, grep your codebase for the flags you really use. In practice most&lt;br&gt;
setups boil down to a handful:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You use&lt;/th&gt;
&lt;th&gt;You need from a replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--header-html&lt;/code&gt; / &lt;code&gt;--footer-html&lt;/code&gt;, page numbers&lt;/td&gt;
&lt;td&gt;header/footer templates with page variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--margin-*&lt;/code&gt;, &lt;code&gt;--orientation&lt;/code&gt;, &lt;code&gt;--page-size&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;standard page options (every option below has these)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--print-media-type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;print-CSS support (&lt;code&gt;@page&lt;/code&gt;, &lt;code&gt;page-break-*&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--javascript-delay&lt;/code&gt;, &lt;code&gt;--window-status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;an explicit "wait until ready" mechanism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--cookie&lt;/code&gt;, &lt;code&gt;--custom-header&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;authenticated fetching of the source page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--toc&lt;/code&gt;, &lt;code&gt;--outline&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;PDF outline/bookmarks — &lt;strong&gt;check carefully, this is the weakest spot everywhere&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stdin/stdout piping in a worker&lt;/td&gt;
&lt;td&gt;either a library call or an API client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two honest warnings apply to &lt;em&gt;every&lt;/em&gt; path below: &lt;strong&gt;no Chromium-based engine reproduces&lt;br&gt;
wkhtmltopdf output pixel-for-pixel&lt;/strong&gt; (different engine, different line breaking, different&lt;br&gt;
font fallback — you will re-approve golden files), and &lt;strong&gt;&lt;code&gt;--toc&lt;/code&gt;/&lt;code&gt;--outline&lt;/code&gt; have no&lt;br&gt;
first-class Chromium equivalent&lt;/strong&gt; (you rebuild outlines with a post-processing step or live&lt;br&gt;
without them).&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 1 — in-process library: WeasyPrint
&lt;/h2&gt;

&lt;p&gt;If your documents are print-oriented (invoices, contracts, statements), your templates don't&lt;br&gt;
run JavaScript, and you're on Python — &lt;a href="https://weasyprint.org/" rel="noopener noreferrer"&gt;WeasyPrint&lt;/a&gt; is the closest&lt;br&gt;
thing to a spiritual successor: a real &lt;code&gt;@page&lt;/code&gt;-first CSS engine, no browser process at all,&lt;br&gt;
excellent page-break control, actively maintained.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choose it when:&lt;/strong&gt; print CSS is enough, no JS charts, Python stack, documents must not
leave your infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk away when:&lt;/strong&gt; templates depend on JavaScript rendering (Chart.js, React-rendered
markup) or you need the page to look exactly like it does in a browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The commercial sibling of this category is PrinceXML (and &lt;a href="https://docraptor.com/" rel="noopener noreferrer"&gt;DocRaptor&lt;/a&gt;,&lt;br&gt;
the hosted API built on it) — the strongest print-CSS engine on the market and the reference&lt;br&gt;
choice when compliance-grade, print-perfect documents justify the price.&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 2 — DIY headless Chromium: Playwright or Puppeteer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;page.pdf()&lt;/code&gt; in &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; / &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; gives&lt;br&gt;
you a modern engine, full JS execution, and total control. The code is five lines; the&lt;br&gt;
operations are not: you now run a browser fleet. Zombie processes, memory ceilings per tab,&lt;br&gt;
crash-looping renderers under load, sandboxing (seccomp/user namespaces if you take isolation&lt;br&gt;
seriously), font packages in the image, timeouts, and scaling the pool — all yours.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choose it when:&lt;/strong&gt; you already operate containers comfortably, render volume is modest or
bursty-but-internal, and you want zero per-document vendor cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk away when:&lt;/strong&gt; PDF generation is a side feature and you'd rather not own a browser
farm's pager duty.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Path 3 — self-hosted rendering API: Gotenberg
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gotenberg.dev/" rel="noopener noreferrer"&gt;Gotenberg&lt;/a&gt; wraps Chromium (and LibreOffice for office formats) in a&lt;br&gt;
Docker container with a clean HTTP API. You keep data on your infrastructure and get out of&lt;br&gt;
the "manage Playwright yourself" business; you still own capacity planning, upgrades and&lt;br&gt;
availability.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choose it when:&lt;/strong&gt; documents must stay in your VPC but you want an API, not a library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk away when:&lt;/strong&gt; you don't want to run and scale the container at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Path 4 — hosted APIs
&lt;/h2&gt;

&lt;p&gt;If rendering is not your core business, a hosted API turns the whole problem into an HTTP&lt;br&gt;
call. The market is healthy — a few honest reference points, all with published pricing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docraptor.com/" rel="noopener noreferrer"&gt;DocRaptor&lt;/a&gt;&lt;/strong&gt; — PrinceXML engine, the print-CSS gold standard,
SOC 2 / HIPAA-BAA posture; documents can be very large. The established choice for
compliance-heavy document generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pdfshift.io/" rel="noopener noreferrer"&gt;PDFShift&lt;/a&gt;, &lt;a href="https://www.api2pdf.com/" rel="noopener noreferrer"&gt;Api2Pdf&lt;/a&gt;,
&lt;a href="https://www.pdfmonkey.io/" rel="noopener noreferrer"&gt;PDFMonkey&lt;/a&gt;&lt;/strong&gt; and others — Chromium-based hosted rendering with
different pricing shapes (per-document, per-credit, template-first workflows). Worth
shortlisting all three; which wins depends on your volume curve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pdfik.net" rel="noopener noreferrer"&gt;PDFik&lt;/a&gt;&lt;/strong&gt; — my product, so discount accordingly. The design bet is
async-first: you &lt;code&gt;POST&lt;/code&gt; a URL or HTML, get a &lt;code&gt;job_id&lt;/code&gt;, and receive an HMAC-signed webhook
when the file is ready (polling and downloads exist too), with a free test mode that runs
the full pipeline without touching your quota. There's a
&lt;a href="https://pdfik.net/wkhtmltopdf-alternative" rel="noopener noreferrer"&gt;flag-by-flag wkhtmltopdf migration map&lt;/a&gt; that
covers the table above in detail — including the places where wkhtmltopdf still wins.
If what you actually want is to keep your existing wkhtmltopdf command lines, see
path 5 below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When is a hosted API simply wrong? Air-gapped environments, documents that legally may not&lt;br&gt;
leave your infrastructure, or rendering volumes so high that per-document pricing can't beat&lt;br&gt;
your marginal server cost. Those cases belong to paths 1–3.&lt;/p&gt;
&lt;h2&gt;
  
  
  Path 5 — keep the wkhtmltopdf interface, swap the engine: &lt;code&gt;pdfik wkhtmltopdf&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Full disclosure again: this one is mine too. Sometimes the blocker is not the engine but&lt;br&gt;
the &lt;em&gt;interface&lt;/em&gt; — hundreds of lines of shell, cron jobs and wrappers (pdfkit, wicked_pdf)&lt;br&gt;
that all speak wkhtmltopdf's flags, and nobody wants to rewrite them just to retire a&lt;br&gt;
binary. The &lt;a href="https://github.com/pdfik/cli" rel="noopener noreferrer"&gt;PDFik CLI&lt;/a&gt; ships a compatibility mode that&lt;br&gt;
takes wkhtmltopdf's own command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;wkhtmltopdf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'pdfik wkhtmltopdf'&lt;/span&gt;
wkhtmltopdf &lt;span class="nt"&gt;-s&lt;/span&gt; A4 &lt;span class="nt"&gt;-O&lt;/span&gt; Landscape &lt;span class="nt"&gt;--footer-center&lt;/span&gt; &lt;span class="s1"&gt;'Page [page] of [topage]'&lt;/span&gt; https://example.com out.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every wkhtmltopdf flag is either mapped to the API, accepted with a warning (it has no&lt;br&gt;
effect in this pipeline), or refused with a reason (it would silently change your output)&lt;br&gt;
— never ignored. &lt;code&gt;--version&lt;/code&gt; and &lt;code&gt;-h&lt;/code&gt; answer the way wrappers expect, so pdfkit and&lt;br&gt;
wicked_pdf keep working unmodified. The CLI itself is one static binary&lt;br&gt;
(Linux/macOS/Windows, MIT, open source), also on Docker as &lt;code&gt;ghcr.io/pdfik/cli&lt;/code&gt;; the&lt;br&gt;
flag-by-flag tables live in the repo's&lt;br&gt;
&lt;a href="https://github.com/pdfik/cli/blob/main/COMPATIBILITY.md" rel="noopener noreferrer"&gt;COMPATIBILITY.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Be clear about what this is: rendering happens in PDFik's cloud, so this is path 4 wearing&lt;br&gt;
a familiar face — you need network access and an API key, and it is wrong for air-gapped&lt;br&gt;
environments for the same reasons. The engine is sandboxed Chromium, not WebKit, so the&lt;br&gt;
golden-files warning above applies in full. And &lt;code&gt;--toc&lt;/code&gt;/&lt;code&gt;--outline&lt;/code&gt; are refused, not&lt;br&gt;
emulated — the honest answer from the inventory table stands here too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choose it when:&lt;/strong&gt; the wkhtmltopdf &lt;em&gt;interface&lt;/em&gt; is load-bearing (scripts, wrappers,
colleagues' muscle memory) and hosted rendering is acceptable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk away when:&lt;/strong&gt; documents may not leave your infrastructure — that is paths 1–3.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The migration checklist (whatever you pick)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Golden files first.&lt;/strong&gt; Render your 10 ugliest real documents on the old and new engine,
diff visually, and get sign-off &lt;em&gt;before&lt;/em&gt; touching production code paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts.&lt;/strong&gt; wkhtmltopdf used system fonts; containers and APIs won't have them by accident.
Embed via &lt;code&gt;@font-face&lt;/code&gt; or install them explicitly, then re-check non-Latin text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page breaks.&lt;/strong&gt; Replace &lt;code&gt;page-break-*&lt;/code&gt; hacks tuned for WebKit with standard
&lt;code&gt;break-inside: avoid&lt;/code&gt; / &lt;code&gt;@page&lt;/code&gt; rules and re-test tables that span pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readiness signal.&lt;/strong&gt; Replace &lt;code&gt;--javascript-delay&lt;/code&gt; guesswork with an explicit wait
(a selector, &lt;code&gt;window-status&lt;/code&gt;-style flag, or your engine's network-idle event).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers/footers.&lt;/strong&gt; Rebuild &lt;code&gt;--header-html&lt;/code&gt; as the engine's header/footer template and
re-verify page numbering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outline/TOC.&lt;/strong&gt; If you used &lt;code&gt;--toc&lt;/code&gt;, decide now: post-process the PDF to rebuild
bookmarks, or drop the feature consciously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts and size limits.&lt;/strong&gt; Async pipelines and APIs enforce both; find your P99 render
time and largest document before your users do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the old binary in CI&lt;/strong&gt; for one release as a fallback renderer behind a flag —
migrations get reverted for boring reasons.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  TL;DR decision tree
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Print-CSS documents, no JS, Python → &lt;strong&gt;WeasyPrint&lt;/strong&gt; (or PrinceXML/DocRaptor when budget
allows).&lt;/li&gt;
&lt;li&gt;Want full control and don't mind operating browsers → &lt;strong&gt;Playwright/Puppeteer&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Data must stay home, but you want an API → &lt;strong&gt;Gotenberg&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Want it to be someone else's pager → &lt;strong&gt;hosted API&lt;/strong&gt; (DocRaptor for print/compliance;
PDFShift/Api2Pdf/PDFMonkey/PDFik for Chromium-based rendering — pick by pricing shape and
workflow; mine is the async/webhook-first one).&lt;/li&gt;
&lt;li&gt;Scripts and wrappers full of wkhtmltopdf flags you'd rather not touch →
&lt;strong&gt;&lt;code&gt;pdfik wkhtmltopdf&lt;/code&gt;&lt;/strong&gt; (compatibility mode of my CLI — hosted rendering behind the
old interface).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Archive status and CVE status last verified: 2026-08-26. If something above is outdated,&lt;br&gt;
tell me and I'll fix it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>webdev</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
