wkhtmltopdf is archived and has a CVSS 9.8 CVE

Migrate from wkhtmltopdf to Papyr

wkhtmltopdf was archived in January 2023 and has an unpatched server-side request forgery vulnerability rated CVSS 9.8. If it's in your stack, you need to replace it. Papyr takes the same HTML input and returns a PDF — migration takes about 15 minutes.

Why wkhtmltopdf has to go

Archived — no more patches

The maintainers archived the project on GitHub in January 2023. No new releases, no security fixes, no bug fixes. The last release was 0.12.6 in 2020.

CVSS 9.8 SSRF vulnerability (CVE-2022-35583)

An unpatched server-side request forgery vulnerability allows attackers to reach your internal network via crafted HTML input. If users control any part of the HTML you render, this is a live attack vector.

No modern CSS

wkhtmltopdf uses QtWebKit, which is stuck at roughly Chrome 28 (2013). Flexbox is broken. CSS Grid doesn't exist. Custom properties don't work. You spend hours writing table-based HTML for a 2024 app.

Binary dependency hell

Font rendering differences between macOS and Linux. Missing system fonts on Alpine. Locale issues. Different output across environments.

Migration guide

Your HTML stays exactly the same. You replace the wkhtmltopdf call with an HTTP request.

Before — wkhtmltopdf CLI or library

Typical wkhtmltopdf usage (shell exec, node-wkhtmltopdf, pdfkit wrapper, etc.):

wkhtmltopdf --page-size A4 --margin-top 20mm input.html output.pdf

After — Papyr API (Node.js)

Node.js
const fs = require("fs");

async function htmlToPdf(html) {
  const res = await fetch("https://api.getpapyr.dev/v1/render/pdf", {
    method: "POST",
    headers: {
      "Authorization": "Bearer pk_live_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, options: { format: "A4" } }),
  });
  return Buffer.from(await res.arrayBuffer());
}

const pdf = await htmlToPdf("<h1>Hello</h1>");
fs.writeFileSync("output.pdf", pdf);

After — Python

Python
import requests

def html_to_pdf(html: str) -> bytes:
    res = requests.post(
        "https://api.getpapyr.dev/v1/render/pdf",
        headers={"Authorization": "Bearer pk_live_..."},
        json={"html": html, "options": {"format": "A4"}},
    )
    res.raise_for_status()
    return res.content

pdf = html_to_pdf("<h1>Hello</h1>")
with open("output.pdf", "wb") as f:
    f.write(pdf)

After — PHP

PHP
$ch = curl_init("https://api.getpapyr.dev/v1/render/pdf");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer pk_live_...",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "html"    => "<h1>Hello</h1>",
        "options" => ["format" => "A4"],
    ]),
]);
$pdf = curl_exec($ch);
file_put_contents("output.pdf", $pdf);

Options mapping

wkhtmltopdf flagPapyr option
--page-size A4options.format: "A4"
--orientation Landscapeoptions.landscape: true
--margin-top 20mmoptions.margin.top: "20mm"
--margin-right 15mmoptions.margin.right: "15mm"
--margin-bottom 20mmoptions.margin.bottom: "20mm"
--margin-left 15mmoptions.margin.left: "15mm"
--no-backgroundoptions.printBackground: false

What gets better immediately

Flexbox and CSS Grid work — rewrite your table-based layouts to modern CSS
Google Fonts and custom @font-face fonts render correctly
No binary to install, no Docker layer, no Linux font packages
Consistent output across macOS, Linux, CI, and production
Active maintenance — security patches, bug fixes, new features
The unpatched SSRF CVE is gone from your attack surface

Ready to migrate?

Free tier includes 100 documents/month. No credit card required.