Escape

Trust and CSP

Intent

One claim decides how quario fits a security review, and it’s worth stating before anything else.

quario trusts the host’s configuration. It trusts neither the report definition nor its render data.

The host is the code that creates the instance and gives it a report. quario trusts what it configures, because that lives in your own process. It covers registered functions and the query budgets. It also covers the scheme allowlist, fonts, and the license key. A definition is the opposite. An agent may have written it, or a user through an embedded editor. So the engine treats it as untrusted input, the same as render data. Nothing a definition says executes, and nothing it says becomes markup.

Render data arrives already resolved, because the engine opens no connection of its own. The one place it waits for your code is an asynchronous registered function, which is host code you registered. Supplying the data describes where the data comes from.

Design decisions

Nothing turns a string into JavaScript. Expressions parse into closures when a definition compiles. There is no eval, no new Function, and no code generation anywhere in the render path, which is why a page can ship a script policy that omits unsafe-eval. The engine’s suites run under Node’s --disallow-code-generation-from-strings, a source scan, and browsers loading the published files under a strict policy.

Escaping belongs to the target, at its own edge. The event stream carries unescaped values. @quario/html escapes every character a definition or its data contributes: literal template text, header cells and interpolated values, and every generated attribute. A consumer that embeds stream values in markup owns its own escaping.

There is no unescaped form. {{{ }}} is a definition error rather than an escape hatch, so no schema syntax can exempt a value from escaping.

Formatting arrives only as declarations. Markup in a definition’s own text makes no element in any target. Emphasis comes from style and styled runs, which a target turns into attributes it escapes.

A location is the one value the engine judges. Every other declaration reaches the target as the author wrote it, because a bad-looking render is a recoverable fault. A URL isn’t: javascript: is a string-to-code path, and escaping at the markup edge can’t answer it. So the engine admits an href before it crosses, against an allowlist the host owns. What no target ever receives is a string it has to decide about.

A report reads your data and can’t assign to it. Expressions also inherit the expression engine’s guards, so __proto__, constructor and prototype never resolve, and a function comes only from the registry you passed. An expression has no loops and can’t call itself, so a definition can’t spin a render forever either.

The no-string-to-code guarantee covers script execution, not every directive. Conditional formatting emits inline style attributes, so a restrictive style-src has to account for them. An image item’s data: URI needs img-src data:. The viewer and the editor paint an SVG image from a data: URI as well, so a page that previews one needs the same grant. A PNG or JPEG there needs none. A grant for content you asked for is an ordinary deployment requirement, not a hole.

The unlicensed marking never depends on a grant. A blocked marking would fail open, so it’s the one output that must render without one.

API walkthrough

Bounding what a render may read

The JSONPath selection is the one place a hostile dataset can cost you time rather than correctness. Budgets are an API argument, never a schema field, so a definition can’t raise its own ceiling.

report.ts
import { quario } from 'quario'

const q = quario({ query: { maxNodes: 10_000, maxDepth: 64, maxResults: 1_000 } })

maxNodes bounds how many nodes the traversal visits. maxDepth bounds how deep it goes. maxResults bounds how many rows it may select.

A budget failure points at data, keeps its RangeError type, and carries code, limit and actual. Every render starts with fresh counters, so one expensive dataset can’t exhaust the next.

quario() checks the budgets when you call it, so a bad value fails at startup even for a report that never runs a query.

quario({ schemes }) is that allowlist, defaulting to https: and mailto:. Each entry is lower-case and ends in a colon. A value the engine can’t read as an allowlist, null included, throws a TypeError at quario(), prefixed options.schemes:.

report.ts
import { quario } from 'quario'

const q = quario({ locale: 'en-IE', schemes: ['https:'] })

A refused value drops out, so the text renders and the link doesn’t. A URL arrives from data, and failing the whole render the day a row changed would break a report that was working. A URL stating no scheme falls with the rest. A scheme list can’t say “no scheme” without also admitting //host/path, which states none and leaves the document all the same.

The allowlist is the host’s, never the document’s. A report that could widen it would be author data authorising itself. A schemes value the engine can’t read throws at construction rather than using the default. An allowlist quietly replaced means a host believes it restricted something it didn’t.

An href beginning # is an internal target naming a group’s label. It leaves the document by no scheme, so no allowlist reads it.

What the boundary doesn’t claim

A registered function is host code, and quario doesn’t limit what it does once the engine calls it. Whoever registers one is inside the boundary, so register only functions you would run on any definition.

quario guarantees that nothing a definition says executes. It doesn’t guarantee that the report is right. A definition an agent wrote can still read the wrong field or total the wrong column, which is what validation and review are for.

Where the claim holds

In HTML, the target escapes every character a definition or its data contributes, without exception.

In a workbook, the target writes no formula from a definition or a data value, so a cell holding =SUM(A1:A9) is a string that says so.

In CSV, the target mangles formula-like text on write, whichever of the two it came from. That’s this format’s version of the same rule.

Environments

The source and the builds are runtime-neutral JavaScript. Node.js LTS and Chromium under a strict Content Security Policy are the tested environments. Every package is ESM-only.