Skip to main content

Add a content security policy

Note

This guide might not be compatible with features introduced in Hydrogen version 2025-05 and above. Check the latest documentation if you encounter any issues.

A content security policy (CSP) adds an important layer of security to your web app by helping to mitigate cross-site scripting and data injection attacks. It enforces what content is loaded in your app. This includes images, CSS, fonts, scripts, network requests, and more.

This guide describes how you can set up and customize a CSP for your site.


In this tutorial, you'll learn how to do the following tasks:

  • Setup a Content Security Policy on an existing Hydrogen app.
  • Define custom directives within your Content Security Policy.
  • Secure third party scripts with a Content Security Policy nonce.

  • You've completed the Hydrogen Getting Started with a Hello World example project.

Anchor to Step 1: Set up a content security policyStep 1: Set up a content security policy

Hydrogen provides a default content security policy. Add the content security policy by using the createContentSecurityPolicy utility within your entry.server.jsx file which returns:

  • nonce: Pass this value to React's renderToReadableStream or any other custom component that renders a script under the hood.

  • NonceProvider: This makes the nonce available throughout the app, and renders it by wrapping RemixServer.

  • header: This is the actual content security policy header value. Add it to your app response headers.

    Your updated entry.server.jsx should look something like the following:

File

/app/entry.server.jsx

import {RemixServer} from '@remix-run/react';
import isbot from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';

export default async function handleRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
) {
// Create the Content Security Policy
const {nonce, header, NonceProvider} = createContentSecurityPolicy();

const body = await renderToReadableStream(
// Wrap the entire app in the nonce provider
<NonceProvider>
<RemixServer context={remixContext} url={request.url} />
</NonceProvider>,
{
// Pass the nonce to react
nonce,
signal: request.signal,
onError(error) {
// eslint-disable-next-line no-console
console.error(error);
responseStatusCode = 500;
},
},
);

if (isbot(request.headers.get('user-agent'))) {
await body.allReady;
}

responseHeaders.set('Content-Type', 'text/html');
// Add the CSP header
responseHeaders.set('Content-Security-Policy', header);

return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
import type {EntryContext} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import isbot from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
// Create the Content Security Policy
const {nonce, header, NonceProvider} = createContentSecurityPolicy();
const body = await renderToReadableStream(
// Wrap the entire app in the nonce provider
<NonceProvider>
<RemixServer context={remixContext} url={request.url} />
</NonceProvider>,
{
// Pass the nonce to react
nonce,
signal: request.signal,
onError(error) {
// eslint-disable-next-line no-console
console.error(error);
responseStatusCode = 500;
},
},
);

if (isbot(request.headers.get('user-agent'))) {
await body.allReady;
}

responseHeaders.set('Content-Type', 'text/html');
// Add the CSP header
responseHeaders.set('Content-Security-Policy', header);

return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}

Anchor to Step 2: Add a nonce to your scriptsStep 2: Add a nonce to your scripts

If you start your app's server, you'll notice a lot of errors in the console and scripts will fail to load. This is because the nonce value also needs to be passed to each script in the app.

  1. Update root.jsx to use the useNonce() hook and pass the value to the ScrollRestoration, Scripts, and LiveRelod components. Make sure to also update the error boundary:

File

/app/root.jsx

import {useNonce} from '@shopify/hydrogen';

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

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...data}>
<Outlet />
</Layout>
{/** Pass the nonce to all components that generate a script **/}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}

export function ErrorBoundary() {
const error = useRouteError();
const [root] = useMatches();
const nonce = useNonce();
let errorMessage = "Unknown error";
let errorStatus = 500;

if (isRouteErrorResponse(error)) {
errorMessage = error?.data?.message ?? error.data;
errorStatus = error.status;
} else if (error instanceof Error) {
errorMessage = error.message;
}

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...root.data}>
<div className="route-error">
<h1>Oops</h1>
<h2>{errorStatus}</h2>
{errorMessage && (
<fieldset>
<pre>{errorMessage}</pre>
</fieldset>
)}
</div>
</Layout>
{/** Make sure to remember to pass the nonce to components within the ErrorBoundary **/}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}
import {useNonce} from '@shopify/hydrogen';

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

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...data}>
<Outlet />
</Layout>
{/** Pass the nonce to all components that generate a script **/}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}

export function ErrorBoundary() {
const error = useRouteError();
const [root] = useMatches();
const nonce = useNonce();
let errorMessage = "Unknown error";
let errorStatus = 500;

if (isRouteErrorResponse(error)) {
errorMessage = error?.data?.message ?? error.data;
errorStatus = error.status;
} else if (error instanceof Error) {
errorMessage = error.message;
}

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...root.data}>
<div className="route-error">
<h1>Oops</h1>
<h2>{errorStatus}</h2>
{errorMessage && (
<fieldset>
<pre>{errorMessage}</pre>
</fieldset>
)}
</div>
</Layout>
{/** Make sure to remember to pass the nonce to components within the ErrorBoundary **/}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}
  1. If you use any third-party scripts, then use the Script component which automatically attaches the nonce:

File

/app/entry.server.jsx

import {useNonce, Script} from '@shopify/hydrogen';

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

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...data}>
<Outlet />
</Layout>
{/** The Script component automatically adds a nonce **/}
<Script src="https://some-custom-script.js" />
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}
import {useNonce, Script} from '@shopify/hydrogen';

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

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout {...data}>
<Outlet />
</Layout>
{/** The Script component automatically adds a nonce **/}
<Script src="https://some-custom-script.js" />
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}

Anchor to Step 3: Customize the content security policyStep 3: Customize the content security policy

You can extend the default CSP generated by Hydrogen by passing custom directives into createContentSecurityPolicy. Refer to the content security policy reference for a description of available directives.

File

/app/entry.server.jsx

const {nonce, header, NonceProvider} = createContentSecurityPolicy({
styleSrc: [
"'self'",
'https://cdn.shopify.com',
'https://some-custom-css.cdn',
],
});
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
styleSrc: [
"'self'",
'https://cdn.shopify.com',
'https://some-custom-css.cdn',
],
});

Was this page helpful?