refactor(security): единый источник security-заголовков — nginx

Убраны дубли HTTP-заголовков. nginx уже шлёт enforcing CSP, X-Frame-Options,
X-Content-Type-Options, Referrer-Policy, HSTS, Permissions-Policy, COOP, CORP
через add_header always. App-уровневый middleware SecurityHeaders дублировал
четыре из них и слал лишний CSP Report-Only; на проде add_header always плюс
PHP-заголовок давали дубль в ответе.

- удалён middleware SecurityHeaders и его регистрация в bootstrap/app.php
- SecurityHeadersTest переписан: фиксирует, что приложение эти заголовки не ставит

Прод-дедуп вступит в силу после деплоя. Verify локально 4 из 4 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-18 09:31:47 +03:00
parent 757537ab90
commit c2bdfdd7d3
4 changed files with 59 additions and 129 deletions
@@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Проставляет безопасные HTTP-заголовки на все web-ответы.
*
* Закрывает Medium-находку go-live аудита 17.06.2026 «Missing Anti-clickjacking
* Header» (ZAP), плюс базовые заголовки гигиены:
* - X-Frame-Options: SAMEORIGIN (анти-clickjacking)
* - X-Content-Type-Options: nosniff (запрет MIME-sniffing)
* - Referrer-Policy: strict-origin-when-cross-origin
*
* Content-Security-Policy ставится в режиме Report-Only: браузер только сообщает
* о нарушениях, но НЕ блокирует ресурсы интерфейс Vue 3 + Vuetify 3 не ломается,
* а нарушения собираются перед переводом политики в блокирующий режим. Перевод в
* блокирующий режим смена имени заголовка на `Content-Security-Policy`.
*/
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy-Report-Only', $this->contentSecurityPolicy());
return $response;
}
/**
* Политика CSP под Vue 3 + Vuetify 3 SPA.
*
* Источники выверены по фактическому составу фронтенда:
* - script-src 'self' бандлы Vite с того же origin, без инлайн-скриптов;
* - style-src 'unsafe-inline' рантайм-стили Vuetify (nonce не поддерживается);
* - https://fonts.googleapis.com CSS-импорт шрифтов Inter / JetBrains Mono;
* - font-src https://fonts.gstatic.com файлы шрифтов Google Fonts;
* - connect-src 'self' API того же origin (Sanctum SPA).
*/
private function contentSecurityPolicy(): string
{
return implode('; ', [
"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'",
]);
}
}
+6 -3
View File
@@ -26,9 +26,12 @@ return Application::configure(basePath: dirname(__DIR__))
'saas-admin' => EnsureSaasAdmin::class,
]);
// Безопасные HTTP-заголовки на все web-ответы (go-live аудит 17.06.2026,
// ZAP Medium anti-clickjacking). CSP вынесен отдельно — см. SecurityHeaders.
$middleware->web(append: [\App\Http\Middleware\SecurityHeaders::class]);
// Защитные HTTP-заголовки (CSP, X-Frame-Options, X-Content-Type-Options,
// Referrer-Policy, HSTS, Permissions-Policy, COOP/CORP) ставит nginx —
// единый источник: /etc/nginx/sites-available/liderra (add_header ... always).
// App-уровневый middleware SecurityHeaders удалён 18.06.2026: он дублировал
// те же заголовки, и на проде add_header always + PHP-заголовок давали дубль
// в ответе. CSP в nginx — enforcing (был Report-Only в middleware).
// Webhook receive endpoint (POST /api/webhook/{token}) не должен требовать
// CSRF — запросы приходят от внешних CRM-систем без сессии браузера.
+15 -25
View File
@@ -3,38 +3,28 @@
declare(strict_types=1);
/**
* SecurityHeaders middleware безопасные HTTP-заголовки на всех web-ответах.
* Закрывает ZAP Medium «Missing Anti-clickjacking Header» (go-live аудит 17.06.2026).
* Консолидация security-заголовков (18.06.2026): единый источник nginx
* (/etc/nginx/sites-available/liderra, add_header ... always CSP enforcing,
* X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS, Permissions-Policy,
* COOP/CORP). App-уровневый middleware SecurityHeaders удалён, чтобы не дублировать
* заголовки в ответе (на проде add_header always + PHP-заголовок = дубль).
*
* Этот тест фиксирует консолидацию: приложение само эти заголовки больше НЕ ставит.
* Бьём по публичному / (welcome SPA) auth и БД не нужны.
*/
test('публичный / отдаёт заголовок X-Frame-Options SAMEORIGIN', function () {
$this->get('/')->assertHeader('X-Frame-Options', 'SAMEORIGIN');
test('приложение НЕ ставит Content-Security-Policy-Report-Only (источник CSP — enforcing nginx)', function () {
$this->get('/')->assertHeaderMissing('Content-Security-Policy-Report-Only');
});
test('публичный / отдаёт X-Content-Type-Options nosniff', function () {
$this->get('/')->assertHeader('X-Content-Type-Options', 'nosniff');
test('приложение НЕ дублирует X-Frame-Options (его ставит nginx)', function () {
$this->get('/')->assertHeaderMissing('X-Frame-Options');
});
test('публичный / отдаёт Referrer-Policy strict-origin-when-cross-origin', function () {
$this->get('/')->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
test('приложение НЕ дублирует Referrer-Policy (его ставит nginx)', function () {
$this->get('/')->assertHeaderMissing('Referrer-Policy');
});
test('публичный / отдаёт непустой Content-Security-Policy-Report-Only', function () {
$csp = $this->get('/')->headers->get('Content-Security-Policy-Report-Only');
expect($csp)->not->toBeNull()
->and($csp)->toContain("default-src 'self'");
});
test('CSP Report-Only выверен под Vuetify-инлайн-стили и шрифты Google', function () {
$csp = $this->get('/')->headers->get('Content-Security-Policy-Report-Only');
expect($csp)->toContain("frame-ancestors 'self'")
->and($csp)->toContain("style-src 'self' 'unsafe-inline' https://fonts.googleapis.com")
->and($csp)->toContain('https://fonts.gstatic.com');
});
test('на шаге Report-Only блокирующий Content-Security-Policy НЕ ставится', function () {
$this->get('/')->assertHeaderMissing('Content-Security-Policy');
test('приложение НЕ дублирует X-Content-Type-Options (его ставит nginx)', function () {
$this->get('/')->assertHeaderMissing('X-Content-Type-Options');
});
+38 -37
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-06-17T16:48:23.620Z
Last updated: 2026-06-18T06:11:12.692Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -8,7 +8,7 @@ Last updated: 2026-06-17T16:48:23.620Z
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 3 week(s) ago |
| C4 Сигнальный статус | ✅ | This file (self-reference) |
| C5 Observer-coverage | ✅ | 2092 episode(s) this month · Stop-hook + post-commit OK |
| C5 Observer-coverage | ✅ | 2177 episode(s) this month · Stop-hook + post-commit OK |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 17 chains in sync |
## Кто на посту (оборона М1–М6)
@@ -39,37 +39,37 @@ Last updated: 2026-06-17T16:48:23.620Z
| Время | Действие | Причина |
|---|---|---|
| 2026-06-17T16:48:11.373Z | bash:( printf "LOAD 'anon';\n" ; cat db/anon_masking_labels.sql ; printf "TABLE anon.pg_masking_rules;\n" ) \| ssh lider | escape владельца |
| 2026-06-17T16:48:07.433Z | bash:git commit -m "chore: cspell-words добавлены термины трекера B3/B6 фронтенде десинк недетерминизм ретеншен" -m "Co- | escape владельца |
| 2026-06-17T16:46:51.497Z | write:c:/моя/проекты/портал crm/документация/cspell-words.txt | escape владельца |
| 2026-06-17T16:36:47.235Z | bash:LEFTHOOK_EXCLUDE=cspell git commit -m "docs: закрыты блокеры B3 и B6 на проде liderra.ru, SAAS_ADMIN_TEST_BYPASS=fa | escape владельца |
| 2026-06-17T16:34:40.394Z | bash:git commit -m "docs: закрыты блокеры B3 и B6 на проде liderra.ru, SAAS_ADMIN_TEST_BYPASS=false и APP_DEBUG/APP_ENV | escape владельца |
| 2026-06-17T16:34:25.674Z | plan-done | escape владельца |
| 2026-06-17T16:31:53.595Z | bash:npm run links > lychee-links-report.txt 2>&1 | escape владельца |
| 2026-06-17T16:29:40.276Z | bash:npm run links | escape владельца |
| 2026-06-17T16:28:24.660Z | write:c:/моя/проекты/портал crm/документация/docs/security/2026-06-17-open-items.md | escape владельца |
| 2026-06-17T16:28:24.659Z | write:c:/моя/проекты/портал crm/документация/docs/security/2026-06-17-open-items.md | escape владельца |
| 2026-06-18T06:08:47.673Z | write:c:/моя/проекты/claude-brain/tools/standby-mode-control.mjs | escape владельца |
| 2026-06-18T06:08:24.060Z | bash:ssh liderra-prod 'sudo -u postgres psql -d liderra -tA -c "SELECT id, tenant_id, email, created_at FROM users ORDER | escape владельца |
| 2026-06-18T06:08:17.915Z | write:c:/моя/проекты/портал crm/документация/docs/superpowers/runbooks/2026-06-18-gitea-prod-deploy-pipeline.md | escape владельца |
| 2026-06-18T06:04:44.929Z | bash:ssh liderra-prod 'sudo -u postgres psql -d liderra -tA -c "SELECT id, subdomain, organization_name, contact_email, | escape владельца |
| 2026-06-18T06:04:30.671Z | gate3-arb:continue:2671a8496c40010114e1165780148740d431e17cae66a5f9b2d94e869d6d3ad4 | escape владельца |
| 2026-06-18T06:00:47.994Z | bash:composer --working-dir=app test -- tests/Feature/AdminBillingIndexTest.php tests/Feature/AdminIncidentsIndexTest.ph | escape владельца |
| 2026-06-18T06:00:10.711Z | write:c:/моя/проекты/портал crm/документация/app/tests/feature/adminbillingindextest.php | escape владельца |
| 2026-06-18T05:59:46.643Z | write:c:/моя/проекты/claude-brain/docs/superpowers/specs/2026-06-18-wall-impl-read-freedom-design.md | escape владельца |
| 2026-06-18T05:56:49.449Z | bash:composer --working-dir=app test -- tests/Feature/AdminBillingIndexTest.php tests/Feature/AdminIncidentsIndexTest.ph | escape владельца |
| 2026-06-18T05:52:42.671Z | write:c:/моя/проекты/портал crm/документация/docs/superpowers/router-mentor-wall-guide.md | escape владельца |
**Недавние блоки (детали):**
| Время | Действие | Причина |
|---|---|---|
| 2026-06-17T16:48:11.255Z | bash:node --version | действие не в плане (ожидался шаг undefined: Bash cd "c:/моя/проекты/портал crm/Документация/app") |
| 2026-06-17T16:46:43.482Z | write:c:/моя/проекты/claude-brain | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-17T16:46:33.490Z | write:c:/users/administrator/.claude/projects/c--------------claude-brain/71a66f64-db64-4b56-99e6-58e78cd7085b.jsonl | path «C:/Users/Administrator/.claude/projects/c--------------claude-brain/71a66f64-db64-4b56-99e6-58e78cd7085b.jsonl» pr |
| 2026-06-17T16:46:29.587Z | powershell:$file = "$env:TEMP\claude-economy-71a66f64-db64-4b56-99e6-58e78cd7085b.json"; if (Test-Path $file) { Get-Cont | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-17T16:46:25.043Z | bash:cat "$TEMP/claude-economy-71a66f64-db64-4b56-99e6-58e78cd7085b.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-17T16:46:24.987Z | bash:cat "$TEMP/claude-economy-71a66f64-db64-4b56-99e6-58e78cd7085b.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cat "$TEMP/claude-econ |
| 2026-06-17T16:45:19.985Z | write:c:/моя/проекты/портал crm/документация/docs/superpowers/plans/2026-06-17-lychee-relative-md-links-fix-plan.md | [судья] замечание (нужно учесть и переписать): — [fatal] spec_fidelity: отсутствует маппинг битых ссылок на корректные п |
| 2026-06-17T16:44:02.040Z | bash:node -e "const fs=require('fs'); for(const f of ['mentor-verdict.test.mjs','judge-engine.test.mjs','enforce-judge-g | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-17T16:44:01.941Z | bash:node -e "const fs=require('fs'); for(const f of ['mentor-verdict.test.mjs','judge-engine.test.mjs','enforce-judge-g | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:node -e "const fs=requ |
| 2026-06-17T16:41:32.113Z | write:c:/моя/проекты/портал crm/документация | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-18T06:11:19.256Z | write:c:/users/administrator/.claude/projects/c--------------claude-brain/5714105c-2a95-4bc9-ad32-1714dc853904.jsonl | path «C:/Users/Administrator/.claude/projects/c--------------claude-brain/5714105c-2a95-4bc9-ad32-1714dc853904.jsonl» pr |
| 2026-06-18T06:11:11.354Z | bash:cat "$TEMP/claude-economy-5714105c-2a95-4bc9-ad32-1714dc853904.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-18T06:11:11.301Z | bash:cat "$TEMP/claude-economy-5714105c-2a95-4bc9-ad32-1714dc853904.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cat "$TEMP/claude-econ |
| 2026-06-18T06:10:44.905Z | write:c:/моя/проекты/портал crm/документация | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-18T06:10:35.112Z | powershell:$tempPath = $env:TEMP; $file = "$tempPath\claude-economy-51a6fd40-3220-4b06-bdcc-8541a67857c1.json"; if (Test | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-18T06:10:31.109Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/51a6fd40-3220-4b06-bdcc-8541a67857c | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/51a6fd40-3220-4b06-bdcc-8541a67857c |
| 2026-06-18T06:10:25.862Z | bash:cat "$TEMP/claude-economy-51a6fd40-3220-4b06-bdcc-8541a67857c1.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-18T06:10:25.762Z | bash:cat "$TEMP/claude-economy-51a6fd40-3220-4b06-bdcc-8541a67857c1.json" 2>/dev/null \|\| echo "FILE_NOT_FOUND" | floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: bash:cat "$TEMP/claude-econ |
| 2026-06-18T06:10:25.438Z | write:c:/моя/проекты/claude-brain | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
| 2026-06-18T06:10:10.586Z | write:c:/моя/проекты/портал crm/документация | разговорный режим: только думать/спрашивать (реализация — после печати артефакта и плана) |
## Метрики (информационные, не алерты)
- Observer evidence: 2092 episodes this month, 0 observer_error markers, 33 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 2092
- Last /brain-retro: 21 day(s) ago
- Observer evidence: 2177 episodes this month, 0 observer_error markers, 38 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 2177
- Last /brain-retro: 22 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
## Метрики дисциплины
@@ -78,16 +78,16 @@ Baseline дисциплины роутера (этап 2 router discipline overh
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|---|---|---|---|
| planning | 244 | 5.3% | 16.8% |
| feature | 67 | 4.5% | 4.5% |
| bugfix | 56 | 7.1% | 12.5% |
| analysis | 49 | 6.1% | 0.0% |
| planning | 247 | 5.3% | 16.6% |
| feature | 74 | 4.1% | 4.1% |
| bugfix | 58 | 6.9% | 12.1% |
| analysis | 50 | 6.0% | 2.0% |
| cleanup | 2 | 0.0% | 0.0% |
| refactor | 1 | 0.0% | 0.0% |
Router step distribution: 1: 1120, 2: 709, 3: 38, 5: 196
Router step distribution: 1: 1162, 2: 747, 3: 40, 5: 199
Boundaries applied (ADR / границы): 31 of 2063 эпизодов (1.5%).
Boundaries applied (ADR / границы): 33 of 2148 эпизодов (1.5%).
## Активные многоэтапные проекты
@@ -99,11 +99,11 @@ Boundaries applied (ADR / границы): 31 of 2063 эпизодов (1.5%).
## Длинные сессии
⚠️ Сегодня (2026-06-17 UTC) есть сессии с ≥50 ходов — корреляция с падением дисциплины роутинга (retro #5 candidate B).
⚠️ Сегодня (2026-06-18 UTC) есть сессии с ≥50 ходов — корреляция с падением дисциплины роутинга (retro #5 candidate B).
| session_id | макс. ход | % regulated | последний эпизод |
|---|---|---|---|
| `8813f1a6` | 67 | 2% | 2026-06-17T14:52:06.332Z |
| `ffef16cb` | 50 | 3% | 2026-06-18T05:23:41.663Z |
Long sessions correlate with discipline drift. Если % regulated просел в текущей сессии — рассмотри перезапуск.
@@ -127,7 +127,7 @@ Episodes since last run: 542 / threshold: 10
## Reviewer: субагент vs fallback
0 эпизодов проверено из 2092.
0 эпизодов проверено из 2177.
## Reviewer findings
@@ -153,8 +153,9 @@ Episodes since last run: 542 / threshold: 10
| PID | Имя | CPU-время | Возраст |
|---|---|---|---|
| 3420 | MsMpEng | 2.13ч | 0.0ч |
| 18808 | php | 1.30ч | 0.0ч |
| 3420 | MsMpEng | 2.91ч | 0.0ч |
| 18808 | php | 1.30ч | NaNч |
| 1104 | svchost | 1.21ч | 12284958.2ч |
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
@@ -168,7 +169,7 @@ Episodes since last run: 542 / threshold: 10
## Целостность журналов действий
🔴 Битые цепочки (3 из 69):
🔴 Битые цепочки (3 из 76):
| session | broken at seq |
|---|---|