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
- Style origins
Leave allowed-style-origins unset, or list only the origins the deployment serves CSS from.
- Content Security Policy
Send a per-response nonce with 'strict-dynamic', allow inline styles, and add the origins your loading strategy and tools reach.
- Graded delivery
For proctored or high-stakes delivery: hosted players, models redacted on your server, outcomes your server computes.
- trust-markup
Keep trust-markup unreachable from page script wherever content is not host-trusted.
- Element packages
Supply an ElementPackagePolicy where authored configs are less trusted than the delivery tier.
- 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,styleand SVGforeignObject- Event handler attributes,
formactionandxlink:href - URLs with protocols other than http(s)
url(),image-set()andsrc()in style attributes, andposition: 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.
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.
| Origin | How it loads | Reach |
|---|---|---|
| Same origin | Fetched and rewritten under .pie-item-player.<scope> | The item only |
| Listed cross-origin | A bare <link rel="stylesheet">, since it cannot be read to scope | The 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.
Base policy
'unsafe-inline'instyle-srcis required: without it the browser blocks authoredstyleattributes and the scoped stylesheets the player appends, and both fail silently.img-srcandmedia-srccover authored media and TTS audio.connect-srcalso 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, sorequire-trusted-types-for 'script'breaks them.
Origins per loading strategy
| Strategy | Element code from | Add to the base policy |
|---|---|---|
iife | The bundle host, https://proxy.pie-api.com/bundles/ unless loaderOptions.bundleHost names another | connect-src https://cdn.jsdelivr.net (MathJax 3 speech maps)font-src data: https://unpkg.com (MathJax 3 fonts) |
esm | https://cdn.jsdelivr.net, or loaderOptions.esmCdnUrl; with the esm.sh
provider, https://raw.esm.sh | connect-src and font-src data: list the CDN origin, which also
serves package metadata and MathJax assets |
preloaded | The host's own bundle | Nothing for current builds, which serve MathJax from their own output |
Tools and workers
| Source | Needs |
|---|---|
| Desmos and GeoGebra calculators | Their vendor scripts, which 'strict-dynamic' admits: www.desmos.com/api/v1.12/calculator.js and www.geogebra.org/apps/deployggb.js |
| Cortex calculator | data: fonts; 'self' in worker-src when the policy sets
it, and blob: when loaded from a CDN |
| Toolbar icons and fonts | Font 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 speech | blob: 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-src | IIFE bundle | Import map | Unlisted CDN import() |
|---|---|---|---|
'nonce-…' 'strict-dynamic' | loads | loads | loads |
'nonce-…' | refused | refused | loads |
'nonce-…' bundle host | loads | refused | loads |
'unsafe-inline' bundle host | loads | loads | refused |
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.