Pulp Engine Document Rendering
Get started
Release v0.45.0

Pulp Engine v0.45.0 — Release Notes

This release hardens the hostile-content trust boundary across four renderer layers: IPv6 link-local and IPv4-mapped IPv6 addresses are now blocked in the PDF renderer’s network guard and the html-renderer’s font-import validator, CSS font import URLs are percent-encoded to prevent string breakout and escape injection, the PDF renderer’s Puppeteer request guard now uses an explicit allowlist for non-HTTP(S) schemes (blocking file: and other filesystem access vectors), and all rendered HTML pages include a Content-Security-Policy meta tag that blocks JavaScript execution even if a sanitizer bypass occurred. Together these materially improve the hostile-content trust boundary. Pulp Engine remains designed for trusted-team use and is not safe to market as fully hostile multi-tenant authoring.

Also included: Docker Compose evaluator setup files, Prisma migration command corrections, editor component refactors (no functional change), and documentation updates.


What changed

1. IPv6 blocking — network guard and html-renderer font-import validator

Prior to this release, the network guard in packages/pdf-renderer/src/network-guard.ts blocked private IPv4 ranges and ULA IPv6 (fc00::/7) but did not block IPv6 link-local addresses (fe80::/10) or IPv4-mapped IPv6 addresses (::ffff:-prefixed). A template with a font import URL of http://[fe80::1]/... or http://[::ffff:127.0.0.1]/... could route requests to private infrastructure that the IPv4-range guards were intended to block.

packages/pdf-renderer/src/network-guard.ts:

Two new patterns added to BLOCKED_IP_PATTERNS:

  • /^fe[89ab][0-9a-f]:/i — blocks IPv6 link-local (fe80::/10). The bracket-stripped host starts with fe80:, fe90:, fea0:, or feb0:, all of which fall in the fe80::/10 range.
  • /^::ffff:/i — blocks IPv4-mapped IPv6 (::ffff: prefix). Addresses such as ::ffff:127.0.0.1 or ::ffff:192.168.1.1 match unconditionally.

Both checks run in isBlockedIpAddress() (synchronous fast path for literal IPs) and in the async DNS-resolution path via resolveHostnameIps().

packages/html-renderer/src/renderer.ts:

The same two blocking patterns are duplicated in PRIVATE_HOST_PATTERNS for fontImports validation. The packages are intentionally independent (auditable in isolation), so the patterns are not shared via a common dependency.


2. CSP meta tag in rendered HTML

buildHead() in packages/html-renderer/src/renderer.ts now injects:

<meta http-equiv="Content-Security-Policy" content="script-src 'none'; object-src 'none';" />

before the <style> block in every HTML page it produces. This ensures that even if an application-layer sanitizer bypass allowed malicious script content to reach the rendered page, the browser would refuse to execute it. Plugin objects are similarly blocked by object-src 'none'.

Scope limitation: This CSP applies to the HTML document that Pulp Engine builds and passes to Puppeteer. It does not apply to Puppeteer’s headerTemplate or footerTemplate contexts, which run in a separate browser frame managed by Puppeteer itself.


3. Font URL CSS escaping

