Security

Chameleon runs under a strict Content-Security-Policy: no 'unsafe-inline', no 'unsafe-eval', and nothing loaded from another origin. This page gives two policies to start from, shows how to add Trusted Types to either, and lists what your framework and your license grant add.

What Chameleon asks of a page⁠

  • No inline styles or style elements. No component writes a style attribute or inserts a <style> element, so style-src-attr 'none' holds.
  • No eval. Nothing in Chameleon calls eval, new Function or a timer with a string. WebAssembly compilation is the one thing it needs beyond plain scripts, for its color functions, and 'wasm-unsafe-eval' allows that and nothing else.
  • No requests to other origins. Chameleon loads no font, image, script, or data from anywhere but your page. Fonts, images and APIs you add are yours to list.
  • No markup from strings. Chameleon builds its own content with DOM APIs and writes no string to an HTML sink, so it needs no HTML policy under Trusted Types.

Chameleon works under both policies below, with Trusted Types enforced, in Chromium, Firefox and Safari.

Choose a policy⁠

The two policies differ only in how scripts are admitted. Every other directive is the same in both.

Policy Admits scripts by Upkeep
Option A Origin: whatever your origin serves may run None when the bundle changes
Option B Their exact bytes, by hash A hash per script, recomputed on every build

Send the policy as a Content-Security-Policy response header, not a <meta> element: a <meta> policy ignores frame-ancestors. Then add your own sources to the directives they belong to.

Option A, by origin⁠

default-src 'none'; script-src 'self' 'wasm-unsafe-eval' blob:; worker-src blob:; style-src-elem 'self'; style-src-attr 'none'; img-src data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'
content_copy
Directive Sources Why
default-src 'none' Chameleon makes no connection and loads no font, frame, or media of its own.
script-src 'self' The bundle. If a CDN serves it, name that origin instead.
script-src 'wasm-unsafe-eval' The color functions, the color picker and theme generation compile WebAssembly on first use.
script-src blob: Chameleon creates blob: URLs from code inside its own bundle. Without blob:, component corner shapes and the color functions fail to load.
worker-src blob: The *Async color functions run in a worker. Refused, they still answer, on the main thread.
style-src-elem 'self' chameleon.css. If you don't link it, this can be 'none'.
style-src-attr 'none' No component writes a style attribute.
img-src data: A built-in image: a data table's warning for a cell whose value doesn't match its column.
base-uri 'none' Hardening. A Blazor app, which uses <base>, needs 'self'.
form-action 'self' A submit button, and Enter in a text field, submit their form. Add your form's action origin if it isn't your own.
frame-ancestors 'none' Hardening. List the origins that frame your app, if any do.

Do not drop blob: or 'wasm-unsafe-eval' to make the policy look stricter: each one is there because removing it breaks something.

Option B, by hash⁠

default-src 'none'; script-src 'sha256-<grant>' 'sha256-<bundle>' 'strict-dynamic' 'wasm-unsafe-eval'; style-src-elem 'self'; style-src-attr 'none'; img-src data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'
content_copy

Load the license grant and the bundle with integrity attributes that repeat the hashes in the policy:

<script src="/chameleon.grant.js" integrity="sha256-<grant>"></script>
<script type="module" src="/chameleon-components-web.js" integrity="sha256-<bundle>"></script>
content_copy

Each hash is sha256- followed by the base64 SHA-256 of the file's exact bytes, which is also its Subresource Integrity value:

openssl dgst -sha256 -binary chameleon-components-web.js | openssl base64 -A
content_copy

Compute both from the files you deploy, in the same build step that writes the page. If a hash no longer matches its file, the browser refuses the script, and no component registers.

What changes from Option A:

  • A hash per script. Every script your markup loads, inline or not, needs its hash in script-src. The browser refuses a script without one, even from your own origin, and its integrity refuses a file whose bytes changed after the hash was taken.
  • 'strict-dynamic' admits what those trusted scripts create while the page runs, which is how Chameleon's own blob: scripts and its color worker get through. It also makes the browser ignore 'self', hosts, and schemes in script-src, so Option A's 'self' and blob: are gone.
  • worker-src is omitted. Workers then follow script-src and its 'strict-dynamic'. Never write 'strict-dynamic' into worker-src: Safari rejects it there and refuses the worker.
  • 'wasm-unsafe-eval' stays, because 'strict-dynamic' doesn't cover WebAssembly.

