Skip to main content
Docs: Security & CSP

Security & CSP

The players sanitize authored content and leave the page, the transport and the learner record to the host. Six things fall to the host because a player cannot enforce them from inside a page it does not own.

Host checklist

  1. Style origins

    Leave allowed-style-origins unset, or list only the origins the deployment serves CSS from.

  2. Content Security Policy

    Send a per-response nonce with 'strict-dynamic', allow inline styles, and add the origins your loading strategy and tools reach.

  3. Graded delivery

    For proctored or high-stakes delivery: hosted players, models redacted on your server, outcomes your server computes.

  4. trust-markup

    Keep trust-markup unreachable from page script wherever content is not host-trusted.

  5. Element packages

    Supply an ElementPackagePolicy where authored configs are less trusted than the delivery tier.

  6. Overlays

    Accept that authored absolute positioning can paint over the page, or set position: relative on .pie-item-player.

Trust boundary

Host page: trusted

It supplies config, env and every attribute, and reaches into the player's light DOM at will. Nothing in PIE defends against the embedding page.

Authored content: untrusted

Item and passage markup, their style attributes, stylesheet URLs and inline SVG tool icons pass through sanitizers and validators. Models reach elements verbatim, and each element sanitizes the rich-content fields it renders.

Element packages: trusted by default

config.elements names executable code, so by default authored data selects the code that renders it. An ElementPackagePolicy restricts it.

Sanitizer

<pie-item-player> renders in light DOM so theme tokens, color schemes and font scaling reach authored markup. Authored CSS is therefore not contained, and the sanitizer carries the containment. The section, assessment and print players render through the same lists.

Removed

  • script, iframe, object, embed, base, form, meta, link, style and SVG foreignObject
  • Event handler attributes, formaction and xlink:href
  • URLs with protocols other than http(s)
  • url(), image-set() and src() in style attributes, and position: fixed

Kept

  • PIE custom elements and the attributes the element contract uses
  • MathML
  • Element ids, unprefixed, since model lookup depends on them
  • position: absolute, which MathJax's assistive MathML needs to stay visible to screen readers

An authored full-viewport absolute overlay can still cover the page. It can neither run script nor send data, since scripts, forms, style elements and url() are gone. Setting position: relative on .pie-item-player anchors such an overlay to the item, which protects host chrome above it.

Graded delivery

By default the whole item config reaches the browser, answer keys included, and the element controllers run there. That fits practice and formative delivery. env.role, env.mode and add-correct-response are public attributes, so any page script can ask a loaded controller for the correct response. Graded delivery needs three things together.

Where the controller runs. By default the authored model, answer key included, reaches the browser and the item player runs the element controller there. In hosted delivery, set with hosted or a delivery backend, a controller on your server sends only view models to the browser.

1. Hosted players

Set hosted on <pie-item-player>, or enable runtime.player.backend.delivery on the section player, which sets it on every embedded item player. Hosted players load element bundles without controllers.

2. Redacted models

The players pass models through as they receive them and redact nothing. With backend.delivery, the PIE API service runs each controller's model() and the browser receives models filtered for the learner; any other server strips answer keys and rationales itself.

3. Server outcomes

Your server runs model() and outcome(). A delivery backend answers load, saveSession, model and score; the built-in client speaks the PIE API service's routes.

The section player's Check answer control scores Tries in the browser, so it needs an unhosted player. Server-scored practice renders its own control and records each Try through SectionController.recordFormativeTry (Formative delivery(opens in new tab)). A solution feedback policy reveals the answer to a learner who has spent a Try, by design.

External stylesheets

Items can name stylesheets in config.resources.stylesheets, and hosts add their own through external-style-urls. With allowed-style-origins unset, only same-origin http(s) URLs load; set, only the listed origins do, the page's own included only when listed.

OriginHow it loadsReach
Same originFetched and rewritten under .pie-item-player.<scope>The item only
Listed cross-originA bare <link rel="stylesheet">, since it cannot be read to scopeThe whole document

Content Security Policy

The players inject element bundles as <script> tags and the ESM strategy injects an import map; neither carries a nonce. 'strict-dynamic' admits script that trusted script creates, so the nonce on your own player bundle covers every strategy.

Network origins under the base policy. The page's nonced module script admits the player through strict-dynamic. The iife strategy loads bundles from the bundle host and MathJax 3 fonts from unpkg and speech maps from jsDelivr; esm loads element modules, metadata and MathJax assets from the ESM CDN; preloaded loads from the host's own build. Tools add Desmos, GeoGebra, Font Awesome and Roboto; the deployment adds its TTS and scoring endpoints to connect-src.

Base policy

  • 'unsafe-inline' in style-src is required: without it the browser blocks authored style attributes and the scoped stylesheets the player appends, and both fail silently.
  • img-src and media-src cover authored media and TTS audio.
  • connect-src also lists the deployment's own endpoints, such as a TTS server or a scoring API.
  • 'unsafe-eval' is not needed: no first-party code evaluates strings.
  • Trusted Types are not supported. The players write sanitized markup through innerHTML, so require-trusted-types-for 'script' breaks them.

Origins per loading strategy

StrategyElement code fromAdd to the base policy
iifeThe bundle host, https://proxy.pie-api.com/bundles/ unless loaderOptions.bundleHost names anotherconnect-src https://cdn.jsdelivr.net (MathJax 3 speech maps)
font-src data: https://unpkg.com (MathJax 3 fonts)
esmhttps://cdn.jsdelivr.net, or loaderOptions.esmCdnUrl; with the esm.sh provider, https://raw.esm.shconnect-src and font-src data: list the CDN origin, which also serves package metadata and MathJax assets
preloadedThe host's own bundleNothing for current builds, which serve MathJax from their own output

Tools and workers

SourceNeeds
Desmos and GeoGebra calculatorsTheir vendor scripts, which 'strict-dynamic' admits: www.desmos.com/api/v1.12/calculator.js and www.geogebra.org/apps/deployggb.js
Cortex calculatordata: fonts; 'self' in worker-src when the policy sets it, and blob: when loaded from a CDN
Toolbar icons and fontsFont Awesome Free from cdn.jsdelivr.net and Roboto from ui.renaissance.com in style-src and font-src, unless the page supplies both stylesheets
Math speechblob: in worker-src when the policy sets it; MathJax starts its speech worker only when a learner enables semantic enrichment

Without 'strict-dynamic'

Listing hosts instead admits IIFE bundles but refuses the injected import map, and ESM falls back to es-module-shims. Measured in Chromium:

script-srcIIFE bundleImport mapUnlisted CDN import()
'nonce-…' 'strict-dynamic'loadsloadsloads
'nonce-…'refusedrefusedloads
'nonce-…' bundle hostloadsrefusedloads
'unsafe-inline' bundle hostloadsloadsrefused

CSP reference(opens in new tab) · Loading from a CDN(opens in new tab) · MathJax assets(opens in new tab)

Escape hatches and package policy

Each of these moves a guarantee from PIE to the host that sets it.

trust-markup

Skips sanitization entirely. It is an observed attribute, so any page script can set it on a live player; it belongs only where authored markup is host-trusted code.

sanitizeMarkup

A property, with no attribute, that replaces the default sanitizer. The replacement owns the forbid-lists, the custom-element contract and id preservation.

ElementPackagePolicy

Off by default. Passed as loaderOptions.elementPackagePolicy, it restricts loading to exact package names or name@version specs, with exact versions enforced.

Integration · Security reference(opens in new tab)