# Content Security Policy

## Що це таке

Content Security Policy — це набір правил, які вебсайт передає браузеру: що його
сторінкам дозволено виконувати, завантажувати й надсилати. Далі браузер сам
стежить за цими правилами від імені сайту. Назву майже завжди скорочують до CSP.
Сайт надсилає правила в заголовку відповіді `Content-Security-Policy` або вписує
їх просто в сторінку через елемент `meta`, і відтоді кожен скрипт, таблицю
стилів, зображення, шрифт, фрейм чи з'єднання, яких правила не дозволяють,
браузер відхиляє ще до завантаження.

Політика — це короткий рядок директив, розділених крапкою з комою. Кожна
директива називає один тип ресурсу чи поведінки та джерела, які вона приймає.
Тут для зручності кожна директива стоїть в окремому рядку, хоча мережею вона йде
як одне значення заголовка:

```text
default-src 'self';
script-src 'nonce-q8Zt3VwR1mKx7NpL2cYfJA==' 'strict-dynamic';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none'
```

Словник невеликий. `'self'` означає власне походження сторінки. Хост на кшталт
`https://static.example.com` або схема на кшталт `https:` додають інші джерела.
`'none'` не дозволяє нічого. Nonce — це випадкове значення, яке сервер генерує
для однієї конкретної відповіді й записує і в політику, і на кожен елемент
`script`, за який ручається. Хеш — це дайджест точного тексту вбудованого
скрипту, тож виконатися може саме цей скрипт, а інший уже ні. Два ключові слова,
`'unsafe-inline'` та `'unsafe-eval'`, знову вимикають захист, і їхні назви —
чесне попередження.

`default-src` — запасний варіант для директив завантаження, які політика
пропустила: `script-src`, `style-src`, `img-src`, `connect-src` та інших. Тільки
для них. Директиви, що керують поведінкою, а не завантаженням, серед них
`base-uri`, `form-action` і `frame-ancestors`, лишаються без обмежень, доки їх
не прописати явно.

Політика може працювати й у режимі спостереження. Надіслана як
`Content-Security-Policy-Report-Only`, вона нічого не блокує, а повідомляє про
кожне порушення на кінцеву точку, вказану через `report-to`, яка спирається на
Reporting API браузера. Старіша директива `report-uri` застаріла на її користь.
Чинний текст стандарту — Content Security Policy Level 3, робочий проєкт W3C від
робочої групи Web Application Security.

## Чому це важливо

CSP створювали проти міжсайтового скриптингу, який зазвичай називають XSS.
Зловмисник вставляє на сторінку довільний текст через поле коментаря, пошуковий
запит, який сторінка повторює у своєму HTML, або скомпрометовану залежність, і
браузер виконує цей текст як скрипт з усіма повноваженнями власного коду сайту.
Такий скрипт може читати сторінку, діяти від імені користувача, що увійшов, і
надсилати знайдене куди завгодно. Перший захист — правильне екранування виводу,
і воно мусить бути правильним у кожному шаблоні, в кожному рядку, щоразу. CSP —
другий захист. Коли помилка екранування все ж проскакує, підкинутому скрипту ще
треба пройти крізь політику, а політика, яка виконує лише скрипти з nonce саме
цієї відповіді, його не пропустить.

Вона також обмежує, куди може дотягнутися скрипт, який таки виконався.
`connect-src` визначає, з якими хостами скрипт може спілкуватися, і цим звужує,
куди скімер може відправити зібране. `frame-ancestors` визначає, які сайти
можуть показувати сторінку у фреймі. Це стандартний захист від клікджекінгу,
коли одну сторінку накладають на іншу, щоб клік потрапив туди, куди користувач і
не збирався натискати. `form-action` визначає, куди можуть надсилатися форми, а
`base-uri` не дає підкинутому елементу `base` непомітно перенаправити кожну
відносну адресу на сторінці.

Варто точно розуміти, кого саме захищає CSP. Вона працює в браузері, отже,
оберігає читача сторінки від вмісту, який туди підклав зловмисник. Серверу від
цього ж читача вона нічим не допомагає, бо той може змінити або прибрати
заголовок у власному браузері за лічені секунди. Політика — привід довіряти
сторінці менше, але ніколи не привід більше довіряти запиту.

Головна ціна — інвентаризація. Політика змушує команду знати кожне походження, з
якого вантажаться її сторінки, кожен вбудований скрипт і кожен обробник подій,
записаний як атрибут. Новий сайт можна з першого коміту будувати під сувору
політику майже задарма. Старіший сайт із вбудованими обробниками, менеджером
тегів і кількома віджетами спершу платить за прибирання, і саме це прибирання
зазвичай і є справжнім проєктом.

## Як це виглядає на практиці

Цей сайт і сам живе за такою політикою. [Сторінка безпеки](/uk/security/)
називає сувору Content Security Policy, оголошену в самій сторінці, з
`form-action 'none'`, бо сайт нікуди не надсилає форм. Там же вказано один
власний скрипт, менший за кілобайт, і жодних запитів до третіх сторін. Саме це й
тримає таку політику короткою: усе, що потрібно сторінці, надходить з одного
хоста, власного, тож дозволяти майже нічого.