Loading your own bundle under Option B⁠

Skip this unless your app's own bundle imports Chameleon.

If your application bundle imports Chameleon, rather than loading it by a tag of its own, list every module up front:

  • Put every module the page imports in the markup, each with its hash and integrity: the bundle as <script type="module" src integrity>, and each chunk as <link rel="modulepreload" href integrity>. Chromium and Firefox refuse a module that a script in the markup imports without one. Safari allows it, so a page tested only in Safari hides the problem.
  • To load the components later, preload the bundle with <link rel="modulepreload" href integrity> and import() it when you need it.
  • In a Blazor app, give blazor.web.js, the grant and the bundle their integrity and hashes, and preload each JavaScript module Blazor imports, such as Chameleon.Components.Blazor.lib.module.js, with <link rel="modulepreload" href integrity>. <ImportMap /> renders the import map as an inline script, so add its hash to script-src.

Older browsers⁠

A browser that predates 'strict-dynamic' reads the rest of script-src as written. To keep one working, append Option A's script sources, 'self' blob:, to Option B's script-src: a browser that supports 'strict-dynamic' ignores them.

Trusted Types, on either option⁠

Trusted Types make every DOM sink that turns a string into markup or a script, such as innerHTML or a script's src, refuse a plain string. Append this to either policy:

require-trusted-types-for 'script'; trusted-types chameleon-pw chameleon-color chameleon-lit
content_copy
Policy What it admits
chameleon-pw Only a blob: URL Chameleon has just created from code in its own bundle, once.
chameleon-color The same, for the color functions' scripts and worker.
chameleon-lit Chameleon's own template markup. If your page also loads Lit, see the next bullets.
  • Chameleon claims all three names as its bundle loads, so a script that runs later can't create a policy under them. A script that runs before the bundle could, so under Option B admit by hash only scripts you would trust with them.
  • If your page loads its own Lit, add lit-html after Chameleon's three names. Do not add 'allow-duplicates': it would let any script create a second policy under Chameleon's names.
  • Add your own policy names, and your framework's, after Chameleon's.

What your framework adds⁠

Framework Policy names to add
React 19 None
Blazor (ASP.NET Core 10) default. Blazor writes markup to innerHTML as plain strings and creates no policy of its own.

A Blazor app needs a default policy, created before blazor.web.js runs, whose createHTML returns its input. Give it no createScript and no createScriptURL, so script sinks stay closed. It reopens every HTML sink on the page to strings, which is the cost of running Blazor under Trusted Types.

Content you hand a component⁠

A toast's content is yours. Pass showToast a DOM node, or a TrustedHTML value from your own policy. On a page that enforces Trusted Types, the browser refuses a string of markup unless your own default policy admits it. The Content-Security-Policy refuses an inline style or event handler in it either way.

What you add⁠

  • The license grant. The grant tooling's Vite plugin writes the grant as an inline script, which both policies refuse. Without the grant, the components render watermarked and disabled. Either add the hash of that script's exact text to script-src, recomputed every build, or write the grant to a file with chameleon-grant stamp --out <directory> and load it with <script src> before the bundle. Option A's 'self' admits that file. Under Option B it carries its hash and integrity.
  • Fonts. Self-hosted fonts need font-src 'self'. Material Symbols from Google Fonts needs style-src-elem https://fonts.googleapis.com and font-src https://fonts.gstatic.com.
  • Images and data. An image URL you give a component, such as an avatar, goes in img-src, and an API your app calls goes in connect-src.

Rolling a policy out⁠

Send the policy first as Content-Security-Policy-Report-Only, with a report-to or report-uri endpoint. The browser reports what it would block and blocks nothing, so you can find your own app's additions without breaking a page. Switch the header to Content-Security-Policy once the reports are clean. Expect reports from browser extensions, which inject their own scripts and styles; they aren't your page's.