# Спека: Content-Security-Policy (Report-Only) для SPA Лидерры

## Цель

Добавить заголовок Content-Security-Policy в режиме Report-Only на все web-ответы
портала, чтобы закрыть Medium-предупреждение go-live аудита (ZAP) о недостающей
политике безопасности контента. Режим Report-Only выбран намеренно первым шагом:
браузер только сообщает о нарушениях, но не блокирует ресурсы, поэтому интерфейс
Vue 3 + Vuetify 3 не ломается, а нарушения можно собрать перед переводом политики
в блокирующий режим.

## Контракт заголовка {#D1}

Заголовок проставляется тем же middleware, что уже ставит安 X-Frame-Options /
X-Content-Type-Options / Referrer-Policy — на все web-ответы.

- Имя заголовка: `Content-Security-Policy-Report-Only` (НЕ блокирующий
  `Content-Security-Policy`).
- Значение — одна строка директив, разделённых `; `.
- Директивы (минимально необходимый набор под текущий SPA):
  - `default-src 'self'`
  - `script-src 'self'`
  - `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`
  - `font-src 'self' https://fonts.gstatic.com data:`
  - `img-src 'self' data: blob:`
  - `connect-src 'self'`
  - `frame-ancestors 'self'`
  - `base-uri 'self'`
  - `form-action 'self'`
  - `object-src 'none'`
- Блокирующий заголовок `Content-Security-Policy` на этом шаге НЕ ставится.

## Политика под Vue 3 + Vuetify 3 {#D2}

Обоснование источников — по фактическому составу фронтенда:

- `script-src 'self'` — сборка Vite отдаёт бандлы JS с того же origin; инлайн-скриптов
  в Blade-оболочке нет; runtime-only сборка Vue без шаблонного компилятора не требует
  `'unsafe-eval'`.
- `style-src 'unsafe-inline'` — Vuetify 3 инжектит стили в рантайме и использует
  атрибуты style; nonce для рантайм-стилей Vuetify не поддерживает, поэтому
  `'unsafe-inline'` обязателен, иначе интерфейс получит шквал нарушений.
- `https://fonts.googleapis.com` в `style-src` — шрифты подключены через CSS
  `@import url('https://fonts.googleapis.com/...')` (Inter, JetBrains Mono).
- `font-src https://fonts.gstatic.com data:` — сами файлы шрифтов Google Fonts
  отдаёт gstatic; `data:` — для инлайн-иконок/шрифтов.
- `img-src data: blob:` — data-URI иконки и blob-превью графиков.
- `connect-src 'self'` — API того же origin (Sanctum SPA, session-cookie). Внешние
  телеметрия/чат (Sentry self-hosted, JivoSite) во фронтенде сейчас фактически не
  подключены — расширение источников отложено до их реальной интеграции.

## Граничные случаи {#D3}

- Report-Only НЕ должен ставить блокирующий `Content-Security-Policy`: наличие
  блокирующего заголовка на этом шаге — дефект.
- Заголовок CSP добавляется НЕ ломая уже проставляемые X-Frame-Options /
  X-Content-Type-Options / Referrer-Policy — они остаются на месте.
- `frame-ancestors 'self'` дополняет X-Frame-Options SAMEORIGIN современным
  эквивалентом, не противоречит ему.
- Политика применяется и к публичным путям (welcome SPA-оболочка `/`), и к
  API-ответам того же web-стека.

## Конвенция middleware {#D4}

- Логика остаётся в `App\Http\Middleware\SecurityHeaders::handle`.
- Строка политики собирается отдельным приватным методом (например
  `contentSecurityPolicy(): string`), чтобы значение читалось одним местом и легко
  переводилось в блокирующий режим в будущем (смена имени заголовка — один шаг).
- Существующий PHPDoc-комментарий про «CSP вынесен отдельно» приводится в
  соответствие: CSP теперь ставится в Report-Only.

## Критерий приёмки {#D5}

Pest-тест `tests/Feature/SecurityHeadersTest.php` по публичному `/`:

- ответ содержит непустой заголовок `Content-Security-Policy-Report-Only`;
- значение содержит подстроку `default-src 'self'`;
- значение содержит `frame-ancestors 'self'` и `https://fonts.googleapis.com`
  (доказательство выверки под Vuetify/шрифты);
- ответ НЕ содержит блокирующий заголовок `Content-Security-Policy`;
- ранее существующие проверки трёх заголовков остаются зелёными.

```verified-context-json
[
  {
    "id": "ctx-sec-headers",
    "kind": "EXTRACTED",
    "ref": "app/app/Http/Middleware/SecurityHeaders.php",
    "anchor": "$response->headers->set('X-Frame-Options', 'SAMEORIGIN');"
  },
  {
    "id": "ctx-fonts",
    "kind": "EXTRACTED",
    "ref": "app/resources/css/app.css",
    "anchor": "fonts.googleapis.com"
  }
]
```
