{{ post.title }}
{{ post.content }}
{# Le code HTML est échappé automatiquement #} {{ userComment }} {# Si userComment = "" #} {# Sera affiché comme : <script>alert('XSS')</script> #} ``` ### 2. Filtres d'échappement personnalisés ```twig {# Échappement HTML (par défaut) #} {{ user.bio|e('html') }} {# Échappement JavaScript #} {# Échappement CSS #} .user-color { color: {{ user.favoriteColor|e('css') }}; } {# Échappement URL #} Site web {# Échappement d'attributs HTML #} ``` ### 3. Gestion du contenu HTML sécurisé Si vous devez afficher du HTML, utilisez un sanitizer : ```bash composer require tgalopin/html-sanitizer ``` ```php // src/Service/ContentSanitizer.php namespace App\Service; use HtmlSanitizer\SanitizerInterface; class ContentSanitizer { public function __construct( private SanitizerInterface $sanitizer ) {} public function sanitize(string $content): string { return $this->sanitizer->sanitize($content); } } ``` Configuration du sanitizer : ```yaml # config/packages/html_sanitizer.yaml framework: html_sanitizer: sanitizers: app.sanitizer: allowed_elements: - p - a - strong - em - ul - ol - li allowed_attributes: a: ['href', 'title'] allowed_hosts: - example.com allow_relative_links: true ``` Utilisation dans le contrôleur : ```php // src/Controller/BlogController.php use App\Service\ContentSanitizer; class BlogController extends AbstractController { #[Route('/blog/post/create', name: 'blog_create')] public function create( Request $request, ContentSanitizer $sanitizer ): Response { $c>request->get('content'); $sanitizedC>sanitize($content); $post = new BlogPost(); $post->setContent($sanitizedContent); // Sauvegarde en base de données return $this->redirectToRoute('blog_show', ['id' => $post->getId()]); } } ``` Dans Twig, marquez le contenu comme sûr : ```twig {# templates/blog/show.html.twig #} {{ post.content|raw }} {# Utilisez |raw UNIQUEMENT sur du contenu sanitizé ! #} ``` ### 4. Protection contre le XSS dans les attributs JSON ```php // src/Twig/JsonEncodeExtension.php namespace App\Twig; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; class JsonEncodeExtension extends AbstractExtension { public function getFilters(): array { return [ new TwigFilter('json_encode_safe', [$this, 'jsonEncodeSafe']), ]; } public function jsonEncodeSafe($value): string { return json_encode( $value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT ); } } ``` Utilisation : ```twig ``` ## Content Security Policy (CSP) La CSP est une couche de sécurité supplémentaire contre le XSS : ```php // src/EventListener/CspListener.php namespace App\EventListener; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; #[AsEventListener(event: KernelEvents::RESPONSE)] class CspListener { public function __invoke(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $resp>getResponse(); $csp = implode('; ', [ "default-src 'self'", "script-src 'self' 'unsafe-inline' https://cdn.example.com", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self'", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'" ]); $response->headers->set('Content-Security-Policy', $csp); } } ``` Enregistrement du listener : ```yaml # config/services.yaml services: App\EventListener\CspListener: tags: - { name: kernel.event_listener, event: kernel.response } ``` ## En-têtes de sécurité supplémentaires ```php // src/EventListener/SecurityHeadersListener.php namespace App\EventListener; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; #[AsEventListener(event: KernelEvents::RESPONSE)] class SecurityHeadersListener { public function __invoke(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $resp>getResponse(); // Empêche le navigateur d'interpréter les fichiers comme autre chose que leur type déclaré $response->headers->set('X-Content-Type-Options', 'nosniff'); // Protection contre le clickjacking $response->headers->set('X-Frame-Options', 'DENY'); // Active le filtre XSS du navigateur $response->headers->set('X-XSS-Protection', '1; mode=block'); // Force HTTPS $response->headers->set( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload' ); // Contrôle du référent $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); // Permissions Policy $response->headers->set( 'Permissions-Policy', 'geolocation=(), microph camera=()' ); } } ``` ## Validation et assainissement des entrées ```php // src/Validator/Constraints/NoHtmlTags.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; #[\Attribute] class NoHtmlTags extends Constraint { public string $message = 'La valeur "{{ value }}" contient des balises HTML non autorisées.'; } ``` ```php // src/Validator/Constraints/NoHtmlTagsValidator.php namespace App\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; class NoHtmlTagsValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (!$constraint instanceof NoHtmlTags) { throw new UnexpectedTypeException($constraint, NoHtmlTags::class); } if (null === $value || '' === $value) { return; } if ($value !== strip_tags($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $value) ->addViolation(); } } } ``` Utilisation dans une entité : ```php // src/Entity/Comment.php namespace App\Entity; use App\Validator\Constraints as AppAssert; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] class Comment { #[ORM\Column(type: 'text')] #[Assert\NotBlank] #[Assert\Length(max: 1000)] #[AppAssert\NoHtmlTags] private string $content; // Getters et setters } ``` ## Tests de sécurité ```php // tests/Security/CsrfProtectionTest.php namespace App\Tests\Security; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class CsrfProtectionTest extends WebTestCase { public function testFormSubmissionWithoutCsrfTokenFails(): void { $client = static::createClient(); // Soumettre un formulaire sans token CSRF $client->request('POST', '/product/new', [ 'product' => [ 'name' => 'Test Product', 'price' => 99.99, ] ]); // La soumission doit échouer $this->assertResponseStatusCodeSame(400); } public function testFormSubmissionWithValidCsrfTokenSucceeds(): void { $client = static::createClient(); $crawler = $client->request('GET', '/product/new'); // Extraire le token CSRF du formulaire $form = $crawler->selectButton('Créer le produit')->form(); // Remplir et soumettre le formulaire avec le token $client->submit($form, [ 'product[name]' => 'Test Product', 'product[price]' => 99.99, ]); $this->assertResponseRedirects('/product/success'); } } ``` ```php // tests/Security/XssProtectionTest.php namespace App\Tests\Security; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class XssProtectionTest extends WebTestCase { public function testUserInputIsEscaped(): void { $client = static::createClient(); $xssPayload = ''; $crawler = $client->request('GET', '/comment/create', [ 'content' => $xssPayload ]); // Vérifier que le contenu est échappé $c>getResponse()->getContent(); $this->assertStringNotContainsString('', $content); $this->assertStringContainsString('<script>', $content); } } ``` ## Meilleures pratiques ### 1. Principe de défense en profondeur - **Ne jamais** désactiver la protection CSRF sauf si absolument nécessaire - **Toujours** valider et assainir les entrées utilisateur - **Utiliser** plusieurs couches de protection (validation, échappement, CSP) ### 2. Checklist de sécurité ```yaml # security_checklist.yaml security_audit: csrf_protection: - Vérifier que csrf_protection est activé dans framework.yaml - Tous les formulaires utilisent la protection CSRF - Les endpoints API vérifient les tokens CSRF xss_protection: - Échappement automatique Twig activé - Utilisation de |raw uniquement sur contenu sanitizé - HTML Sanitizer configuré pour le contenu riche - CSP correctement configurée headers: - Content-Security-Policy défini - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - Strict-Transport-Security configuré validation: - Validation côté serveur pour toutes les entrées - Contraintes personnalisées pour les cas spécifiques - Tests de sécurité automatisés ``` ### 3. Surveillance et logging ```php // src/EventListener/SecurityEventListener.php namespace App\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; #[AsEventListener(event: KernelEvents::EXCEPTION)] class SecurityEventListener { public function __construct( private LoggerInterface $logger ) {} public function __invoke(ExceptionEvent $event): void { $exception = $event->getThrowable(); if ($exception instanceof TokenNotFoundException) { $request = $event->getRequest(); $this->logger->warning('Tentative CSRF détectée', [ 'ip' => $request->getClientIp(), 'uri' => $request->getRequestUri(), 'user_agent' => $request->headers->get('User-Agent'), 'referer' => $request->headers->get('referer') ]); } } } ``` ## Conclusion La sécurisation d'une application Symfony contre les attaques CSRF et XSS nécessite une approche multi-couches : 1. **Protection CSRF** : Utilisez les tokens CSRF pour tous les formulaires et requêtes modifiant des données 2. **Protection XSS** : Profitez de l'échappement automatique de Twig et sanitizez le contenu HTML 3. **En-têtes de sécurité** : Implémentez CSP et autres en-têtes de sécurité 4. **Validation** : Validez et assainissez toutes les entrées utilisateur 5. **Tests** : Automatisez les tests de sécurité 6. **Surveillance** : Loggez les tentatives d'attaques pour analyse Symfony fournit des outils puissants pour sécuriser votre application, mais leur configuration correcte et leur utilisation cohérente sont essentielles. Restez vigilant, maintenez vos dépendances à jour et effectuez des audits de sécurité réguliers. ## Ressources supplémentaires - [Documentation Symfony Security](https://symfony.com/doc/current/security.html) - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [Content Security Policy Guide](https://content-security-policy.com/) - [HTML Sanitizer Bundle](https://symfony.com/doc/current/html_sanitizer.html)