Skip to content
forma.
v3.0
☰
Browse documentation
Start here
Build interfaces
Native rendering
Tooling
Security and operations
Architecture
Application guides
API reference

Docs/Security and operations

FORMA 3.0

Content Security Policy

Use Forma with CSP, Trusted Types and reviewed HTML sinks.

View source ↗Markdown ↓Source 753be2a
On this page

The historical version table is omitted and the staged-contract label is updated to Forma 3.0; the linked source preserves the original wording.

FormaJS needs no unsafe-inline and no unsafe-eval, in any build. There is no switch that could change that: expressions in data-* attributes are evaluated by an allowlist AST interpreter, and no shipped artifact contains eval, new Function or with(). The opt-in fallback that used to exist has been deleted, not disabled.

The flagship example in docs/HTML-RUNTIME.md — arrow-function callback and all — is served in CI under a real Content-Security-Policy: script-src 'self' response header, and the browser console is asserted to report zero violations.

Verified by src/__tests__/runtime-csp-default.test.ts > "no build can reach new Function, with any configuration" Verified by src/__tests__/runtime-csp-default.test.ts > "never constructs a function, not even one that would have succeeded" Verified by src/__tests__/build-artifacts.test.ts > "no build emits new Function or a with() scope wrapper"


How It Works#

Strict CSP headers block inline styles and scripts unless they carry a nonce:

text
style-src 'nonce-abc123' 'self';
script-src 'nonce-abc123' 'self';

FormaJS handles this in two ways:

Scripts: <script> tags rendered by forma-server's page renderer include a nonce attribute. No inline event handlers are used — FormaJS attaches events via addEventListener, and an on* attribute is dropped in any casing on both the client and SSR paths.

Verified by src/dom/__tests__/element-url-safety.test.ts > "drops ONCLICK-cased string props instead of writing an inline handler"

Styles: The h() function applies styles via the CSSOM API (Object.assign(el.style, ...)) instead of el.style.cssText or setAttribute('style', ...). CSSOM property assignment is not blocked by CSP — only string-based style injection is.


Style Patterns — All CSP-Safe#

typescript
// Object style (always worked)
h('div', { style: { color: 'red', fontSize: '14px' } })

// String style (CSP-safe since v1.0.9)
// Internally parsed into individual properties via CSSOM
h('div', { style: 'color: red; font-size: 14px' })

// Reactive object style
h('div', { style: () => ({ borderColor: active() ? '#6366f1' : '#333' }) })

// Reactive string style
h('div', { style: () => `border-top: 3px solid ${color()}` })

All four patterns work under strict CSP. Internally, string styles are parsed by parseCssString() into property objects, then applied via Object.assign(el.style, ...).


What CSP Blocks (and FormaJS Avoids)#

TechniqueBlocked by CSP?FormaJS uses it?
el.style.cssText = '...'Yes (style-src without unsafe-inline)No — removed in v1.0.9
el.setAttribute('style', '...')YesNo
innerHTML with style="..."YesNot for library-generated markup — h() builds DOM with createElement/textContent. innerHTML is reached only through the explicit opt-in sinks: dangerouslySetInnerHTML, setHTMLUnsafe(), template() and templateMany(), mountMarkup() and its returned handle's update(), and the srcdoc attribute (the browser parses its value as a document), each of which takes trustedHtml(). See SECURITY.md and Trusted Types.
Object.assign(el.style, {...})No (CSSOM API)Yes — all styles go through this
el.style.color = 'red'No (CSSOM API)Yes (via Object.assign)
new Function(...)Yes (script-src without unsafe-eval)No — an allowlist AST interpreter is used instead, in every build. There is no opt-in and no fallback; scripts/verify-dist.mjs greps every published artifact.
eval(...)Yes (script-src without unsafe-eval)No — same gate.
with (scope) { … }No, but it defeats scope isolationNo — removed with the evaluator that used it. See SECURITY.md.

Common Issues#

"Applying inline style violates CSP directive"#

Cause: Using a version of FormaJS before v1.0.9 that still uses el.style.cssText.

Fix: Update @getforma/core to v1.0.9+.

"Refused to execute inline script"#

Cause: A <script> tag is missing its nonce attribute. This happens if you inject scripts via innerHTML, create them with document.createElement('script') without setting the nonce.

Fix: Return forma-server's PageResponse from a typed PageHandle::render call. The renderer draws the nonce and attaches the document policy. Put code in the bundle and pass data as declared props or escaped config_json; render_page, caller-supplied nonces and config_script are removed in Forma 3.0. See the migration guide.

