Skip to content

Instrumentation challenge's srcdoc iframe fails under strict CSP (needs nonce propagation) #229

@TheophileDiot

Description

@TheophileDiot

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

  1. 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.
  2. Relax to 'unsafe-inline' on script-src — works, but drops the nonce guarantee wholesale and weakens the parent page's XSS posture.
  3. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions