MAISON CODE .
/ Security · Hydrogen · Remix · OWASP · Architecture

CSP Guide: Locking Down Shopify Hydrogen

Cross-Site Scripting (XSS) is the #1 threat to headless commerce. A technical guide to implementing Strict CSP with Nonces in Remix and Hydrogen.

AB
Alex B.
CSP Guide: Locking Down Shopify Hydrogen

In e-commerce, Trust is the only currency. If a customer types their credit card number into your site, they trust you to secure it. But if you have a Cross-Site Scripting (XSS) vulnerability, an attacker can inject a script that reads keys strokes. The attacker gets the credit card. You get the lawsuit.

The defense against XSS is the Content Security Policy (CSP). It is a whitelist. It tells the browser: “Only load scripts from these domains. Block everything else.”

Implementing CSP in a Single Page App (SPA) like Shopify Hydrogen (Remix) is notoriously difficult because of Hydration and Third-Party Tags. This is the definitive guide to doing it right.

1. The Strategy: Strict CSP with Nonces

In the old days, we used “Domain Whitelisting”. script-src 'self' https://google-analytics.com https://facebook.com This is insecure. If Google Analytics has an Open Redirect vulnerability, the attacker can bypass your CSP.

The modern standard is Nonces (Number used ONCE).

  1. The Server generates a random cryptographic token (abc123yz) for every request.
  2. The Server adds a header: Content-Security-Policy: script-src 'nonce-abc123yz'.
  3. The HTML includes the token: <script nonce="abc123yz">.

If an attacker injects <script>alert(1)</script>, it has no nonce. The browser blocks it.

2. Implementation in Remix (Hydrogen)

In Remix, we need to generate the nonce in the root loader and pass it down.

Step 1: Generate Nonce in entry.server.tsx

// app/entry.server.tsx
import { generateNonce } from './utils'; // crypto.randomBytes(16).toString('base64')

export default function handleRequest(request, responseStatusCode, responseHeaders, remixContext) {
  const nonce = generateNonce();
  
  // Define Directives
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' https://cdn.shopify.com https://challenges.cloudflare.com`,
    "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", // unsafe-inline needed for CSS-in-JS usually
    "img-src 'self' data: https://cdn.shopify.com",
    "connect-src 'self' https://monorail-edge.shopifysvc.com https://www.google-analytics.com",
    "frame-ancestors 'none'", // Anti-Clickjacking
  ].join('; ');

  responseHeaders.set('Content-Security-Policy', csp);

  // Pass nonce to context so <Scripts /> can use it
  return <RemixServer context={remixContext} url={request.url} nonce={nonce} />;
}

Step 2: Attach Nonce to Scripts in root.tsx

// app/root.tsx
import { useNonce } from '@shopify/hydrogen';

export default function App() {
  const nonce = useNonce();

  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration nonce={nonce} />
        <Scripts nonce={nonce} />
      </body>
    </html>
  );
}

Now, every script tag generated by Remix will legally have the nonce.

3. The “Report Only” Fail-Safe

Deploying a CSP is scary. If you forget a domain (e.g., fonts.gstatic.com), your site breaks. Solution: Report-Only Mode.

  1. Set header Content-Security-Policy-Report-Only.
  2. The browser will load the resources but log the violation to the console (and an endpoint).
  3. Run this for 2 weeks. Monitor logs.
  4. Once logs are clean, switch to enforcing mode.

4. Reporting: Using Sentry

Reading CSP violations in the console is useless for production users. You need a collection endpoint. Sentry (and Datadog) support CSP Reporting key-values. report-uri https://o450.ingest.sentry.io/api/.../security/?sentry_key=...; When a user in Brazil triggers a CSP violation (maybe a malware extension), Sentry alerts you. Noise Filtering: You will see a lot of noise from Browser Extensions (LastPass causes violations). Ignore moz-extension and chrome-extension schemes. Focus on http injections.

5. Subresource Integrity (SRI)

What if the CDN gets hacked? If you load https://code.jquery.com/jquery.min.js, and a hacker modifies that file on the CDN, they bypass your CSP (because the domain is whitelisted). Fix: Use SRI. <script src="..." integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..." crossorigin="anonymous"></script> The browser hashes the downloaded file. If it doesn’t match the integrity attribute, it refuses to execute. This guarantees that code cannot change without you deploying a new HTML file.

