Bienvenue {{ user.name }}
{# ⚠️ ATTENTION - Désactivation de l'échappement #} {{ content|raw }} {# ✅ BON - Échappement selon le contexte #} {# ✅ BON - Utilisation de filtres appropriés #} {{ user.name }} ``` ## 3. Configuration CSRF (Cross-Site Request Forgery) Symfony intègre une protection CSRF robuste pour les formulaires. ### Activation globale de la protection CSRF ```yaml # config/packages/framework.yaml framework: csrf_protection: true secret: '%env(APP_SECRET)%' ``` ### Utilisation dans les formulaires ```php use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('email', TextType::class) ->add('save', SubmitType::class); // CSRF activé par défaut } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'data_class' => User::class, 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_token_id' => 'user_item', ]); } } ``` ### Protection CSRF pour les requêtes AJAX ```php use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; class ApiController extends AbstractController { public function __construct( private CsrfTokenManagerInterface $csrfTokenManager ) {} #[Route('/api/delete/{id}', methods: ['DELETE'])] public function delete(Request $request, int $id): JsonResponse { $token = $request->headers->get('X-CSRF-Token'); if (!$this->csrfTokenManager->isTokenValid( new CsrfToken('delete_item', $token) )) { return $this->json(['error' => 'Invalid CSRF token'], 403); } // Traitement de la suppression return $this->json(['success' => true]); } } ``` ## 4. Gestion sécurisée de l'authentification ### Configuration du Security Component ```yaml # config/packages/security.yaml security: password_hashers: App\Entity\User: algorithm: auto cost: 12 providers: app_user_provider: entity: class: App\Entity\User property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true provider: app_user_provider form_login: login_path: app_login check_path: app_login enable_csrf: true max_attempts: 5 interval: '15 minutes' logout: path: app_logout invalidate_session: true clear_site_data: - cookies - storage - executionContexts remember_me: secret: '%kernel.secret%' lifetime: 604800 path: / secure: true httponly: true samesite: strict access_control: - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/profile, roles: ROLE_USER } ``` ### Implémentation d'un système de limitation de tentatives ```php use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\RateLimiter\RateLimiterFactory; class LoginAuthenticator extends AbstractLoginFormAuthenticator { public function __construct( private RateLimiterFactory $loginLimiter ) {} public function authenticate(Request $request): Passport { $email = $request->request->get('email', ''); $limiter = $this->loginLimiter->create($request->getClientIp()); if (false === $limiter->consume(1)->isAccepted()) { throw new TooManyRequestsHttpException(); } return new Passport( new UserBadge($email), new PasswordCredentials($request->request->get('password', '')), [new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token'))] ); } } ``` ### Configuration du Rate Limiter ```yaml # config/packages/rate_limiter.yaml framework: rate_limiter: login: policy: 'sliding_window' limit: 5 interval: '15 minutes' ``` ## 5. Sécurisation des en-têtes HTTP ### Configuration des en-têtes de sécurité ```yaml # config/packages/framework.yaml framework: http_client: default_options: headers: 'X-Frame-Options': 'SAMEORIGIN' 'X-Content-Type-Options': 'nosniff' 'X-XSS-Protection': '1; mode=block' 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains' 'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" 'Referrer-Policy': 'strict-origin-when-cross-origin' 'Permissions-Policy': 'geolocation=(), microph camera=()' ``` ### Création d'un Event Subscriber pour les en-têtes ```php use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; class SecurityHeadersSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ KernelEvents::RESP> 'onKernelResponse', ]; } public function onKernelResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $resp>getResponse(); $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-XSS-Protection', '1; mode=block'); $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); $csp = implode('; ', [ "default-src 'self'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'", "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); } } ``` ## 6. Validation et sanitisation des données ### Utilisation des contraintes de validation ```php use Symfony\Component\Validator\Constraints as Assert; class User { #[Assert\NotBlank(message: 'L\'email ne peut pas être vide')] #[Assert\Email(message: 'L\'email {{ value }} n\'est pas valide')] #[Assert\Length( max: 180, maxMessage: 'L\'email ne peut pas dépasser {{ limit }} caractères' )] private ?string $email = null; #[Assert\NotBlank] #[Assert\Length( min: 12, minMessage: 'Le mot de passe doit contenir au moins {{ limit }} caractères' )] #[Assert\Regex( pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/', message: 'Le mot de passe doit contenir au moins une majuscule, une minuscule, un chiffre et un caractère spécial' )] private ?string $plainPassword = null; #[Assert\NotBlank] #[Assert\Length(min: 2, max: 100)] #[Assert\Regex( pattern: '/^[a-zA-ZÀ-ÿ\s\-]+$/', message: 'Le nom ne peut contenir que des lettres, espaces et tirets' )] private ?string $name = null; } ``` ### Validation dans les contrôleurs ```php use Symfony\Component\Validator\Validator\ValidatorInterface; class UserController extends AbstractController { #[Route('/register', methods: ['POST'])] public function register( Request $request, ValidatorInterface $validator, UserPasswordHasherInterface $passwordHasher ): Response { $user = new User(); $data = json_decode($request->getContent(), true); $user->setEmail($data['email'] ?? ''); $user->setPlainPassword($data['password'] ?? ''); $user->setName($data['name'] ?? ''); $errors = $validator->validate($user); if (count($errors) > 0) { $errorMessages = []; foreach ($errors as $error) { $errorMessages[$error->getPropertyPath()] = $error->getMessage(); } return $this->json(['errors' => $errorMessages], 400); } // Hashage du mot de passe $hashedPassword = $passwordHasher->hashPassword( $user, $user->getPlainPassword() ); $user->setPassword($hashedPassword); // Sauvegarde en base $this->entityManager->persist($user); $this->entityManager->flush(); return $this->json(['success' => true], 201); } } ``` ## 7. Gestion sécurisée des fichiers uploadés ```php use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\String\Slugger\SluggerInterface; class FileUploadService { private const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf' ]; private const MAX_FILE_SIZE = 5242880; // 5MB public function __construct( private string $uploadDirectory, private SluggerInterface $slugger ) {} public function upload(UploadedFile $file): string { // Vérification du type MIME if (!in_array($file->getMimeType(), self::ALLOWED_MIME_TYPES)) { throw new \InvalidArgumentException('Type de fichier non autorisé'); } // Vérification de la taille if ($file->getSize() > self::MAX_FILE_SIZE) { throw new \InvalidArgumentException('Fichier trop volumineux'); } // Génération d'un nom de fichier sécurisé $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); $safeFilename = $this->slugger->slug($originalFilename); $fileName = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension(); try { $file->move($this->uploadDirectory, $fileName); } catch (FileException $e) { throw new \RuntimeException('Erreur lors de l\'upload du fichier'); } return $fileName; } } ``` ## 8. Protection contre les attaques par force brute ### Configuration avancée du Rate Limiter ```yaml # config/packages/rate_limiter.yaml framework: rate_limiter: login: policy: 'sliding_window' limit: 5 interval: '15 minutes' api: policy: 'token_bucket' limit: 100 rate: interval: '1 hour' password_reset: policy: 'fixed_window' limit: 3 interval: '1 hour' ``` ### Utilisation dans un contrôleur ```php use Symfony\Component\RateLimiter\RateLimiterFactory; class ApiController extends AbstractController { #[Route('/api/data', methods: ['GET'])] public function getData( Request $request, RateLimiterFactory $apiLimiter ): JsonResponse { $limiter = $apiLimiter->create($request->getClientIp()); if (false === $limiter->consume(1)->isAccepted()) { return $this->json([ 'error' => 'Trop de requêtes. Veuillez réessayer plus tard.' ], 429); } // Traitement normal return $this->json(['data' => 'votre contenu']); } } ``` ## 9. Journalisation des événements de sécurité ```php use Psr\Log\LoggerInterface; class SecurityEventSubscriber implements EventSubscriberInterface { public function __construct( private LoggerInterface $securityLogger ) {} public static function getSubscribedEvents(): array { return [ LoginSuccessEvent::class => 'onLoginSuccess', LoginFailureEvent::class => 'onLoginFailure', LogoutEvent::class => 'onLogout', AuthenticationFailureEvent::class => 'onAuthenticationFailure', ]; } public function onLoginSuccess(LoginSuccessEvent $event): void { $user = $event->getUser(); $request = $event->getRequest(); $this->securityLogger->info('Connexion réussie', [ 'user' => $user->getUserIdentifier(), 'ip' => $request->getClientIp(), 'user_agent' => $request->headers->get('User-Agent'), 'timestamp' => new \DateTime(), ]); } public function onLoginFailure(LoginFailureEvent $event): void { $request = $event->getRequest(); $this->securityLogger->warning('Tentative de connexion échouée', [ 'email' => $request->request->get('email'), 'ip' => $request->getClientIp(), 'reason' => $event->getException()->getMessage(), 'timestamp' => new \DateTime(), ]); } } ``` ## 10. Configuration de Monolog pour la sécurité ```yaml # config/packages/monolog.yaml monolog: channels: - security handlers: security: type: stream path: "%kernel.logs_dir%/security.log" level: info channels: [security] security_critical: type: stream path: "%kernel.logs_dir%/security_critical.log" level: error channels: [security] slack_security: type: slack token: '%env(SLACK_TOKEN)%' channel: '#security-alerts' level: critical channels: [security] ``` ## Conclusion La sécurisation d'une application Symfony nécessite une approche multicouche et une vigilance constante. En suivant ces bonnes pratiques, vous réduisez considérablement les risques d'attaques. ### Points clés à retenir : 1. **Toujours utiliser les mécanismes de sécurité natifs** de Symfony (Security Component, CSRF, validation) 2. **Ne jamais faire confiance aux données utilisateur** - validez et sanitisez systématiquement 3. **Mettre en place une stratégie de défense en profondeur** avec plusieurs couches de sécurité 4. **Journaliser tous les événements de sécurité** pour détecter les tentatives d'intrusion 5. **Maintenir Symfony et ses dépendances à jour** avec les derniers correctifs de sécurité 6. **Utiliser HTTPS en production** avec des certificats valides 7. **Implémenter des tests de sécurité** dans votre pipeline CI/CD 8. **Former l'équipe** aux bonnes pratiques de sécurité ### Ressources complémentaires - [Documentation officielle Symfony Security](https://symfony.com/doc/current/security.html) - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [Symfony Security Checker](https://github.com/fabpot/local-php-security-checker) - [Mozilla Observatory](https://observatory.mozilla.org/) La sécurité n'est pas un état final mais un processus continu d'amélioration et d'adaptation aux nouvelles menaces. Restez informé des dernières vulnérabilités et mettez régulièrement à jour vos pratiques de sécurité.Sécuriser votre application Symfony contre les vulnérabilités OWASP Top 10
# Introduction La sécurité des applications web est devenue un enjeu critique dans le développement moderne. Symfony, l'un des frameworks PHP les plus populair
Sécuriser votre application Symfony contre les vulnérabilités OWASP Top 10
15/01/2024