Font import URLs in fontImports are now percent-encoded before being emitted into @import rules:

  • Backslash (\) → %5C
  • Single quote (') → %27

Without this encoding, a font URL containing ') could break out of the CSS string context and inject arbitrary @import or other CSS rules. The encoding is applied unconditionally to all fontImports values before the @font-face block is written.


4. Explicit non-HTTP(S) scheme blocking (PDF renderer)

installRequestGuard() in packages/pdf-renderer/src/renderer.ts has three layers. Layer 3 previously fell through for all non-HTTP(S) schemes, which meant file: URLs were passed to req.continue() and could potentially access the local filesystem from within the Puppeteer renderer process.

Layer 3 now uses an explicit allowlist:

  • data: and blob: — passed through (legitimate inline-resource schemes)
  • Any other scheme (including file:, ftp:, etc.) — aborted with blockedbyclient

The PDF renderer has no legitimate need for local filesystem access.


5. Tests

packages/html-renderer/src/node-renderers/renderers.test.ts:

  • CSP meta tag is present in buildHead() output
  • CSP meta tag precedes the <style> block
  • script-src 'none' and object-src 'none' directives are both present
  • Backslash in font URL is encoded to %5C
  • Single quote in font URL is encoded to %27

packages/pdf-renderer/src/network-guard.test.ts:

  • IPv6 link-local address (fe80::1) is blocked by isBlockedIpAddress()
  • IPv4-mapped IPv6 address (::ffff:192.168.1.1) is blocked by isBlockedIpAddress()
  • DNS rebinding via IPv6 link-local hostname resolution caught in async path
  • DNS rebinding via IPv4-mapped IPv6 hostname resolution caught in async path

packages/pdf-renderer/src/renderer.test.ts:

  • file:///etc/passwd URL is aborted by installRequestGuard()
  • IPv6 link-local address blocked at synchronous fast path
  • IPv4-mapped IPv6 address blocked at synchronous fast path

6. Documentation

docs/api-guide.md updated to reflect:

  • fontImports validation now blocks IPv6 link-local (fe80::/10), IPv4-mapped IPv6 (::ffff:), and ULA (fc00::/7) in addition to existing IPv4 ranges
  • customCss safe-mode behaviour clarified: @import and url() are stripped unconditionally
  • New “Browser-layer backstop” section documents the CSP meta tag and its scope

docs/editor-guide.md gains a “Rendered-page security posture” section documenting the CSP meta tag, network-guard blocking ranges, and font URL escaping, with a trust-model note that these are defences for trusted-team use only.


7. Docker Compose evaluator setup

compose.yaml and compose.postgres.yaml added for turnkey evaluation:

  • compose.yaml — file-mode, no external database required; templates and assets stored in named volumes; includes health checks
  • compose.postgres.yaml — PostgreSQL mode; service dependency chain: postgresmigratepulp-engine; handles schema migrations automatically

.env.file-mode.example and .env.postgres.example added alongside as starter environment files.

README.md updated with explicit version-pinning guidance (vX.Y.Z tags rather than latest) and a “Deployment model — trusted authors” note explaining the intended security model.


8. Prisma migration command corrections

Dockerfile, docs/deployment-guide.md, and docs/release-checklist.md corrected to use the --entrypoint flag syntax for Prisma migration commands. The previous examples used an incorrect invocation form that would fail in the published Docker image.


9. Editor component refactors (no functional change)

PreviewPanel.tsx:

Capability state machine (fetch, TTL, visibility-change re-check) extracted to apps/editor/src/hooks/use-preview-capability.ts. Preview content rendering (status panes, idle prompt, HTML iframe, PDF object) extracted to apps/editor/src/components/preview/PreviewContentArea.tsx. OutputState interface now exported. No behaviour change; identical logic in new locations.

EditorShell.tsx:

Duplicate asset-fetch effects consolidated and extracted to apps/editor/src/hooks/use-asset-fetching.ts. Issues badge and popover panel extracted to apps/editor/src/components/shell/IssuesPanel.tsx. No behaviour change.

RenderConfigEditor.tsx and TemplatePickerDialog.tsx: eslint-disable suppression comments removed (underlying lint issues resolved).


Validation

  • pnpm --filter @pulp-engine/html-renderer test — 159 passed, 0 failed
  • pnpm --filter @pulp-engine/pdf-renderer test — 157 passed, 0 failed
  • pnpm --filter @pulp-engine/editor lint — 0 errors
  • pnpm run version:check — passed

Upgrade

No breaking changes. All env var names, API shapes, and database schemas are unchanged. No migrations required.

The CSP meta tag in rendered HTML (script-src 'none'; object-src 'none') is a new addition to every rendered document. In normal Pulp Engine use this has no impact: rendered documents are document-layout output, not interactive web pages. Any deployment relying on JavaScript execution within rendered HTML documents should note the change.

Residual risk:

  • The DNS-rebinding TOCTOU window is materially reduced but not eliminated: IP addresses are checked at request time, but a sufficiently low DNS TTL could allow a rebinding attack to succeed between the check and the subsequent request.
  • The CSP meta tag does not cover Puppeteer headerTemplate/footerTemplate contexts.
  • fontImports URL escaping covers backslash and single-quote breakout vectors; fontImports remain operator-supplied and should be treated as trusted input only.
  • Pulp Engine is still not designed for environments where template authors are untrusted or adversarial. These protections harden the boundary for trusted-team deployments; they do not elevate Pulp Engine to a hostile multi-tenant authoring platform.
docker pull ghcr.io/OWNER/pulp-engine:v0.45.0