6. Third-Party Hell (Google Tag Manager)

Marketing loves GTM. Security hates GTM. GTM injects scripts dynamically. Does GTM propagate the nonce? Yes, if configured correctly.

In your GTM snippet, you must inspect the nonce of the parent script and pass it forward. However, most third-party pixels (Facebook) are not nonce-aware. Compromise: You often have to whitelist their domains in script-src in addition to the nonce.

script-src 'self' 'nonce-...' https://connect.facebook.net ...

5. Preventing Clickjacking (Frame Ancestors)

Clickjacking is when an attacker embeds your site in an <iframe> on their site (evil.com). They put an invisible “Win iPhone” button on top of your “Buy Now” button. The user thinks they are clicking “Win”, but they are clicking “Buy”.

Fix: frame-ancestors 'none'. This prohibits anyone from iframe-ing your site. If you need to be iframed (e.g., by a partner), whitelist them: frame-ancestors 'self' https://partner.com.

6. CSP Violated? What happens?

When a CSP is violated, the browser throws a simplified error in the console. Refused to load script from 'http://evil.com/hack.js' because it violates the following Content Security Policy directive: "script-src 'self' ...".

For the user, the malicious script simply fails to execute. The rest of the site works fine. The attack is neutralized silently.

7. Trusted Types (The Future of XSS Defense)

CSP blocks loading malicious scripts. Trusted Types blocks writing malicious code. It forces you to sanitize strings before they touch the DOM. element.innerHTML = dirtyString; -> Blocked Browser Error. You must wrap it: element.innerHTML = DOMPurify.sanitize(dirtyString); This creates a “Policy” object. The browser guarantees that only Policy-Created strings can touch the DOM. It destroys an entire class of DOM-based XSS vulnerabilities.

8. Web Worker Security

Workers (Service Workers, Web Workers) are isolated. But they can still be dangerous (Crypto Mining). You must restrict them via specific directives:

  • worker-src 'self' blob:;: Only allow workers from your domain.
  • child-src 'self': Restricts iframes and workers. Attackers love to spin up a Worker in the background to DDOS other sites or mine Bitcoin. Your CSP stops this.

10. CSP for WebAssembly (WASM)

If you use WASM (e.g., for resizing images client-side), you need special directives. script-src 'unsafe-eval' (For WASM compilation) is dangerous. Modern browsers support: script-src 'wasm-unsafe-eval'. This allows WASM to compile without opening the door to JavaScript eval(). It keeps the “Bad Good” parts separate from the “Bad Bad” parts.

11. The Nonce Lifecycle (Request Scope)

A Nonce is useless if it is static. If you hardcode nonce="123" in your HTML, an attacker just injects <script nonce="123">. The Nonce MUST be generated per request. Remix Pattern:

  1. entry.server.tsx: Generate nonce = crypto.randomUUID().
  2. Pass to <RemixServer nonce={nonce}>.
  3. Hydrate client with <Scripts nonce={nonce}>. This ensures that if I refresh the page, I get a new nonce. The attacker cannot guess it.

12. Styles vs Scripts (‘unsafe-inline’)

We often allow style-src 'unsafe-inline'. Why? Because CSS-in-JS libraries (Emotion, Styled Components) inject <style> tags at runtime. Is this a vulnerability? Technically, yes (CSS Exfiltration). But it is much lower risk than XSS. If you can, use a library like Vanilla Extract (Zero-Runtime CSS) which generates static .css files. Then you can remove 'unsafe-inline' and achieve CSP Nirvana.

Why Maison Code Discusses This

At Maison Code, we believe that Security is Reputation. A hack doesn’t just cost money; it costs Trust. We don’t settle for “It works”. We demand “It is secure”. We implement Strict Crypto-Nonce CSPs for every client. We monitor the violations (Sentry) and patch holes before they are exploited. We sleep well at night because we know the browser is enforcing our rules.

12. Conclusion

A strict CSP is the hallmark of a mature engineering team. It shows you understand the hostile environment of the web. It protects your customers from Magecart, keyloggers, and data exfiltration. It is difficult to set up, but mandatory for any brand capturing payment data. Don’t wait for the breach. Lock the door now.


Is your store vulnerable?

We perform Penetration Testing and CSP implementation for Shopify Plus brands.

Hire our Architects.