"Refused to apply inline style" on <style> tags#

Cause: A <style> tag is missing its nonce attribute. Forma-server's checked renderers add their nonce to the personality CSS <style> tag automatically. If you create <style> elements in JavaScript, they won't have nonces.

Fix: Use CSS classes instead of dynamic <style> injection. Or use el.style.property = value (CSSOM) which is not blocked.

"An expression rendered nothing and the console says data-forma-expr-error"#

Cause: The expression is outside the grammar, or it named something the allowlist does not offer. It was not evaluated, by design — the binding left the DOM alone rather than writing an empty string.

The console.error beside the attribute names the cause with a stable code and a column: FORMA_E_SYNTAX (the parser refused the text), FORMA_E_UNRESOLVED (an identifier that is not state, not a magic and not one of the frozen namespaces — document, window and fetch land here), FORMA_E_METHOD_DENIED / FORMA_E_PROPERTY_DENIED (not on the allowlist for that receiver), FORMA_E_CALL_DENIED (a call whose target is not an allowlisted callable: a value held in state, or a method passed as an argument), FORMA_E_KEY_DENIED (constructor, __proto__, call, …), FORMA_E_ASSIGN_DENIED (unknown or read-only target), FORMA_E_UNSUPPORTED (parsed, but outside the grammar: an arrow function anywhere but a callback's first argument, a block body, destructured parameters, a computed object key, a handler statement that is not an assignment, an update, an if or a call), FORMA_E_LIMIT / FORMA_E_BUDGET (a parse or evaluation budget), and under a tiered mount FORMA_E_GRANT_DOM / FORMA_E_GRANT_NAVIGATE (a grant the mount does not give; see docs/HTML-RUNTIME.md).

Fix: Rewrite it within the grammar (see docs/HTML-RUNTIME.md, The expression grammar is an allowlist, not a blocklist) or precompute the value server-side. There is no fallback to opt into: adding 'unsafe-eval' to your policy will not make it run, because FormaJS has no code path that would use it. Call getDiagnostics() for the full list, or listen for the formajs:diagnostic event.


Server Configuration (forma-server)#

forma-server generates strict CSP headers automatically via build_csp_header():

text
default-src 'none';
script-src 'nonce-{random}' 'self';
style-src 'nonce-{random}' 'self';
connect-src 'self';
img-src 'self' data:;
font-src 'self';
frame-ancestors 'none';
base-uri 'none';
form-action 'self'

Every page render generates a unique cryptographic nonce. Scripts and the personality <style> tag get this nonce. Everything else must come from 'self' (same origin).

The preview frame's document is the one place a script is allowed by hash, not by nonce: forma-server writes formajs-frame.global.js inline into that document (PageResponse::frame), and the document's policy names the script's SHA-256 (script-src 'sha256-…') with no 'self', so the opaque-origin frame runs exactly that runtime and nothing a module could add. Styles there are allowed by nonce, which the hash-allowed runtime has none of to give a <style> element, so a module styles itself with style attributes only. The runtime is built so its hash holds: one classic script, byte-identical from build to build, with no </script, <!-- or source map comment (see docs/HTML-RUNTIME.md, The preview frame).

Verified by src/__tests__/runtime-frame.test.ts > "the frame runtime is one classic script a server can inline as it is" Verified by src/__tests__/runtime-frame.test.ts > "two builds of the frame runtime are byte-identical"

This header is satisfied by every build. The hardened artifact is no longer a stronger guarantee — it is the same runtime, bundled without code splitting — so pick either on size and packaging grounds alone.

Do not add unsafe-inline or unsafe-eval. FormaJS is designed to work without them.

Note that img-src 'self' data: above permits data: images. FormaJS matches that posture: data:image/svg+xml is allowed only for image-context sinks (<img src>, <video poster>, …) and blocked for document-context sinks (<iframe src>, <object data>, <a href>, <use href>), where the browser would parse it as a document and run script inside it.

Verified by src/security/__tests__/url-safety.test.ts > "allows data:image/svg+xml for image-context sinks" Verified by src/security/__tests__/url-safety.test.ts > "blocks data:image/svg+xml for document-context sinks"


Trusted Types#

A policy carrying require-trusted-types-for 'script' makes the browser refuse a string written to an HTML sink (innerHTML, a <template>'s innerHTML, an iframe's srcdoc, insertAdjacentHTML, …) or a script sink (an on* attribute, a script's src) unless a Trusted Types policy the page allows made it. forma-server 3.x includes both directives in every enforced document policy:

text
Content-Security-Policy: …; require-trusted-types-for 'script'; trusted-types forma forma-runtime

This applies in debug and release, with or without a reporting endpoint. A route may use the named relax_trusted_types(reason) exit; it removes only these enforced directives and retains them in Report-Only. The nonblank reason is checked at runtime; owner and end date are checked by the consumer's SR8 forma-raw-lint allowlist in CI, not by a runtime calendar. forma and forma-runtime are core's two policies:

  • forma makes what trustedHtml(markup) returns, the one raw-HTML entry (docs/API.md). Every raw sink writes a trustedHtml() value as the TrustedHTML it is, so the page accepts it. Core refuses strings and look-alikes with stable codes before the browser sink, regardless of whether native Trusted Types is available.
  • forma-runtime belongs to the internal parser used by the public mountMarkup() and returned MountHandle.update() APIs. Internal reconciliation trims trusted markup before parsing, so browser TrustedHTML is minted again, trimmed, through this policy. A changed iframe srcdoc within that trusted markup uses the same policy. Plain strings are refused before parsing and are never minted; reconciliation is not a public export.

Both pass markup through unchanged: what they enforce is that a raw entry is named. Each is created once per page, on first use, since an enforcing page refuses a second policy of one name, and every copy of core on the page (the runtime's IIFE beside core's ESM build, say) shares it through a registry on globalThis under the key @getforma/core#trusted-types. The registry holds no policy: it holds the mint trustedHtml() is, so a script that reaches the key can mint forma values as trustedHtml() can (SR8's lint lists the key as a raw entry), and a forma-runtime parser that takes only a browser TrustedHTML, so on an enforcing page no script can mint a forma-runtime value from a string. If the page refuses either name (it is not listed, or another script created it first), core writes markup as a string, which the page then refuses, and a development build logs FORMA_W_TT_POLICY_REFUSED once.

What core writes of its own passes such a policy:

  • mount(), the function it returns, and a reactive dangerouslySetInnerHTML that becomes null empty an element with replaceChildren(), which no policy checks.
  • mountMarkup() and its returned handle's update() pass trusted markup to the internal forma-runtime parser. Reconciliation writes no on* attribute or script src: a changed one is removed, as h() and adoption write no on* name either. Raw strings are refused before parsing or a cached-markup shortcut.

Raw APIs accept only named trusted markup. Runtime data-bind:srcdoc and {item…} interpolation cannot carry that value, so both are refused before the DOM sink with a coded diagnostic identifying the binding. Their neighboring attributes continue to work; there is no uncaught native TypeError.

Under the header above, Report-Only and enforced, e2e/trusted-types.spec.ts runs core's own test pages and every raw sink given trustedHtml(), including a reconcile() diff that changes an iframe's srcdoc and a button's onclick, and asserts zero violations; a string written to innerHTML raises one report, and the page still renders under Report-Only. It runs in Chromium on every merge, and in Firefox and WebKit before each release.

Verified by src/dom/__tests__/trusted-html.test.ts > "core's own writes raise no Trusted Types violation: mount, unmount, the null clear, and every sink given trustedHtml()" Verified by src/dom/__tests__/trusted-html.test.ts > "reconcile() parses trusted markup through the forma-runtime policy and a string through no policy" Verified by src/dom/__tests__/trusted-html.test.ts > "reconcile()'s diff mints a changed srcdoc of trusted markup and writes no on* or script src" Verified by src/dom/__tests__/trusted-html.test.ts > "a string cannot enter reconcile after trusted markup populated its cache" Verified by src/dom/__tests__/trusted-html.test.ts > "the shared registry holds no policy, and its forma-runtime handle mints nothing from a string" Verified by src/dom/__tests__/trusted-html.test.ts > "trustedHtml() returns a TrustedHTML from the forma policy where the browser has Trusted Types" Verified by src/__tests__/runtime-srcdoc.test.ts > "data-bind:srcdoc is refused with its binding code while neighboring attributes work"


Testing CSP#

Check the browser console for CSP violations. They appear as:

text
Refused to apply inline style because it violates the following
Content Security Policy directive: "style-src 'nonce-...' 'self'"

If you see this with FormaJS v1.0.9+, the issue is outside FormaJS — check for:

  • innerHTML containing style="..." attributes
  • el.style.cssText = '...' in non-FormaJS code
  • Third-party libraries that inject inline styles

Find your next answer.

Search Forma 3.0 documentation, guides and examples.