Files
portal/app/bootstrap/app.php
T
Дмитрий 84936929eb feat(security): middleware безопасных HTTP-заголовков — закрытие ZAP Medium anti-clickjacking
X-Frame-Options SAMEORIGIN + X-Content-Type-Options nosniff + Referrer-Policy на все web-ответы (go-live аудит 17.06). CSP вынесен отдельно (SPA Vue+Vuetify). TDD-тест на публичном /.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:12:32 +03:00

102 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use App\Http\Middleware\EnsureSaasAdmin;
use App\Http\Middleware\SetTenantContext;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
// /api/auth/* размещены в web.php (см. routes/web.php) и используют
// session-based Sanctum auth. Token-based mode (через api.php) пока
// не нужен — добавим если понадобится для интеграций.
$middleware->alias([
'tenant' => SetTenantContext::class,
'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]);
// Webhook receive endpoint (POST /api/webhook/{token}) не должен требовать
// CSRF — запросы приходят от внешних CRM-систем без сессии браузера.
// Авторизация — через webhook_token в URL + (на prod) HMAC.
$middleware->validateCsrfTokens(except: [
'api/webhook/*',
]);
})
->withExceptions(function (Exceptions $exceptions): void {
// Reduce verbosity of constraint-violation logging (SQLSTATE 23xxx):
// data-validity errors do not need a full stack trace в laravel.log.
// Incident 2026-05-29: 420k повторов B1+SMS check_violation накопили
// 8.7 GB stack traces → disk full → 4h prod downtime.
// Solution: log a warning summary с sqlstate, return false to stop
// default reporting (which would write full stack trace).
// Ref: docs/incidents/2026-05-29-disk-full-pg-recovery.md §5
$exceptions->reportable(function (QueryException $e) {
$sqlState = $e->errorInfo[0] ?? '';
if (is_string($sqlState) && str_starts_with($sqlState, '23')) {
Log::warning('db.constraint_violation', [
'sqlstate' => $sqlState,
'message' => mb_substr($e->getMessage(), 0, 200),
]);
return false; // skip default reporting (no stack trace в laravel.log)
}
return null; // continue default reporting для non-constraint QueryExceptions
});
$exceptions->render(function (QueryException $e, Request $request) {
$sqlState = $e->errorInfo[0] ?? '';
$isConstraintViolation = is_string($sqlState) && str_starts_with($sqlState, '23');
if (! $isConstraintViolation) {
// Default verbose log для non-constraint QueryExceptions (table missing,
// syntax error, etc. — these are bugs needing investigation).
Log::error('db.query_exception', [
'message' => $e->getMessage(),
'sql' => $e->getSql(),
'path' => $request->path(),
]);
}
// Constraint violations уже залогированы в reportable() выше как warning,
// дублировать не нужно.
if ($request->expectsJson()) {
return response()->json([
'message' => 'Не удалось сохранить. Проверьте данные или попробуйте ещё раз.',
], 422);
}
return null; // default render for non-JSON
});
// Supplier webhook always returns JSON, even when client omits Accept header.
// Without this render, Laravel's default ValidationException handler returns
// 302 redirect to /, which strips POST body — losing supplier leads.
// Confirmed 2026-05-25: 76 of 234 webhook hits today got 302 instead of 422.
$exceptions->render(function (ValidationException $e, Request $request) {
if ($request->is('api/webhook/supplier/*')) {
return response()->json([
'message' => 'Validation failed',
'errors' => $e->errors(),
], 422);
}
return null; // default render for other routes
});
})->create();