Understanding CSPs through Hydrogen

Understanding CSPs through Hydrogen

Content Security Policy (CSP) is a crucial tool in web development that can make or break your site’s security. If you’ve spent time debugging font loading issues or mysterious errors while working with 3rd party imports, you’ve likely bumped into CSP issues.

In this post, I will break down what a CSP is, how it works in Hydrogen (can translate to any other framework), and how you can adjust it to fit your specific needs using a real-world example from my own projects.


Note: Hydrogen is Shopify's React framework, built on top of Remix


What is Content Security Policy (CSP)?

Content Security Policy (CSP) is a feature that protects your site cyber attacks like Cross-Site Scripting (XSS) and data injection. Essentially, CSP allows you to define sources from which your site can load resources; such as scripts, styles, and fonts.

By setting up a CSP, you tell the browser exactly which sources are trusted, and the browser will block anything not explicitly allowed. This is extremely useful in securing yourself against unauthorized scripts/styles from executing on your site.


How CSP Works

CSP works by using HTTP headers to specify the policy. The browser reads this policy and applies it to all resources loaded by your site. If a resource comes from a source not specified in your CSP, it will be blocked, and you might see errors in the console like:

Refused to load the font 'https://fonts.gstatic.com/s/opensans/v40/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6FxZCJgvAQ.woff2' because it violates the following Content Security Policy directive: "font-src 'self' https://fonts.googleapis.com https://maxcdn.bootstrapcdn.com https://use.typekit.net".

This is the browser telling you that your current CSP is too restrictive for the resources you’re trying to load.


Using CSP in Shopify Hydrogen

In Hydrogen, you’ll typically control the CSP in entry.server.jsx or server.jsx.

Hydrogen is a React-based framework for building headless Shopify storefronts, and managing security in such environments is critical.

Here's a glimpse into how CSP is typically set up in Hydrogen:

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

export default async function handleRequest(
  request,
  responseStatusCode,
  responseHeaders,
  remixContext,
) {
  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    styleSrc: [
      "'self'",
      "'unsafe-inline'",
      'https://cdn.shopify.com',
      'https://cdnjs.cloudflare.com',
      'https://fonts.googleapis.com',
      'https://maxcdn.bootstrapcdn.com',
      'https://use.typekit.net',
      'https://p.typekit.net', // Added for Adobe Fonts
    ],
    fontSrc: [
      "'self'",
      'https://fonts.googleapis.com',
      'https://maxcdn.bootstrapcdn.com',
      'https://use.typekit.net',
      'https://fonts.gstatic.com', // Added for Google Fonts
    ],
  });

  const body = await renderToReadableStream(
    <NonceProvider>
      <CartProvider>
        <RemixServer context={remixContext} url={request.url} />
      </CartProvider>
    </NonceProvider>,
    {
      nonce,
      signal: request.signal,
      onError(error) {
        console.error(error);
        responseStatusCode = 500;
      },
    },
  );

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

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

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

In this example, CSP is generated using the createContentSecurityPolicy function. The styleSrc and fontSrc directives are updated to allow external resources like fonts and stylesheets from 3rd parties.

In other frameworks, this CSP may be included within a <meta> tag in your root file, or your root server component.


Real-World Example: Adjusting CSP for Adobe Fonts and Google Fonts

Recently, I ran into an issue where my Hydrogen app refused to load fonts from Google and Adobe. The errors were indicating that my CSP was too restrictive:

Refused to load the stylesheet 'https://p.typekit.net/p.css'... Refused to load the font 'https://fonts.gstatic.com/s/opensans/v40/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6FxZCJgvAQ.woff2'...

Here was my fix:

  1. Add https://use.typekit.net and https://p.typekit.net to styleSrc: Adobe Fonts need access to these domains for loading styles and tracking images.

  2. Add https://fonts.gstatic.com to fontSrc: Google Fonts are served from this domain, so you need to allow it for your fonts to load properly.

  3. Ensure unsafe-inline is present in styleSrc: This is required by Adobe Fonts, as they use inline styles for rendering fonts.

The updated styleSrc and fontSrc look like so:

styleSrc: [
  "'self'",
  "'unsafe-inline'", 
  'https://cdn.shopify.com',
  'https://cdnjs.cloudflare.com',
  'https://fonts.googleapis.com',
  'https://maxcdn.bootstrapcdn.com',
  'https://use.typekit.net',
  'https://p.typekit.net',
],
fontSrc: [
  "'self'",
  'https://fonts.googleapis.com',
  'https://maxcdn.bootstrapcdn.com',
  'https://use.typekit.net',
  'https://fonts.gstatic.com',
],

Once these changes were made, the fonts and styles loaded without issues, and the CSP errors were resolved.


Conclusion

CSP is a powerful tool that helps protect your site from malicious attacks. However, it can be a bit troublesome, especially when using boilerplate that implements it without you knowing.

Keep in mind, making your CSP more permissive can solve loading issues, but it’s important to strike the right balance between security and functionality. Always consider the changes you make to ensure you’re not unintentionally exposing your site to cyber attacks.