Skip to content

Serve the detector from an https URL and load it with a script tag instead of a blob import #3192

Description

@forgetso

Summary

The detector is currently loaded by importing it from a blob: URL. That choice causes two independent, silent failure modes for integrators. Serving it from a regular https:// URL and loading it with an injected <script type="module"> removes both, and leaves integrators with a stricter CSP than we ask for today.

Problem 1 — CSP scheme matching

A script-src wildcard matches neither blob: nor data:. Policies frequently permit one scheme and not the other, so on those origins the import is blocked outright.

Our only remedy today is to ask the integrator to add blob: or data: to script-src. Both are broad, and data: in particular is a well-known XSS amplifier — any injection on the page can then execute arbitrary script. A competent security review should push back on that request, and they do.

Problem 2 — bundler rewriting

A dynamic import() with a non-literal specifier is rewritten by webpack into a context module that cannot resolve a blob or data URL at runtime. Avoiding that requires both /* @vite-ignore */ and /* webpackIgnore: true */ on the import.

Those are comments, and removeComments in the shared build config strips them from dist/, so anyone bundling us from npm receives a bare import(url) regardless. Disabling removeComments is not an option — it would emit source comments into shipped output.

Impact

Both failures are silent. The load throws, the widget falls through to its no-detector path, and the request carries no detection result. The session is then served a challenge it cannot pass frictionlessly. The only client-side trace is a single console.error, and nothing on the wire records why.

Proposal

Serve the script from an https:// URL on the provider and load it with an injected <script type="module" src>.

  • CSP becomes an origin allowlist. A named origin is an ordinary, easily-approved CSP entry and materially safer for the integrator than blob: or data:. We stop asking people to weaken their policy.
  • The bundler problem disappears permanently. A script tag contains no import expression, so there is nothing for webpack or Vite to rewrite, and removeComments can stay on.
  • We get a failure reason. A script tag has onerror, so we can finally report why loading failed instead of silently proceeding without a detection result.

Work required

  • New provider endpoint serving the script, keyed per session. Needs Content-Type: text/javascript and CORS headers — module scripts and import() both require CORS cross-origin, a classic script would not.
  • The script must self-register rather than rely on a module default export, since a script tag cannot return a value. The loader plants a resolver ahead of injection and the script calls it. Emit both shapes during a compatibility window so existing widget versions keep working.
  • Add an optional URL field to the assign response alongside the existing inline script, and have the client prefer the URL and fall back to the current path. Old widgets ignore the unknown field, so no lockstep between widget and provider releases.

Tradeoffs / open questions

  • One extra round trip, since the script is currently returned inline. No meaningful caching benefit to offset it.
  • Wildcard vs single host. The script is served by whichever provider handled the session, so the allowlist would need to cover them all. Fronting it behind a single hostname would reduce that to one CSP entry — worth deciding as part of the design.
  • Classic script + IIFE would avoid the CORS requirement entirely, but is a larger build change given the current module format.
  • Exposure implications of a fetchable URL need to be worked through as part of refinement.

Related

PR #3191 adds a data: fallback to the existing path. It is a stopgap for policies that allow data: but not blob:, and does not address either root cause.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions