Skip to content

Security

BroadPaper is designed to run inside enterprise software where templates are authored by end users and data is sensitive.

No HTML in, no HTML out

Rich text is stored as a structured model (paragraphs, marks, tokens), never as HTML. Rendering produces DOM from that model with text nodes and attribute setters; innerHTML is never used with user content. Pasting into the inline editor accepts plain text only. Custom block renderers return virtual nodes that we serialise, so a block cannot emit raw markup by accident.

No code execution

The expression language is parsed with its own tokenizer and Pratt parser and evaluated by a tree-walking interpreter. There is no eval, no Function, no template compilation to JavaScript. Property access reads own data properties only: client.constructor, __proto__ and function members resolve to undefined. Expressions have length and nesting limits. Runtime data problems produce empty output and warnings, never exceptions that abort a render.

URLs

Images, logos, links and buttons accept http(s):, mailto:, relative paths and data:image/* URLs. javascript: and other schemes are rejected at validation and again at render. Inline SVG data URLs are refused if they contain scripts, event handlers or a foreignObject — after being decoded, whether they are percent-encoded or base64, and over the whole payload rather than a prefix. That matters because an uploaded file always arrives as base64: an image the designer accepts is one the renderers accept.

The PDF service

  • Each render uses a fresh, isolated browser context.
  • All network requests are intercepted. Only data: URLs and hosts on the allow-list may load, and private, loopback, link-local and CGNAT ranges are blocked even for allowed hostnames after DNS resolution (SSRF protection). Request counts are capped.
  • Timeouts, maximum page counts and request body sizes are enforced; a shared-secret bearer token gates the endpoint.
  • The page never executes template-supplied JavaScript because templates contain none; the only scripts are BroadPaper's runtime and, optionally, the host's own custom block bundle.
  • Metadata is written with pdf-lib; no shell commands are involved anywhere.

The browserless renderer has no browser and no network stack of its own: it draws what it is given. Assets referenced by URL have to be fetched by your code and handed over as bytes or data: URIs, which means the allow-listing decision is yours and is made before anything is fetched.

Data locality

BroadPaper has no SaaS component, no vendor telemetry and no licence check that phones home. Nothing about your templates, your schemas or your data is sent anywhere by the SDK, and there is no endpoint of ours for it to be sent to. Report data is bound in the user's browser (editor, preview, print fallback) or in your own renderer.

That is a claim about our code, not about the page it runs on. The network is used when you ask for it, and these are the ways to ask:

What you configureWhat it fetches, and from where
A theme naming a Google font, or any fonts entry with a URLThe font files, from that host, by the browser or your renderer.
A logo, image or chart asset with an http(s) URLThat asset, from that host.
pdfService: { url }The template, the data and the theme are POSTed to that URL — your service, but a network call with your customer's data in it.
Your own onExportPdf, createMeasurer or data-loading codeWhatever you make it fetch.
Custom blocksYour code, in your trust domain.

Embed font bytes rather than naming a hosted family, and use data: URIs or same-origin assets, and a render makes no outbound request at all. The Chromium PDF service blocks everything not on an explicit allow-list precisely so that this is enforceable rather than merely intended.

Custom blocks

Custom blocks are your code and run in your trust domain. Keep render functions pure, validate any URLs you emit with isSafeUrl, and prefer theme tokens to user-supplied colours.

Licensing and third-party notices

Everything BroadPaper depends on at runtime is under a permissive licence — MIT, ISC, Apache-2.0 — with no copyleft anywhere in the tree. Two of those licences require their notice to travel with the software, and one of the two is easy to miss.

The browserless engine is a single WebAssembly binary. Statically linked into it are its whole Rust dependency tree and two complete fonts — Noto Sans Regular and Bold, under the SIL Open Font License 1.1. Neither is visible from a package.json. A subset of Noto is embedded into any PDF containing a character the standard PDF faces cannot represent: a Greek or Cyrillic name, a symbol. Documents produced that way carry no restriction — the OFL explicitly exempts anything created with the font — but the font software inside the engine does, so the notice has to ship with anything that ships the engine.

THIRD-PARTY-NOTICES.md carries all of it, and is generated by scripts/third-party-notices.mjs by reading the installed tree and the engine binary: crate names and versions out of the compiled artefact, font licences out of the fonts' own name tables. It is regenerated by pnpm build, ships inside every published package, and is served beside the demo — a browser bundle strips the npm metadata that would otherwise carry a licence along with the .wasm.

If a future version of the engine embeds a font under a licence you cannot accept, the generator is what will tell you: it fails rather than emitting a notice it could not read.

The Chromium backend is different. playwright-core is Apache-2.0 but downloads a browser build separately at install time, and that browser carries its own licences, which are not restated here. An image built on that backend needs to carry Chromium's notices too. The browserless engine has no such tail.