From c2bdfdd7d39f9db99f350fbb64c7624eff5e2718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 18 Jun 2026 09:31:47 +0300 Subject: [PATCH] =?UTF-8?q?refactor(security):=20=D0=B5=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=20security-=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=20=E2=80=94=20nginx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Убраны дубли 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 --- app/app/Http/Middleware/SecurityHeaders.php | 64 ------------------ app/bootstrap/app.php | 9 ++- app/tests/Feature/SecurityHeadersTest.php | 40 +++++------ docs/observer/STATUS.md | 75 +++++++++++---------- 4 files changed, 59 insertions(+), 129 deletions(-) delete mode 100644 app/app/Http/Middleware/SecurityHeaders.php diff --git a/app/app/Http/Middleware/SecurityHeaders.php b/app/app/Http/Middleware/SecurityHeaders.php deleted file mode 100644 index 73fb6a19..00000000 --- a/app/app/Http/Middleware/SecurityHeaders.php +++ /dev/null @@ -1,64 +0,0 @@ -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'", - ]); - } -} diff --git a/app/bootstrap/app.php b/app/bootstrap/app.php index 58be76ce..6b895de4 100644 --- a/app/bootstrap/app.php +++ b/app/bootstrap/app.php @@ -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-систем без сессии браузера. diff --git a/app/tests/Feature/SecurityHeadersTest.php b/app/tests/Feature/SecurityHeadersTest.php index 9c25761f..2a9bb3ac 100644 --- a/app/tests/Feature/SecurityHeadersTest.php +++ b/app/tests/Feature/SecurityHeadersTest.php @@ -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'); }); diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index fab77d03..1f55a049 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -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 | |---|---|