Replacing wkhtmltopdf / DinkToPdf in .NET: CVE-2022-35583 and your options — Custom Software article by Rui Codex

Many .NET applications produce their invoices, reports, delivery notes and labels the same way: render a Razor view to HTML, then pass it to wkhtmltopdf, usually through DinkToPdf, Rotativa or a similar wrapper. It still works. The problem is what sits underneath it, and this article covers why that is, what CVE-2022-35583 means in practice, and which replacements make sense, with their licence caveats and the migration steps we follow.

Why wkhtmltopdf is a dead end

  • The engine is archived. The wkhtmltopdf repository on GitHub was archived by its owner on 2 January 2023 and is read-only.
  • The rendering engine is very old. The maintainer's own status page sums it up: "Qt 4 (which wkhtmltopdf uses) hasn't been supported since 2015, the WebKit in it hasn't been updated since 2012." Templates written for today's CSS are rendered by an engine that predates most of it.
  • The maintainer advises caution. The same page says: "Do not use wkhtmltopdf with any untrusted HTML", recommends sandboxing it with AppArmor or SELinux, and points users with dynamic pages to Puppeteer or one of its wrappers.
  • The .NET wrapper is unmaintained as well. DinkToPdf has more than 32 million downloads on NuGet. Its last release, 1.0.8, was published on 18 April 2017, and its repository has had no code pushed since April 2020.
  • A maintained wrapper does not change the engine. Rotativa.AspNetCore still gets releases, but its README is explicit: "Rotativa uses wkhtmltopdf/wkhtmltoimage behind the scenes."

CVE-2022-35583 in one paragraph

The NVD entry for CVE-2022-35583 describes a server-side request forgery (CWE-918) in wkhtmltopdf 0.12.6: an attacker who can inject an iframe tag pointing at an internal address makes the renderer fetch it, which can expose internal services. NVD scores it 9.8 (Critical) under CVSS 3.1. Because the upstream project is archived, there is no maintained project to ship a fixed release.

Whether it matters for a given application depends on one question: can user-influenced content reach the HTML that wkhtmltopdf renders? Customer names, addresses, order notes, product descriptions and anything users upload are typical routes. Razor's default HTML encoding helps a lot, but every Html.Raw and every rich-text field deserves a look. That is an assessment to make on your own code; a version number alone does not settle it.

The options

OptionHow it worksLicenceGood fit
Microsoft.PlaywrightHeadless Chromium prints your HTML to PDFMITExisting HTML or Razor templates
PuppeteerSharpHeadless Chromium through a Puppeteer-style APIMITSame use case; a matter of API preference
QuestPDFDocuments are laid out in C# code; no HTML at allCommunity licence or paid (see below)Invoices and reports where you control the layout in code
GotenbergA separate Docker service with an HTTP API for converting documents to PDFMITTeams that prefer to keep rendering out of the application process

Licence caveats. Playwright and PuppeteerSharp are MIT-licensed and download a Chromium build, so treat Chromium as a dependency you have to update like any other. QuestPDF is not MIT: under its Community License it is free for individuals and businesses with annual gross revenue under USD 1,000,000, and for charities, academic institutions and open-source projects, while public-sector bodies and publicly traded companies are not eligible regardless of revenue. Everyone else needs a paid licence. Several commercial HTML-to-PDF libraries also exist; note that many of the "migrate from wkhtmltopdf" guides you will find are written by their vendors.

What the code looks like

With Playwright, an existing HTML template keeps working almost unchanged. The request routing below is the important part: it makes the renderer refuse to fetch anything except your own assets, which closes the class of problem behind CVE-2022-35583 instead of relying on every template being perfectly encoded.

using Microsoft.Playwright;

// Create once at startup and reuse; launching Chromium per document is slow.
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();

// One page per document. Disable JavaScript if your templates do not need it.
var page = await browser.NewPageAsync(new BrowserNewPageOptions { JavaScriptEnabled = false });

