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
styleattribute or inserts a<style>element, sostyle-src-attr 'none'holds. - No
eval. Nothing in Chameleon callseval,new Functionor 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'
| 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'
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>
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
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 itsintegrityrefuses 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 ownblob:scripts and its color worker get through. It also makes the browser ignore'self', hosts, and schemes inscript-src, so Option A's'self'andblob:are gone.worker-srcis omitted. Workers then followscript-srcand its'strict-dynamic'. Never write'strict-dynamic'intoworker-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>andimport()it when you need it. - In a Blazor app, give
blazor.web.js, the grant and the bundle theirintegrityand hashes, and preload each JavaScript module Blazor imports, such asChameleon.Components.Blazor.lib.module.js, with<link rel="modulepreload" href integrity>.<ImportMap />renders the import map as an inline script, so add its hash toscript-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
| 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-htmlafter 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 withchameleon-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 andintegrity. - Fonts. Self-hosted fonts need
font-src 'self'. Material Symbols from Google Fonts needsstyle-src-elem https://fonts.googleapis.comandfont-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 inconnect-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.