Content Security Policy (CSP)

CSP — HTTP-заголовок, который указывает браузеру, какие ресурсы можно загружать и откуда. Мощная защита от XSS и data injection атак.

Зачем нужно

Даже если злоумышленник нашёл XSS-уязвимость, CSP не даст выполнить inline-скрипт или загрузить скрипт с чужого домена. Это второй рубеж обороны после санитизации ввода. CSP также предотвращает clickjacking, подмену ресурсов и утечку данных.

Где используется

Все публичные веб-приложения. GitHub, Google, Facebook, Twitter — все используют CSP. Обязательно для приложений, работающих с пользовательскими данными.

Предпосылки

XSS, HTTPS и SSL, HTTP-заголовки

Как работает CSP

1. Сервер отправляет HTTP-заголовок:
   Content-Security-Policy: script-src 'self' https://cdn.example.com

2. Браузер разрешает ТОЛЬКО:
   - Скрипты с того же домена ('self')
   - Скрипты с cdn.example.com

3. Всё остальное БЛОКИРУЕТСЯ:
   - Inline-скрипты (<script>alert('XSS')</script>)
   - Скрипты с evil.com
   - eval, setTimeout('string')

Установка CSP

Через HTTP-заголовок (рекомендуется)

// Express.js
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'"
  );
  next;
});

// Через Helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", 'https://cdn.example.com'],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'", 'https://api.example.com'],
    fontSrc: ["'self'", 'https://fonts.gstatic.com'],
    frameSrc: ["'none'"],
  },
}));

Через meta-тег (ограниченно)

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'">

Ограничения meta: нельзя использовать frame-ancestors, report-uri, sandbox.

Директивы CSP

Основные директивы

Директива Контролирует Пример
default-src Всё по умолчанию 'self'
script-src JavaScript 'self' https://cdn.com
style-src CSS 'self' 'unsafe-inline'
img-src Изображения 'self' data: https:
connect-src fetch, XHR, WebSocket 'self' https://api.com
font-src Шрифты 'self' https://fonts.gstatic.com
frame-src iframe 'none'
media-src Audio, Video 'self'
object-src Flash, Java applets 'none'
base-uri Тег <base> 'self'
form-action Куда отправлять формы 'self'
frame-ancestors Кто может вставить в iframe 'none'

Значения источников

Значение Описание
'self' Тот же origin (протокол + домен + порт)
'none' Ничего не разрешено
'unsafe-inline' Inline-скрипты/стили (старайся избегать!)
'unsafe-eval' eval и подобные (избегай!)
https: Любой HTTPS-источник
data: Data URI (data:image/png;base64,...)
https://cdn.com Конкретный домен
*.example.com Все поддомены

Nonce — безопасные inline-скрипты

Вместо 'unsafe-inline' используй одноразовые nonce:

// Сервер генерирует уникальный nonce на каждый запрос
const crypto = require('crypto');

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;

  res.setHeader(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${nonce}'`
  );
  next;
});
<!-- Только скрипты с правильным nonce выполнятся -->
<script nonce="rAnd0mN0nCe">
  console.log('Разрешено!');
</script>

<!-- Без nonce — заблокировано -->
<script>
  console.log('Заблокировано CSP!');
</script>

Hash — разрешение конкретных скриптов

Content-Security-Policy: script-src 'sha256-xyz123...'
<!-- Только скрипт с этим точным содержимым выполнится -->
<script>console.log('hello');</script>
# Вычислить hash скрипта
echo -n "console.log('hello');" | openssl dgst -sha256 -binary | openssl base64

Примеры CSP для разных сценариев

Минимальный (строгий)

Content-Security-Policy:
  default-src 'none';
  script-src 'self';
  style-src 'self';
  img-src 'self';
  connect-src 'self';
  font-src 'self';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none'

SPA с CDN и API

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https:;
  connect-src 'self' https://api.myapp.com wss://ws.myapp.com;
  frame-ancestors 'none';
  base-uri 'self'

С Google Analytics и reCAPTCHA

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://www.google.com https://www.gstatic.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' https://www.google-analytics.com;
  connect-src 'self' https://www.google-analytics.com;
  frame-src https://www.google.com

Report-Only — тестирование CSP

// Сначала только ОТЧЁТ, без блокировки
res.setHeader(
  'Content-Security-Policy-Report-Only',
  "default-src 'self'; report-uri /csp-report"
);

// Endpoint для получения отчётов
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  console.log('CSP violation:', req.body);
  res.status(204).end();
});

Отчёт приходит в формате:

{
  "csp-report": {
    "document-uri": "https://myapp.com/page",
    "violated-directive": "script-src 'self'",
    "blocked-uri": "https://evil.com/script.js",
    "original-policy": "default-src 'self'; script-src 'self'"
  }
}

Частые ошибки

1. 'unsafe-inline' для скриптов

// ПЛОХО: обнуляет защиту от XSS
script-src 'self' 'unsafe-inline'

// ХОРОШО: используй nonce или hash
script-src 'self' 'nonce-rAnd0m'

2. Слишком мягкий default-src

// ПЛОХО: разрешает всё
default-src *

// ХОРОШО: запрещает по умолчанию
default-src 'none'

3. Забыли connect-src для API

// Fetch/XHR к API заблокирован!
// Нужно явно разрешить:
connect-src 'self' https://api.example.com

4. Не тестировали через Report-Only

// Всегда начинай с Report-Only, чтобы не сломать сайт
Content-Security-Policy-Report-Only: ...
// Потом, когда ошибок нет, переключай на Content-Security-Policy

Практика

  1. Добавь CSP-заголовок в Express-приложение через Helmet
  2. Настрой script-src с nonce для inline-скриптов
  3. Используй Content-Security-Policy-Report-Only и проверь, что все ресурсы загружаются
  4. Попробуй выполнить inline-скрипт при включённом CSP — убедись, что он заблокирован
  5. Настрой CSP для сайта с Google Fonts и внешним CDN

Связанные темы

Ресурсы