// Allow only your own asset host; abort every other request (fonts, images, iframes...).
await page.RouteAsync("**/*", async route =>
{
    if (route.Request.Url.StartsWith("https://assets.example.com/"))
        await route.ContinueAsync();
    else
        await route.AbortAsync();
});

await page.SetContentAsync(html);   // html = your rendered Razor template
byte[] pdf = await page.PdfAsync(new PagePdfOptions
{
    Format = "A4",
    PrintBackground = true,
    Margin = new Margin { Top = "15mm", Bottom = "15mm", Left = "12mm", Right = "12mm" }
});
await page.CloseAsync();

Playwright's PdfAsync returns the PDF as a byte[] and also supports DisplayHeaderFooter, HeaderTemplate, FooterTemplate and PreferCSSPageSize.

QuestPDF takes the other route: no HTML and no browser, just a layout described in C#.

using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

QuestPDF.Settings.License = LicenseType.Community; // only if your organisation qualifies

byte[] pdf = Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(1.5f, Unit.Centimetre);
        page.Header().Text("Invoice 2026-0042").FontSize(18).SemiBold();
        page.Content().PaddingVertical(10).Text("Line items go here.");
        page.Footer().AlignCenter().Text(text =>
        {
            text.CurrentPageNumber();
            text.Span(" / ");
            text.TotalPages();
        });
    });
}).GeneratePdf();

Migration steps

  1. Inventory. List every place that creates a PDF (DinkToPdf, Rotativa, Wkhtmltopdf.NetCore or a direct call to the binary) and the options each one uses: paper size, margins, header and footer HTML, page numbering.
  2. Freeze a baseline. Render every template with the current engine and keep the PDFs as reference output.
  3. Choose per template. HTML templates usually move to Chromium. Documents that are mostly tables and totals are often simpler and faster in QuestPDF, if the licence fits.
  4. Port headers and footers. wkhtmltopdf fills placeholders such as [page] and [topage] in its header HTML. Chromium uses HeaderTemplate and FooterTemplate with elements carrying the classes pageNumber and totalPages, and those templates must carry their own inline styles.
  5. Fix page breaks with CSS. Use @page for size and margins and break-inside: avoid on table rows and totals blocks.
  6. Fonts on Linux. Chromium can only use fonts that exist in the container. Install the fonts your templates use, or embed them with @font-face. Playwright publishes Docker images named mcr.microsoft.com/playwright/dotnet:v<version>-noble; pin the tag to the same version as your NuGet package. The same page notes that the image is meant for testing and development and is not recommended for visiting untrusted sites, which is one more reason to block outgoing requests as shown above.
  7. Manage the browser lifecycle. Launch Chromium once, open a page per document, close it afterwards, and cap the number of concurrent renders.
  8. Compare. Check page counts, totals and layout against the baseline, page by page, before switching over.
  9. Remove the old binaries. Delete wkhtmltopdf from build agents, images and deployment folders, so nothing can call it by accident.

Common questions

Is DinkToPdf itself vulnerable?

DinkToPdf is a wrapper around the wkhtmltopdf library, so it renders with the same engine. The CVE is recorded against wkhtmltopdf 0.12.6.

Do I need a paid PDF library to replace it?

No. Playwright and PuppeteerSharp are MIT-licensed, and QuestPDF is free for organisations that meet its Community License conditions. A paid library is a choice, not a requirement.

Rui Codex helps .NET teams move PDF generation off wkhtmltopdf and DinkToPdf: inventory, template migration, Linux and Docker fonts, and a page-by-page comparison, done in your own repository. See our custom software services or contact us.

Sources: NVD, CVE-2022-35583; wkhtmltopdf status page; wkhtmltopdf on GitHub; NuGet, DinkToPdf; Rotativa.AspNetCore README; QuestPDF Community License; Playwright for .NET, Docker. Checked on 22 October 2026.

Tags: .NET PDF Generation wkhtmltopdf DinkToPdf Playwright QuestPDF Legacy Migration

Need Help Implementing This?

Our team can help you put these insights into practice. From AI automation to custom software development, we build solutions that deliver real results.

Book a Discovery Call