Застосунку, де користувачі входять в обліковий запис, короткого переліку
дозволених джерел замало. На платформі з професійними та організаційними даними
[проксі-мідлвар Next.js встановлює Content-Security-Policy з nonce для кожного запиту та `strict-dynamic`](/uk/portfolio/hardened-the-application-with-nonce-based-csp-hsts-77/),
а також HSTS і SameSite cookies. Свіжий nonce у кожній відповіді означає, що
виконаються лише ті елементи `script`, які сервер позначив саме в цій відповіді.
Із `strict-dynamic` у політиці скрипт, якому довіряє nonce, може сам
підвантажувати інші скрипти, тож політиці не треба перелічувати кожен хост, з
якого бандл тягне свої частини. Та сама сторінка прямо каже, де закінчується
роль браузера. Її робоче припущення таке: клієнт вороже налаштований, тому права
доступу сервер перевіряє повторно на кожен запит, а фронтендні обмеження
розглядаються лише як UX. CSP там — один із кількох рівнів. Вона захищає
користувачів від підкинутого коду, а те, кому що дозволено, вирішується деінде.

## Де виникають проблеми

**Політику вмикають, і пів сайту перестає працювати.** Сувора політика, одразу
відправлена в режим примусу, блокує вбудований скрипт, про який усі забули, і
тег відстеження, який хтось додав два роки тому. Спершу надсилайте її як
`Content-Security-Policy-Report-Only`, читайте звіти, доки вони не вщухнуть,
дозвольте або приберіть кожне джерело, яке вони називають, і лише тоді вмикайте
примус. Заголовок report-only має бути справжнім заголовком, бо специфікація не
підтримує його в елементі `meta`.

**Перелік довіряє цілому хосту.** `script-src https://cdn.example.com` дозволяє
кожен файл на цьому хості, зокрема старі версії бібліотек і кінцеві точки, які
повертають введене користувачем як скрипт. Специфікація сама визнає, що
розгорнути політику, яка справді працює проти міжсайтового скриптингу, складно,
і радить те, що називає Strict CSP: `script-src` з nonce або хешів разом із
`'strict-dynamic'`, плюс `base-uri` зі значенням `'self'` або `'none'`.

**Вважають, що `'unsafe-inline'` дозволяє будь-який вбудований скрипт.** Щойно в
тому самому переліку джерел з'являється nonce чи хеш, браузери, які їх
розуміють, ігнорують `'unsafe-inline'`. Це правило дає політиці змогу лишити
`'unsafe-inline'` як запасний варіант для браузерів, надто старих для nonce,
тоді як сучасні застосовують суворий перелік. І воно ж дивує команду, яка додала
nonce одному скрипту й побачила, що решта вбудованих скриптів тепер заблокована.

**Nonce перестає бути секретом.** Специфікація вимагає нового значення щоразу,
коли надсилається політика, і просить щонайменше 128 біт від криптографічно
стійкого генератора. Nonce, жорстко вписаний у шаблон, або ціла сторінка, яка
разом зі своїм nonce лежить у спільному кеші й віддається всім, дає зловмиснику
саме те значення, яке потрібне, щоб позначити власний скрипт.

**Політика в елементі `meta` робить менше, ніж та сама політика в заголовку.**
Браузер ігнорує `frame-ancestors`, `report-uri` і `sandbox`, коли вони приходять
таким шляхом, а політика діє лише на те, що стоїть після елемента в документі,
тож усе вище нього в head завантажується без перевірки. Сайт, який не має впливу
на заголовки відповіді, все одно отримує реальний захист від політики в `meta`,
розміщеної в head якомога раніше, але захист від показу сторінки в чужих фреймах
потребує заголовка.

**Друга політика затягує, а не послаблює.** Відповідь може нести кілька політик,
від фреймворку, проксі та CDN, і ресурс мусить пройти кожну з них. Додана
політика здатна лише обмежувати, тож поблажливий заголовок, доданий далі в
ланцюжку, щоб розблокувати запит, нічого не змінить. Виправлення треба вносити в
ту політику, яка заблокувала.

## Що почитати далі

- [Content Security Policy Level 3](https://www.w3.org/TR/CSP3/) — сам стандарт,
  із рекомендацією Strict CSP і розділами про те, як nonce перехоплюють і
  викрадають.
- [HTML Standard: елемент meta](https://html.spec.whatwg.org/multipage/semantics.html)
  — прагма, що застосовує політику, оголошену в сторінці, директиви, які вона
  відкидає, і чому ресурси, завантажені до неї, можуть проскочити.
- [Reporting API](https://www.w3.org/TR/reporting-1/) — механізм кінцевих точок,
  на який спирається `report-to`, щоб читати порушення ще до ввімкнення примусу.
- [Trusted Types](https://www.w3.org/TR/trusted-types/) — супутній стандарт,
  який вмикає директива `require-trusted-types-for`, спрямований проти скриптів,
  підкинутих через DOM.

<https://engineer.company/uk/glossary/csp/>
