Защита внешних подресурсов с помощью целостности подресурсов, CORS и политики реферера

При загрузке скриптов, таблиц стилей, шрифтов или изображений из сторонних сетей доставки контента или внешних источников проверяйте их криптографическую целостность с помощью Subresource Integrity (integrity в HTML и integrity() в CSS url()), настраивайте обработку учетных данных из разных источников (crossorigin и cross-origin()) и ограничивайте исходящие заголовки Referer (referrerpolicy и referrer-policy()). Чтобы узнать больше о Content Security Policy на уровне документа и белых списках исходящих подключений, ознакомьтесь со статьями Веб-безопасность и Как ограничить исходящие сетевые подключения с помощью белых списков подключений.

1. Создавайте криптографические дайджесты целостности для неизменяемых объектов.

Целостность субресурсов (SRI) гарантирует, что браузер выполняет или отображает внешний объект, только если его байты соответствуют криптографическому хешу, объявленному вашим приложением.

  • Вычисляйте дайджесты во время сборки или выпуска, а не на основе ответов, полученных в реальном времени и не заслуживающих доверия. Генерируйте хеши целостности на этапе сборки или выпуска на основе байтов локальных объектов, которые точно не содержат ошибок (или скопируйте официальный дайджест, опубликованный автором пакета), и задайте их в коде HTML, CSS или JavaScript. Никогда не извлекайте сторонний URL в браузере во время выполнения, чтобы вычислить его собственный хеш integrity. Это приведет к тому, что будут приняты любые байты, возвращенные удаленным сервером, и нарушит SRI.
  • Использовать sha384- по умолчанию. Метаданные целостности форматируются как <algorithm>-<base64-digest> с помощью sha384- (рекомендуется по умолчанию), sha512- или sha256-.
  • Привязывайте внешние подресурсы к неизменяемым URL с указанием версии. Всегда ссылайтесь на сторонние ресурсы, используя неизменяемые URL с указанием версии или адреса контента (например, https://cdn.example.com/ui@2.4.0/app.min.js), а не изменяемые плавающие URL (latest/app.min.js), которые делают ваш сайт уязвимым для незаметных изменений в цепочке поставок и нарушают SRI, как только удаленный файл обновляется. Если вам нужно загрузить изменяемый сторонний скрипт, который обновляется на месте (например, тег аналитики поставщика), не указывайте integrity в URL и ограничьте его с помощью Content Security Policy (strict-dynamic/nonce) или изолированной среды iframe.
  • Если конечная точка может передавать несколько допустимых полезных нагрузок, укажите несколько хешей, разделенных пробелами. Все браузеры, поддерживающие SRI, реализуют sha256-, sha384- и sha512-. Если URL может возвращать один из нескольких допустимых пакетов данных (например, во время A/B-тестирования, поэтапного развертывания CDN или сборки с согласованием контента), укажите несколько хешей одного алгоритма, разделенных пробелом (например, integrity="sha384-variantA... sha384-variantB..."). Браузер примет ответ, если он соответствует любому из указанных хешей самого надежного алгоритма.
  • Используйте заголовки CORS для передачи дополнительных ресурсов из разных источников. Браузеры могут проверять целостность ответов из разных источников, только если они доступны для чтения CORS (Access-Control-Allow-Origin: * для анонимных запросов или точный источник запроса с Access-Control-Allow-Credentials: true для запросов с учетными данными).
# Generate a sha384- SRI hash at build time from a known-good local file
printf "sha384-%s\n" "$(openssl dgst -sha384 -binary app.min.js | openssl base64 -A)"
// Build-time / tooling helper (Node.js 19+, Deno, Bun) to compute an SRI string from known-good local bytes
export async function computeIntegrityMetadata(bytes) {
  const rawBytes =
    typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
  const digestBuffer = await crypto.subtle.digest('SHA-384', rawBytes);
  const binary = Array.from(new Uint8Array(digestBuffer), (b) =>
    String.fromCharCode(b),
  ).join('');
  return `sha384-${btoa(binary)}`;
}

2. Защита HTML-скриптов, таблиц стилей и предварительных загрузок

Установите заголовок HTTP-ответа Referrer-Policy: strict-origin-when-cross-origin на уровне документа (или включите <meta name="referrer" content="strict-origin-when-cross-origin"> в <head>) в качестве базового значения по умолчанию, чтобы любой подресурс или CSS url(), который опускает модификатор URL перехода для каждого запроса, по-прежнему удалял конфиденциальный путь и параметры запроса в запросах из разных источников.

Затем для внешних элементов <script>, <link rel="stylesheet">, <link rel="preload"> и <link rel="modulepreload"> всегда задавайте integrity, crossorigin и referrerpolicy вместе:

  • integrity="sha384-..." – блокирует выполнение или применение ресурса, если байты ответа не соответствуют ожидаемому дайджесту (для заблокированного <link rel="stylesheet"> linkElement.sheet остается null и запускается событие error).
  • crossorigin="anonymous" (рекомендуемое значение по умолчанию для общедоступных объектов CDN) или crossorigin="use-credentials". Требуется, если для ресурса из разных источников задан заголовок integrity. Если не указать crossorigin, браузер получит ресурс в режиме no-cors (что приведет к непрозрачному ответу, который нельзя проверить с помощью хеша) и заблокирует его.
  • referrerpolicy="no-referrer" (рекомендуется по умолчанию для сторонних CDN) или referrerpolicy="strict-origin-when-cross-origin". Переопределяет правила в отношении URL перехода, чтобы предотвратить утечку информации об источнике или пути в заголовке запроса Referer на сторонний хост.
<!-- Document-level baseline referrer policy in <head> (or via Referrer-Policy HTTP header) -->
<meta name="referrer" content="strict-origin-when-cross-origin" />

<!-- External stylesheet with SRI, anonymous CORS, and no referrer -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/ui@2.4.0/styles.css"
  integrity="sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
/>

<!-- External script with SRI, anonymous CORS, and no referrer -->
<script
  src="https://cdn.example.com/ui@2.4.0/bundle.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

При программном получении внешних подресурсов с помощью fetch() передайте соответствующие параметры integrity, mode, credentials и referrerPolicy (несоответствие дайджеста отклоняет обещание с помощью TypeError):

const response = await fetch('https://cdn.example.com/ui@2.4.0/manifest.json', {
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
  mode: 'cors',
  credentials: 'omit',
  referrerPolicy: 'no-referrer',
});

3. Защитите изображения, шрифты и импортированные таблицы стилей CSS с помощью модификаторов url().

Модификаторы запросов CSS url() (cross-origin(), referrer-policy() и integrity()) управляют режимом получения, заголовком реферера и проверкой целостности подресурсов для ресурсов, загружаемых с помощью CSS, включая правила background-image, content, mask-image, border-image-source, filter, @font-face src и @import, непосредственно из CSS:

Функция-модификатор Синтаксис аргументов и допустимые значения Цель
cross-origin(<ident>) Ключевое слово без кавычек: anonymous (рекомендуется по умолчанию) или use-credentials. Получает ресурс CSS с помощью CORS и управляет отправкой учетных данных. Всегда указывайте вместе с integrity() в ресурсах CSS из разных источников.
referrer-policy(<ident>) Ключевое слово без кавычек: no-referrer (рекомендуемое значение по умолчанию для сторонних объектов), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade или unsafe-url. Переопределяет правила в отношении источника запроса для этого конкретного запроса CSS.
integrity("<string>") Строка в кавычках: "sha384-<base64>" (или хеши, разделенные пробелами "sha384-... sha384-...") Перед отрисовкой или применением полученного ресурса CSS проверяет его на соответствие ожидаемому криптографическому дайджесту.

При создании модификаторов url() следуйте приведенным ниже правилам синтаксиса и безопасности CSS.

  • Заключите строку URL в кавычки внутри url("..."). Модификаторы запросов действительны, только если URL внутри url(...) заключен в кавычки <string> (URL без кавычек не могут использовать модификаторы).
  • Указывайте каждый тип модификатора не более одного раза в элементе url(). Вы можете комбинировать элементы cross-origin(), referrer-policy() и integrity() в любом порядке внутри элемента url("..." ...), но повторять один и тот же тип модификатора дважды (например, два модификатора integrity()) нельзя. Если вы хотите указать несколько хешей, разделите их пробелами в одной строке integrity("sha384-... sha384-...").
  • Всегда используйте integrity() вместе с cross-origin(anonymous). Хотя @font-face, src и mask-image по умолчанию получают данные в анонимном режиме cors, background-image, content, border-image-source и @import по умолчанию используют режим no-cors (создавая непрозрачный ответ, который нельзя проверить с помощью хеша), а внешний SVG filter по умолчанию использует режим same-origin (блокируя перекрестные URL). Добавление cross-origin(anonymous) (или cross-origin(use-credentials)) обеспечивает согласованную проверку CORS для каждого свойства CSS.
  • Помещайте модификаторы внутри url("...") в правилах @import и @font-face. Оставляйте cross-origin(), referrer-policy() и integrity() внутри url("..." ...), а дескрипторы шрифтов, такие как format("woff2") и tech(...), – за пределами url(...).
@import url(
  "https://cdn.example.com/theme@1.2.0/tokens.css"
  cross-origin(anonymous)
  referrer-policy(no-referrer)
  integrity("sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5")
);

@font-face {
  font-family: "BrandSans";
  src: url(
    "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
  ) format("woff2");
  font-display: swap;
}

.verified-graphic {
  background-image: url(
    "https://cdn.example.com/media@1.0.0/badge.svg"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
  );
}

Резервные стратегии

Если целевая версия Baseline пока не поддерживает модификаторы запросов CSS url() (cross-origin(), referrer-policy() или integrity()), объедините правила на уровне документа с каскадными объявлениями CSS, чтобы подресурсы оставались функциональными и защищенными:

  1. Если требуется кроссбраузерная проверка целостности субресурсов, используйте HTML-тег <link rel="stylesheet">, а не CSS-правило @import. Поскольку атрибуты subresource-integrity и referrer-policy тега <link rel="stylesheet"> поддерживаются всеми браузерами, загружайте внешние таблицы стилей с помощью <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer">, а не @import, если проверку целостности нужно выполнять во всех браузерах.
  2. Положитесь на заголовок Referrer-Policy или элемент <meta name="referrer"> на уровне документа, добавленные на шаге 2. В браузерах, которые не поддерживают referrer-policy() в CSS-правиле url(), заголовок Referrer-Policy: strict-origin-when-cross-origin или элемент <meta name="referrer" content="strict-origin-when-cross-origin"> на уровне документа автоматически удаляет конфиденциальные параметры пути и параметры запроса из перекрестных CSS-запросов.
  3. Используйте каскадные отдельные объявления CSS для прогрессивного улучшения. Браузеры, которые не распознают модификатор внутри url("..." ...), отбрасывают все объявление при синтаксическом анализе CSS. Сначала объявите обычный url("..."), затем cross-origin() и referrer-policy() (для браузеров, которые поддерживают эти два модификатора, но не поддерживают integrity()), а затем полное объявление, включая integrity("sha384-..."):

    • Никогда не указывайте непроверенный url() в том же списке @font-face src, разделенном запятыми. Если вы укажете src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); в одном дескрипторе src, то во время выполнения будет обойдена проверка целостности субресурсов. Если CDN предоставит измененный шрифт, который не пройдет проверку integrity(), браузер расценит это как ошибку загрузки и автоматически вернется ко второму url("font.woff2"), разделенному запятыми. Использование отдельных объявлений src: гарантирует, что поддерживающие браузеры перезапишут непроверенное объявление во время синтаксического анализа CSS и заблокируют поддельные шрифты во время выполнения.
    @font-face {
      font-family: "BrandSans";
      /* 1. Parse-time fallback for browsers without url() modifier support */
      src: url("https://cdn.example.com/fonts@1.0.0/brand-sans.woff2") format("woff2");
      /* 2. Parse-time fallback for browsers supporting cross-origin() and referrer-policy() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      ) format("woff2");
      /* 3. Full protection in browsers supporting cross-origin(), referrer-policy(), and integrity() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
      ) format("woff2");
      font-display: swap;
    }
    
    .verified-graphic {
      background-image: url("https://cdn.example.com/media@1.0.0/badge.svg");
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      );
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
      );
    }
    
  4. Определите поддержку модификатора url() в CSS с помощью CSS.supports() в JavaScript:

export function getCssUrlModifierSupport() {
  const supportsCrossOriginAndReferrer =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer))',
    );

  const supportsUrlIntegrity =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91"))',
    );

  return { supportsCrossOriginAndReferrer, supportsUrlIntegrity };
}