Pulp Engine Document Rendering
Get started
Release v0.8.0

Pulp Engine v0.8.0

Release date: 2026-03-19

Highlights

  • Defense-in-depth security hardening across the PDF and HTML renderer pipeline.
  • Font import URLs are now validated (https: only, private hosts blocked) — no bypass path.
  • Custom CSS is sanitised in safe mode by default; trusted mode requires explicit opt-in.
  • Image src URLs are checked for SSRF risk (private/internal hosts blocked).
  • Puppeteer request interception blocks private-network fetches as a browser-layer backstop.
  • Chrome sandbox is now enabled by default; container deployments must set PULP_ENGINE_DISABLE_SANDBOX=true.

Security hardening

fontImports — URL validation

renderConfig.fontImports entries are now validated unconditionally before being injected as @import url(...) rules. A font URL is accepted only if:

  • Protocol is https: (http:, data:, file:, blob: are rejected)
  • Hostname does not match any private/internal address range (loopback, RFC 1918, link-local / AWS metadata endpoint 169.254.x, ULA IPv6)

Rejected URLs are dropped with a console.warn and never reach the <style> block. There is no bypass path — the check is unconditional regardless of trust mode.

customCss — trust model

renderConfig.customCss is now subject to a two-mode trust model:

  • Safe mode (default): @import rules and url() content are stripped before injection. This prevents outbound CSS fetches regardless of the CSS content. No configuration is required — safe mode is active whenever allowUnsafeCustomCss is not explicitly set.

  • Trusted mode: Pass allowUnsafeCustomCss: true as the third argument to HtmlRenderer.render(). The CSS is injected verbatim. Use only when customCss originates from a verified administrative source (e.g. a back-office template editor accessible only to trusted operators).

The new HtmlRenderTrustOptions interface is exported from @pulp-engine/html-renderer:

import { HtmlRenderer, type HtmlRenderTrustOptions } from '@pulp-engine/html-renderer'

// Safe mode (default — no third argument needed)
renderer.render(template, data)

// Trusted mode — admin CSS, verified source only
renderer.render(template, data, { allowUnsafeCustomCss: true })

Image src — SSRF prevention

Image node src templates are now validated after Handlebars resolution. HTTP/HTTPS URLs pointing to private/internal hosts (same pattern set as fontImports) are rejected and replaced with an empty src="", preventing the browser from fetching internal network resources during render.

  • Relative paths (no protocol) are always allowed.
  • data: URIs are always allowed (inline data, no network fetch).
  • http:/https: URLs to non-private hosts are allowed.
  • http:/https: URLs to private/internal hosts are blocked.
  • javascript:, vbscript:, file:, and malformed URLs are blocked.

Puppeteer request interception

A browser-layer request guard is now installed on every Puppeteer page before page.setContent() is called. page.setRequestInterception(true) is enabled and a page.on('request', ...) handler blocks any request whose URL resolves to a private/internal host (same pattern set as above).

This provides a complementary backstop layer that blocks at the browser network level, even if an SSRF-bearing URL somehow bypasses input-layer validation (e.g. via DNS rebinding). Data URIs are never blocked.

The guard is implemented in packages/pdf-renderer/src/network-guard.ts (isBlockedRenderUrl) and applied in packages/pdf-renderer/src/renderer.ts (installRequestGuard).

Chrome sandbox — enabled by default

Previously, --no-sandbox and --disable-setuid-sandbox were always passed to Puppeteer. These flags are now conditional — they are only added when PULP_ENGINE_DISABLE_SANDBOX=true is set in the environment.

  • --disable-dev-shm-usage and --disable-gpu are always passed (unchanged).
  • macOS, Windows, and Linux hosts with kernel sandbox support: no change needed — sandbox is on by default and these platforms support it.
  • Container environments (Docker, Kubernetes without elevated capabilities): Must set PULP_ENGINE_DISABLE_SANDBOX=true in .env. Chrome will fail to launch without it.

The sandbox logic is implemented in getChromiumArgs(env) (exported from packages/pdf-renderer/src/browser-pool.ts) and is fully unit-tested.

SSRF limitation note

All hostname-pattern blocking (fontImports, image src, Puppeteer request interception) matches literal hostnames in the URL string. It does not perform DNS resolution. DNS rebinding attacks — where a public domain resolves to a private IP at render time — are not fully prevented. The input-layer checks and browser-layer interception together substantially reduce browser reachability without claiming to eliminate every SSRF path.

New files

FilePurpose
packages/pdf-renderer/src/network-guard.tsisBlockedRenderUrl(url) — pure helper for private-host blocking
packages/pdf-renderer/src/network-guard.test.ts26 unit tests for all blocked categories and allowed cases
packages/pdf-renderer/src/browser-pool.test.ts6 unit tests for getChromiumArgs sandbox flag logic

Modified files

FileChange
packages/html-renderer/src/renderer.tsisSafeFontUrl, sanitiseCssForSafeMode, HtmlRenderTrustOptions, allowUnsafeCustomCss opt-in
packages/html-renderer/src/index.tsExports HtmlRenderTrustOptions
packages/html-renderer/src/node-renderers/image.tsisSafeImageSrc with private-host blocking
packages/pdf-renderer/src/browser-pool.tsgetChromiumArgs(env) exported; sandbox flags now conditional
packages/pdf-renderer/src/renderer.tsinstallRequestGuard(page) called before setContent in both render paths
packages/html-renderer/src/node-renderers/renderers.test.tsTests for fontImports validation, customCss trust model, image SSRF
packages/pdf-renderer/src/renderer.test.tsTests for request interception handler

Test coverage

packages/pdf-renderer

FileTestsScope
network-guard.test.ts26All private CIDR ranges, IPv6 (including bracket-wrapped), data: URIs, malformed URLs, public hosts
browser-pool.test.ts6Sandbox absent by default, absent for “false”/“1”, present for “true”, always-present flags
renderer.test.ts (additions)7setRequestInterception called before setContent, handler registered, block/allow by URL

packages/html-renderer

Describe blockTestsScope
Security — fontImports URL validation8Allows public https, drops http/data:/private-IP/loopback/localhost/169.254, mixed list
Security — customCss trust model6Safe mode strips @import and url(), preserves non-fetch CSS; trusted mode passes verbatim
Security — image SSRF prevention5Blocks 192.168.x, localhost, 169.254.x; allows public https and data: URI

Behaviour changes / migration notes

Container deployments — action required

If you run Pulp Engine in Docker, Kubernetes, or any environment where the Chrome sandbox is unavailable:

Add PULP_ENGINE_DISABLE_SANDBOX=true to your .env before upgrading.

Without this, Chrome will fail to launch and all PDF render requests will return 500 errors.

HtmlRenderer.render() — new optional third parameter

The method signature is now:

render(
  template: TemplateDefinition,
  data: Record<string, unknown>,
  trustOptions?: HtmlRenderTrustOptions,
): string

The third parameter is optional and defaults to safe mode. Existing callers with no third argument are unaffected. The only breaking case is if a caller was relying on raw customCss @import or url() fetches during render — those are now stripped in safe mode. Pass { allowUnsafeCustomCss: true } to restore the previous verbatim behaviour, but only when the CSS source is verified as admin-only.