Description
When using svelte-clerk (or any non-Next.js Clerk integration) with a nonce-based Content Security Policy, clerk.browser.js creates inline <script> elements at runtime that don't carry the nonce attribute. These scripts are blocked by the browser, preventing Clerk from initializing.
Setup:
SvelteKit app with svelte-clerk v1.1.0 (@clerk/shared v4.3.0)
Nonce generated per request, passed to
CSP header: script-src 'nonce-{uuid}' 'strict-dynamic'
What works:
The initial clerk.browser.js and ui.browser.js script tags load correctly — the nonce prop is applied via loadClerkJSScript in @clerk/shared
What breaks:
After clerk.browser.js loads, it creates inline <script> elements at runtime (visible at (index):60 and (index):61 in the browser)
These inline scripts don't have the nonce attribute
strict-dynamic only propagates trust to dynamically-loaded scripts with src attributes, not inline scripts (per CSP spec)
Result: Clerk fails to initialize, auth state is unavailable on the client
Browser console output:
Executing inline script violates the following Content Security Policy directive 'script-src 'nonce-xxx' 'strict-dynamic''. Either the 'unsafe-inline' keyword, a hash, or a nonce is required to enable inline execution. The action has been blocked.
Expected behavior:
clerk.browser.js should read the nonce from its own <script> element (via document.currentScript.nonce or the nonce attribute set by @clerk/shared) and apply it to any inline <script> elements it creates at runtime. This would make nonce-based strict CSP work across all frameworks, not just Next.js.
Current workaround:
Using script-src 'self' 'unsafe-inline' https://{fapi-domain} — which defeats the purpose of nonce-based CSP and is flagged in enterprise security audits (SOC 2, PCI DSS).
Context:
The Next.js integration solves this via the dynamic prop on ClerkProvider and automatic nonce threading through clerkMiddleware. Non-Next.js frameworks (SvelteKit, Remix, Nuxt, etc.) don't have this plumbing and rely on manually passing the nonce prop, which only reaches the initial script load — not the runtime inline scripts created by clerk.browser.js.
Suggested fix:
In packages/clerk-js, when creating inline script elements at runtime, propagate the nonce from the parent script element. The nonce is already available on the <script> tag that loaded clerk.browser.js.
Environment
System:
OS: Linux 6.6 Ubuntu 24.04.3 LTS 24.04.3 LTS (Noble Numbat)
CPU: (16) x64 AMD Ryzen 7 9800X3D 8-Core Processor
Memory: 24.08 GB / 30.14 GB
Container: Yes
Shell: 5.2.21 - /bin/bash
Binaries:
Node: 22.22.0 - /home/monge/.nvm/versions/node/v22.22.0/bin/node
npm: 10.9.4 - /home/monge/.nvm/versions/node/v22.22.0/bin/npm
Browsers:
Chromium: 146.0.7680.80
npmPackages:
@sveltejs/adapter-auto: ^7.0.1 => 7.0.1
@sveltejs/kit: ^2.55.0 => 2.55.0
@sveltejs/vite-plugin-svelte: ^7.0.0 => 7.0.0
svelte: ^5.55.0 => 5.55.0
svelte-clerk: ^1.1.1 => 1.1.1
typescript: ^5.9.3 => 5.9.3
vite: ^8.0.2 => 8.0.2
Reproduction
https://github.com/mongeaumichel/sveltekit-clerk
Publishable key
pk_test_YmVjb21pbmctd2FsbGV5ZS03My5jbGVyay5hY2NvdW50cy5kZXYk
Description
When using svelte-clerk (or any non-Next.js Clerk integration) with a nonce-based Content Security Policy, clerk.browser.js creates inline <script> elements at runtime that don't carry the nonce attribute. These scripts are blocked by the browser, preventing Clerk from initializing.
Setup:
SvelteKit app with svelte-clerk v1.1.0 (@clerk/shared v4.3.0)
Nonce generated per request, passed to
CSP header: script-src 'nonce-{uuid}' 'strict-dynamic'
What works:
The initial clerk.browser.js and ui.browser.js script tags load correctly — the nonce prop is applied via loadClerkJSScript in @clerk/shared
What breaks:
After clerk.browser.js loads, it creates inline <script> elements at runtime (visible at (index):60 and (index):61 in the browser)
These inline scripts don't have the nonce attribute
strict-dynamic only propagates trust to dynamically-loaded scripts with src attributes, not inline scripts (per CSP spec)
Result: Clerk fails to initialize, auth state is unavailable on the client
Browser console output:
Executing inline script violates the following Content Security Policy directive 'script-src 'nonce-xxx' 'strict-dynamic''. Either the 'unsafe-inline' keyword, a hash, or a nonce is required to enable inline execution. The action has been blocked.
Expected behavior:
clerk.browser.js should read the nonce from its own <script> element (via document.currentScript.nonce or the nonce attribute set by @clerk/shared) and apply it to any inline <script> elements it creates at runtime. This would make nonce-based strict CSP work across all frameworks, not just Next.js.
Current workaround:
Using script-src 'self' 'unsafe-inline' https://{fapi-domain} — which defeats the purpose of nonce-based CSP and is flagged in enterprise security audits (SOC 2, PCI DSS).
Context:
The Next.js integration solves this via the dynamic prop on ClerkProvider and automatic nonce threading through clerkMiddleware. Non-Next.js frameworks (SvelteKit, Remix, Nuxt, etc.) don't have this plumbing and rely on manually passing the nonce prop, which only reaches the initial script load — not the runtime inline scripts created by clerk.browser.js.
Suggested fix:
In packages/clerk-js, when creating inline script elements at runtime, propagate the nonce from the parent script element. The nonce is already available on the <script> tag that loaded clerk.browser.js.
Environment
Reproduction
https://github.com/mongeaumichel/sveltekit-clerk
Publishable key
pk_test_YmVjb21pbmctd2FsbGV5ZS03My5jbGVyay5hY2NvdW50cy5kZXYk