<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* SÉCURITÉ (protection CSRF sans modifier les formulaires) : les actions qui modifient des données
* (POST/PUT/DELETE, et toute adresse contenant "delete", "supprim", "suppr", "remove") doivent venir
* d'une page du backoffice lui-même. On compare l'en-tête Origin (ou à défaut Referer) à l'hôte du site.
* Un lien piégé sur un autre site ne peut donc plus déclencher une suppression.
*/
class RequestOriginSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [KernelEvents::REQUEST => ['onKernelRequest', 6]];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$route = (string) $request->attributes->get('_route', '');
if ($route === '' || $route[0] === '_') {
return;
}
$safeMethod = \in_array($request->getMethod(), ['GET', 'HEAD', 'OPTIONS'], true);
$sensitivePath = (bool) preg_match('~(delete|supprim|suppr|remove)~i', $request->getPathInfo());
if ($safeMethod && !$sensitivePath) {
return;
}
$source = $request->headers->get('Origin');
if ($source === null || $source === '') {
$source = $request->headers->get('Referer');
}
if ($source === null || $source === '') {
// Aucune indication d'origine : on tolère un POST (cookie SameSite=Lax), pas une suppression par simple lien.
if ($safeMethod) {
$event->setResponse($this->refus());
}
return;
}
if (!$this->memeHote($source, $request)) {
$event->setResponse($this->refus());
}
}
private function memeHote(string $source, Request $request): bool
{
$p = parse_url($source);
if (!\is_array($p) || empty($p['host'])) {
return false;
}
$hote = strtolower($p['host']);
$port = $p['port'] ?? (($p['scheme'] ?? '') === 'https' ? 443 : 80);
$attendu = strtolower($request->getHost());
$portAttendu = $request->getPort();
return $hote === $attendu && (int) $port === (int) $portAttendu;
}
private function refus(): Response
{
return new Response('Requête refusée (origine invalide).', 403, ['Content-Type' => 'text/plain; charset=UTF-8']);
}
}