Використання Content-Security-Policy разом із React & Emotion

Content-Security-Policy(CSP) – це HTTP заголовок, який покращує безпеку веб-застосунків за рахунок заборони небезпечних дій, таких як завантаження та відправка даних на довільні домени, використання eval, inline-скриптів і т.д. У цій статті буде зроблено фокус на директиві style-srcта її використання разом із CSS-in-JS бібліотекою emotion.

Коротко про CSP та style-src

Content-Security-Policyзаголовок повинен бути виставлений у відповіді разом із завантажуваною веб-сторінкою (наприклад, index.html). Це виглядає так:

Content-Security-Policy: style-src 'self'

style-src– це директива, яка відповідає за те, які стилі можна завантажувати та застосовувати на сторінці. Можливі значення:

  • 'none'– усі стилі заборонені
  • 'self'– Дозволені файли стилів, які завантажуються з того ж домену, що і основний документ (сторінка)
  • <url>, наприклад https://example.com– дозволені файли стилів з цього домену, також допускаються wildcard (*) на місці під-домену та порту
  • '<hash-algorithm>-<base64-value>', наприклад 'sha256-ozBpjL6dxO8fsS4u6fwG1dFDACYvpNxYeBA6tzR+FY8='– дозволені файли стилів та inline -стилі (тег <style>), у яких хеш збігається із зазначеним значенням
  • 'nonce-<value>', наприклад 'nonce-abc'– дозволяються inline -стилі, у яких атрибут nonceзбігається із зазначеним (у прикладі – abc)
  • 'unsafe-hashes'– дозволяє inline -стилі, зазначені в атрибуті styleрядком, наприклад <div style="color:red;"></div>, при цьому хеш значення атрибута повинен збігатися з хешом, вказаним у'<hash-algorithm>-<base64-value>'
  • 'unsafe-inline'– дозволяє всі inline -стилі, створені через тег<style>
  • 'unsafe-eval'– дозволяє додати/змінити CSS declarations, які призводять до парсингу рядка, наприклад, за допомогою CSSStyleDeclaration.cssText

Директива може приймати кілька значень через пропуск. У цьому випадку це сприймається як логічне “або” – при задоволенні хоча б одного значення стилі дозволяються.

CSP та emotion

emotionдодає styleелементи динамічно і в останніх версіях не може виймати всі стилі в окремий файл під час складання програми. Це означає, що для того, щоб можна було використовувати emotionразом з style-src, є такі опції:

  1. 'unsafe-inline'– Найпростіша опція з усіх. Не вимагає будь-яких налаштувань з боку emotion. При цьому ми знижуємо безпеку нашої програми, тому це рішення можна використовувати лише як тимчасове.
  2. 'nonce-<value>'– можна дозволити inline-стилі, створені emotion. Для цього потрібно задати nonceпри створенні cache.

При використанні @emotion/reactабо @emotion/styledце можна зробити так:

import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";

export function App() {
  const cache = createCache({
    key: 'my-app',
    nonce: getNonceValue(),
  });
  
  return (
    <CacheProvider cache={cache}>
      {/* children */}
    </CacheProvider>
  );
}

Якщо використовується @emotion/cssбезпосередньо, то потрібно створити свій екземпляр emotion:

import createEmotion from '@emotion/css/create-instance';

export const {
  flush,
  hydrate,
  cx,
  merge,
  getRegisteredStyles,
  injectGlobal,
  keyframes,
  css,
  sheet,
  cache
} = createEmotion({
  key: 'my-app',
  nonce: getNonceValue(),
});

При використанні createEmotionпотрібно змінити всі місця, де раніше імпортувався @emotion/cssна цей модуль:

// import { css } from "@emotion/css"; 
import { css } from "./emotion";

Передача nonce на фронтенд

Т.к. значення CSP заголовка недоступне коду, що виконується на клієнті, значення потрібно додатково передати іншим чином. Один із варіантів – це створення inline-скрипту зі значенням, яке виставляється на бекенді:

<script id="nonce" type="application/json">
  "abc"
</script>

На фронтенді це можна використовувати таким чином:

function getNonceValue() {
  const nonceElement = document.getElementById("nonce");
  return JSON.parse(nonceElement.textContent);
}

Зверніть увагу на type="application/json"– таким чином браузер не вважає це виконуваним кодом, і особливе значення script-srcне потрібно.

Джерело