what problem would this feature solve, if any?
When a site embedding the Cap.js widget uses a strict nonce-based CSP (e.g. script-src 'nonce-<random>' https://cdn.jsdelivr.net 'wasm-unsafe-eval'), the proof-of-work flow works — but any sitekey with instrumentation challenges enabled fails at the instrumentation step.
The widget implements that check by creating a sandboxed iframe and injecting an inline <script> via srcdoc:
// widget.js (0.1.44, minified)
s = document.createElement("iframe");
s.setAttribute("sandbox", "allow-scripts");
// ...
s.srcdoc = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>'
+ '<script>' + i + '</script></body></html>';
document.body.appendChild(s);
The srcdoc iframe inherits the parent document's CSP (allow-scripts without allow-same-origin gives it an opaque origin, but per CSP spec it still inherits the policy). The injected inline <script> has no nonce attribute, so the browser blocks it:
about:srcdoc:1 Executing inline script violates the following Content Security Policy directive
'script-src 'nonce-YfhfZ3IQqF6IgIKSI7I15k0Koy385opE' <cap-url> 'wasm-unsafe-eval''.
Either the 'unsafe-inline' keyword, a hash, or a nonce is required to enable inline execution.
widget.js:1 [cap] Instrumentation failed
The only workaround today is adding 'unsafe-inline' to script-src on the parent page — which per CSP3 forces dropping the nonce entirely ('unsafe-inline' is ignored when a nonce is present). That's a meaningful defense-in-depth regression for integrators.
Related but not a duplicate: #129 covers the general CSP / nonce story and was resolved via window.CAP_CSS_NONCE plus CSP tuning. That reporter likely did not have instrumentation enabled, which is why they didn't hit this.
describe the solution you'd like
Expose a script nonce the same way window.CAP_CSS_NONCE exposes the style nonce:
<script nonce="{NONCE}">
window.CAP_CSS_NONCE = "{NONCE}";
window.CAP_SCRIPT_NONCE = "{NONCE}"; // proposed
</script>
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget" nonce="{NONCE}"></script>
When CAP_SCRIPT_NONCE is set, the widget stamps it on the inline <script> it writes inside the srcdoc:
s.srcdoc = '<!DOCTYPE html><html><head>...</head><body>'
+ '<script' + (window.CAP_SCRIPT_NONCE ? ` nonce="${window.CAP_SCRIPT_NONCE}"` : '')
+ '>' + i + '</script></body></html>';
That lets integrators keep nonce-only script-src and still use instrumentation. Same pattern as #129's resolution, extended to the instrumentation iframe.
describe alternatives you've considered
- Disable instrumentation on the sitekey — works, but requires every operator to know about this CSP subtlety, and instrumentation is on by default per the standalone quickstart.
- Relax to
'unsafe-inline' on script-src — works, but drops the nonce guarantee wholesale and weakens the parent page's XSS posture.
- Hash the srcdoc inline
<script> — can't; its content is generated per challenge, so no stable hash.
Nonce propagation seems like the cleanest fix and mirrors the CAP_CSS_NONCE pattern you already support.
additional context:
Environment where the bug reproduces:
- Cap standalone
tiago2/cap:3.0.4
- Widget version
0.1.44 (via WIDGET_VERSION=0.1.44, ENABLE_ASSETS_SERVER=true)
- Parent CSP on the challenge page:
script-src 'nonce-<n>' <cap-url> 'wasm-unsafe-eval'; style-src 'self' 'nonce-<n>'; connect-src <cap-url>; frame-src <cap-url>; worker-src blob: <cap-url>;
- Sitekey with instrumentation challenges enabled (default on new sitekeys).
Happy to help test a patch.
what problem would this feature solve, if any?
When a site embedding the Cap.js widget uses a strict nonce-based CSP (e.g.
script-src 'nonce-<random>' https://cdn.jsdelivr.net 'wasm-unsafe-eval'), the proof-of-work flow works — but any sitekey with instrumentation challenges enabled fails at the instrumentation step.The widget implements that check by creating a sandboxed iframe and injecting an inline
<script>viasrcdoc:The srcdoc iframe inherits the parent document's CSP (
allow-scriptswithoutallow-same-origingives it an opaque origin, but per CSP spec it still inherits the policy). The injected inline<script>has no nonce attribute, so the browser blocks it:The only workaround today is adding
'unsafe-inline'toscript-srcon the parent page — which per CSP3 forces dropping the nonce entirely ('unsafe-inline'is ignored when a nonce is present). That's a meaningful defense-in-depth regression for integrators.Related but not a duplicate: #129 covers the general CSP / nonce story and was resolved via
window.CAP_CSS_NONCEplus CSP tuning. That reporter likely did not have instrumentation enabled, which is why they didn't hit this.describe the solution you'd like
Expose a script nonce the same way
window.CAP_CSS_NONCEexposes the style nonce:When
CAP_SCRIPT_NONCEis set, the widget stamps it on the inline<script>it writes inside the srcdoc:That lets integrators keep nonce-only
script-srcand still use instrumentation. Same pattern as #129's resolution, extended to the instrumentation iframe.describe alternatives you've considered
'unsafe-inline'on script-src — works, but drops the nonce guarantee wholesale and weakens the parent page's XSS posture.<script>— can't; its content is generated per challenge, so no stable hash.Nonce propagation seems like the cleanest fix and mirrors the
CAP_CSS_NONCEpattern you already support.additional context:
Environment where the bug reproduces:
tiago2/cap:3.0.40.1.44(viaWIDGET_VERSION=0.1.44,ENABLE_ASSETS_SERVER=true)script-src 'nonce-<n>' <cap-url> 'wasm-unsafe-eval'; style-src 'self' 'nonce-<n>'; connect-src <cap-url>; frame-src <cap-url>; worker-src blob: <cap-url>;Happy to help test a patch.