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:
@@ -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'",
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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-систем без сессии браузера.
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|---|---|
|
||||
|
||||
Reference in New Issue
Block a user