179 lines
5.2 KiB
PHP
179 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\View;
|
|
use Illuminate\Support\Facades\Vite;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class ContentSecurityPolicy
|
|
{
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$scriptNonce = base64_encode(random_bytes(16));
|
|
$styleNonce = base64_encode(random_bytes(16));
|
|
|
|
$request->attributes->set('csp_script_nonce', $scriptNonce);
|
|
$request->attributes->set('csp_style_nonce', $styleNonce);
|
|
|
|
View::share('cspNonce', $scriptNonce);
|
|
View::share('cspStyleNonce', $styleNonce);
|
|
|
|
Vite::useCspNonce($scriptNonce);
|
|
|
|
$response = $next($request);
|
|
|
|
if (app()->environment('local') || config('app.debug')) {
|
|
return $response;
|
|
}
|
|
|
|
if ($response->headers->has('Content-Security-Policy')) {
|
|
return $response;
|
|
}
|
|
|
|
$allowUnsafeInlineStyles = $request->is('event-admin*');
|
|
|
|
$matomoOrigin = $this->normaliseOrigin(config('services.matomo.url'));
|
|
$scriptSources = [
|
|
"'self'",
|
|
"'nonce-{$scriptNonce}'",
|
|
'https://cdn.paddle.com',
|
|
'https://global.localizecdn.com',
|
|
];
|
|
|
|
$styleSources = [
|
|
"'self'",
|
|
"'nonce-{$styleNonce}'",
|
|
'https:',
|
|
];
|
|
$styleElemSources = [];
|
|
if ($allowUnsafeInlineStyles) {
|
|
$styleElemSources = [
|
|
"'self'",
|
|
"'unsafe-inline'",
|
|
'https:',
|
|
'data:',
|
|
];
|
|
}
|
|
|
|
$connectSources = [
|
|
"'self'",
|
|
'https://api.paddle.com',
|
|
'https://sandbox-api.paddle.com',
|
|
'https://checkout.paddle.com',
|
|
'https://sandbox-checkout.paddle.com',
|
|
'https://checkout-service.paddle.com',
|
|
'https://sandbox-checkout-service.paddle.com',
|
|
'https://global.localizecdn.com',
|
|
];
|
|
|
|
$frameSources = [
|
|
"'self'",
|
|
'https://checkout.paddle.com',
|
|
'https://sandbox-checkout.paddle.com',
|
|
'https://checkout-service.paddle.com',
|
|
'https://sandbox-checkout-service.paddle.com',
|
|
];
|
|
|
|
$imgSources = [
|
|
"'self'",
|
|
'data:',
|
|
'blob:',
|
|
'https:',
|
|
];
|
|
|
|
$fontSources = [
|
|
"'self'",
|
|
'data:',
|
|
'https:',
|
|
];
|
|
|
|
$mediaSources = [
|
|
"'self'",
|
|
'data:',
|
|
'blob:',
|
|
'https:',
|
|
];
|
|
|
|
if ($matomoOrigin) {
|
|
$scriptSources[] = $matomoOrigin;
|
|
$connectSources[] = $matomoOrigin;
|
|
$imgSources[] = $matomoOrigin;
|
|
}
|
|
|
|
$isDev = app()->environment(['local', 'development']) || config('app.debug');
|
|
|
|
if ($isDev) {
|
|
$devHosts = [
|
|
'http://fotospiel-app.test:5173',
|
|
'http://127.0.0.1:5173',
|
|
'https://localhost:5173',
|
|
'https://127.0.0.1:5173',
|
|
];
|
|
$wsHosts = [
|
|
'ws://fotospiel-app.test:5173',
|
|
'ws://127.0.0.1:5173',
|
|
'wss://localhost:5173',
|
|
'wss://127.0.0.1:5173',
|
|
];
|
|
|
|
$scriptSources = array_merge($scriptSources, $devHosts, ["'unsafe-inline'", "'unsafe-eval'"]);
|
|
$styleSources = array_merge($styleSources, $devHosts, ["'unsafe-inline'"]);
|
|
$connectSources = array_merge($connectSources, $devHosts, $wsHosts);
|
|
$fontSources = array_merge($fontSources, $devHosts);
|
|
$mediaSources = array_merge($mediaSources, $devHosts);
|
|
}
|
|
|
|
$styleSources[] = 'data:';
|
|
$connectSources[] = 'https:';
|
|
$fontSources[] = 'https:';
|
|
|
|
$directives = [
|
|
'default-src' => ["'self'"],
|
|
'script-src' => array_unique($scriptSources),
|
|
'style-src' => array_unique($styleSources),
|
|
'style-src-elem' => $styleElemSources,
|
|
'style-src-attr' => ["'unsafe-inline'"],
|
|
'img-src' => array_unique($imgSources),
|
|
'font-src' => array_unique($fontSources),
|
|
'connect-src' => array_unique($connectSources),
|
|
'media-src' => array_unique($mediaSources),
|
|
'frame-src' => array_unique($frameSources),
|
|
'form-action' => ["'self'"],
|
|
'base-uri' => ["'self'"],
|
|
'object-src' => ["'none'"],
|
|
'frame-ancestors' => ["'self'"],
|
|
];
|
|
|
|
$csp = collect($directives)
|
|
->map(fn ($values, $directive) => $directive.' '.implode(' ', array_filter($values)))
|
|
->implode('; ');
|
|
|
|
$response->headers->set('Content-Security-Policy', $csp);
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function normaliseOrigin(?string $url): ?string
|
|
{
|
|
if (! $url) {
|
|
return null;
|
|
}
|
|
|
|
$parsed = parse_url($url);
|
|
if (! $parsed || ! isset($parsed['scheme'], $parsed['host'])) {
|
|
return null;
|
|
}
|
|
|
|
$origin = strtolower($parsed['scheme'].'://'.$parsed['host']);
|
|
|
|
if (isset($parsed['port'])) {
|
|
$origin .= ':'.$parsed['port'];
|
|
}
|
|
|
|
return $origin;
|
|
}
|
|
}
|