From e2780ca5f69cc6b32ff60d0ee3ad0c953be40efd Mon Sep 17 00:00:00 2001 From: Lyra Bot Date: Mon, 17 Aug 2026 11:27:46 -0400 Subject: [PATCH] fix: allow same-origin fetch in CSP when inline login script is used When subdomain redirection is off, the login form is served inline on the protected host and submission happens via a same-origin fetch() call in _script.html.twig. The CSP default-src 'none' was blocking that fetch (connect-src falls back to default-src). Add connect-src 'self' to the CSP only when the request is not on the auth subdomain (i.e. when the inline script is present). On the auth subdomain the form POSTs normally with no inline script, so the stricter policy still applies there. This is the least-privilege relaxation: only same-origin connections, only on pages that need them. --- src/Listener/SecurityHeadersListener.php | 29 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Listener/SecurityHeadersListener.php b/src/Listener/SecurityHeadersListener.php index 9c05013..b0dd34e 100644 --- a/src/Listener/SecurityHeadersListener.php +++ b/src/Listener/SecurityHeadersListener.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Listener; +use App\Service\DomainInterface; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ResponseEvent; @@ -15,6 +16,11 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent; */ final readonly class SecurityHeadersListener { + public function __construct( + private DomainInterface $domainManager, + ) { + } + #[AsEventListener(priority: 0)] public function onKernelResponse(ResponseEvent $event): void { @@ -36,11 +42,24 @@ final readonly class SecurityHeadersListener /* Content-Security-Policy — the login page uses inline styles * and scripts (via Twig includes), so we allow 'unsafe-inline' - * for those. No external resources are loaded. */ - $headers->set( - 'Content-Security-Policy', - "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';" - ); + * for those. No external resources are loaded. + * + * When subdomain redirection is off (or the request is not on + * the auth subdomain), the login form is served inline on the + * protected host and submission is performed via a same-origin + * fetch() call in _script.html.twig. That fetch is blocked by + * the default 'none' policy, so we add connect-src 'self' only + * in that case — the least privilege needed to make the form + * work. On the auth subdomain the form POSTs normally and no + * inline script is included, so the stricter policy applies. */ + $inlineScript = $this->domainManager->getAuthSubdomain() !== $event->getRequest()->getHost(); + $csp = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';"; + + if ($inlineScript) { + $csp .= " connect-src 'self';"; + } + + $headers->set('Content-Security-Policy', $csp); /* HSTS — enforce HTTPS for one year (app is designed for HTTPS behind a proxy) */ $headers->set('Strict-Transport-Security', 'max-age=31536000');