Skip to content

CSP Nonce breaks on inline styles with tailwind classes #760

Description

@brettwray

Environment

|                      |                                                                                                                                                                                                                                                         |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Operating system** | `macOS 23.3.0`                                                                                                                                                                                                                                          |
| **CPU**              | `Apple M3 Max (16 cores)`                                                                                                                                                                                                                               |
| **Node.js version**  | `v24.11.1`                                                                                                                                                                                                                                              |
| **nuxt/cli version** | `3.35.2`                                                                                                                                                                                                                                                |
| **Package manager**  | `pnpm@9.0.0`                                                                                                                                                                                                                                            |
| **Nuxt version**     | `4.4.7`                                                                                                                                                                                                                                                 |
| **Nitro version**    | `2.13.4`                                                                                                                                                                                                                                                |
| **Builder**          | `vite@7.3.5`                                                                                                                                                                                                                                            |
| **Config**           | `$development`, `$env`, `$production`, `alias`, `compatibilityDate`, `eslint`, `features`, `fonts`, `hooks`, `ignore`, `modules`, `pinia`, `routeRules`, `security`, `sourcemap`, `typescript`, `vite`                                                  |
| **Modules**          | `@pinia/nuxt@0.11.3`, `@pinia/colada-nuxt@0.2.6`, `@nuxt/eslint@1.15.2`, `pinia-plugin-persistedstate/nuxt@4.7.1`, `dayjs-nuxt@2.1.11`, `@nuxtjs/mdc@0.20.2`, `@nuxt/image@2.0.0`, `@comark/nuxt@0.4.0`, `nuxt-security@2.6.0`, `@nuxt/scripts@1.3.0`   |

Nuxt Security Version

v2.6.0

Default setup used?

Yes, the bug happens even if the security option is not customized

Security options

Reproduction

https://stackblitz.com/edit/nuxt-starter-5vmre8wj?file=app%2Fpages%2Fhome.vue

For reproduction, you need to use nuxt preview so it does the production build

Description

Description

When features.inlineStyles: true (Nuxt's built-in inline-CSS optimization) is combined with nuxt-security's CSP nonce injection (nonce: true, the default), a Tailwind CSS arbitrary-value class selector containing an escaped quote (e.g. bg-[url('/image.svg')], rendered in the compiled CSS as .bg-\[url\(\'\/image\.svg\'\)\]) causes the nonce injector to silently drop the nonce attribute from every <script> and <link> tag that follows it in the same HTML section, up until a later quote happens to rebalance the count. This includes the entry <script type="module"> and every <link rel="modulepreload"> for the app's own bundle.

Under an enforced CSP with script-src 'strict-dynamic' 'nonce-...', a nonce-less entry script is a hard block per spec. Firefox enforces this and the page never hydrates. Chrome appears to tolerate it in some cases, which let this slip through initially.

Root cause

runtime/nitro/plugins/40-cspSsrNonce.js's injectNonceToTags() masks quoted substrings before looking for tags to nonce:

const QUOTE_MASK_RE = /"([^"\\]*(?:\\.[^"\\]*)*)"/g;
This regex is designed to skip over legitimate double-quoted attribute values so the tag-matching regexes (SCRIPT_RE/LINK_RE) don't get confused by quotes inside them. But it operates on raw literal " characters in the string, with no awareness of why a " is there. Tailwind's arbitrary-value class selectors escape characters like (, ), '/", / for CSS selector syntax (e.g. " or ' as a literal escaped character in a selector name), not as a JS/HTML string-escape sequence. When one of these CSS-escaped quote characters appears in an inlined <style> block, it's still just a bare " (or ', when single-quoted arbitrary values are used) character to the regex — it gets treated as a real string delimiter, gets paired with whatever the next " in the document happens to be (which may be far away, inside an entirely unrelated attribute), and everything in between - including real <script>/ tags - is masked out as "inside a string" and never reaches the nonce-injection step.

Because the masking is based on quote-counting parity rather than actual tag boundaries, the corruption isn't necessarily total: it corrupts a span of the document (from the offending selector to wherever the count next rebalances), so some tags before and after the corrupted span do get their nonce while everything inside it doesn't.

Actual behavior

The entry <script type="module"> and one or more tags are emitted without a nonce attribute, even though nonce: true is enabled and other tags on the same page do have a correct, matching nonce.

Example from the reproduction:

`<script integrity="sha384-..." type="module" src="/_nuxt/CJANYYeR.js" crossorigin></script>

` <script nonce="b1hgNGHYqikGFF4Tgi5RG+5E">"use strict";... The <style> tag earlier in has the correct nonce; the entry script/modulepreload links in the middle don't; a later inline script does again.

Expected behavior

Every <script>/ tag nonuce-eligible tag should receive the request's nonce, regardless of what CSS content (including CSS-escaped quote characters from utility-class frameworks like Tailwind) appears earlier in the same HTML section. Nonce injection should be based on actual tag/attribute parsing (or at least quote-masking that understands CSS escape sequences aren't string delimiters), not a context-free quote-counting regex that can be desynced by content that was never a JS/HTML string in the first place.

Additional context

Attaching the page source from the reproduction to save time

sample-page-source.html

Logs

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions