Bonjour {{ user.name }}
{# Si vous devez afficher du HTML de confiance #} {{ content|raw }} {# Échappement pour JavaScript #} {# Échappement pour attributs HTML #} ``` ### Content Security Policy (CSP) : ```php // src/EventListener/SecurityHeadersListener.php namespace App\EventListener; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener(event: 'kernel.response')] class SecurityHeadersListener { public function onKernelResponse(ResponseEvent $event): void { $resp>getResponse(); // Content Security Policy $response->headers->set( 'Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" ); // Autres headers de sécurité $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-Frame-Options', 'DENY'); $response->headers->set('X-XSS-Protection', '1; mode=block'); $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); $response->headers->set('Permissions-Policy', 'geolocation=(), microph camera=()'); // HTTPS strict $response->headers->set( 'Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload' ); } } ``` ## 5. Validation et sanitisation des données ### Utilisation des contraintes de validation : ```php // src/Entity/User.php namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; #[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé')] 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: 8, max: 4096, 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 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; #[Route('/api/user', name: 'api_user_create', methods: ['POST'])] public function create( Request $request, ValidatorInterface $validator, EntityManagerInterface $entityManager ): JsonResponse { $data = json_decode($request->getContent(), true); $user = new User(); $user->setEmail($data['email'] ?? ''); $user->setName($data['name'] ?? ''); // Validation $errors = $validator->validate($user); if (count($errors) > 0) { $errorMessages = []; foreach ($errors as $error) { $errorMessages[$error->getPropertyPath()] = $error->getMessage(); } return new JsonResponse(['errors' => $errorMessages], 400); } $entityManager->persist($user); $entityManager->flush(); return new JsonResponse(['id' => $user->getId()], 201); } ``` ## 6. Contrôle d'accès et autorisation ### Utilisation des Voters pour une logique d'autorisation complexe : ```php // src/Security/Voter/PostVoter.php namespace App\Security\Voter; use App\Entity\Post; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class PostVoter extends Voter { const VIEW = 'view'; const EDIT = 'edit'; const DELETE = 'delete'; protected function supports(string $attribute, mixed $subject): bool { return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE]) && $subject instanceof Post; } protected function voteOnAttribute( string $attribute, mixed $subject, TokenInterface $token ): bool { $user = $token->getUser(); if (!$user instanceof User) { return false; } /** @var Post $post */ $post = $subject; return match($attribute) { self::VIEW => $this->canView($post, $user), self::EDIT => $this->canEdit($post, $user), self::DELETE => $this->canDelete($post, $user), default => false, }; } private function canView(Post $post, User $user): bool { // Les admins peuvent tout voir if (in_array('ROLE_ADMIN', $user->getRoles())) { return true; } // Les posts publiés sont visibles par tous if ($post->isPublished()) { return true; } // Les auteurs peuvent voir leurs propres brouillons return $post->getAuthor() === $user; } private function canEdit(Post $post, User $user): bool { return $user === $post->getAuthor() || in_array('ROLE_ADMIN', $user->getRoles()); } private function canDelete(Post $post, User $user): bool { return in_array('ROLE_ADMIN', $user->getRoles()); } } ``` ### Utilisation dans les contrôleurs : ```php use Symfony\Component\Security\Http\Attribute\IsGranted; class PostController extends AbstractController { #[Route('/post/{id}', name: 'post_show')] public function show(Post $post): Response { // Vérifie l'autorisation $this->denyAccessUnlessGranted('view', $post); return $this->render('post/show.html.twig', [ 'post' => $post, ]); } #[Route('/post/{id}/edit', name: 'post_edit')] #[IsGranted('edit', subject: 'post')] public function edit(Post $post, Request $request): Response { // Le voter a déjà vérifié les permissions $form = $this->createForm(PostType::class, $post); // ... } } ``` ## 7. Protection contre les attaques par force brute ### Rate Limiting avec le composant RateLimiter : ```bash composer require symfony/rate-limiter ``` ### Configuration : ```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' } ``` ### Implémentation : ```php use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; class LoginController extends AbstractController { #[Route('/login', name: 'app_login', methods: ['POST'])] public function login( Request $request, RateLimiterFactory $loginLimiter ): Response { $limiter = $loginLimiter->create($request->getClientIp()); if (false === $limiter->consume(1)->isAccepted()) { throw new TooManyRequestsHttpException( null, 'Trop de tentatives de connexion. Veuillez réessayer plus tard.' ); } // Logique de connexion } } ``` ## 8. Sécurisation des sessions ### Configuration des sessions : ```yaml # config/packages/framework.yaml framework: session: handler_id: null cookie_secure: auto # Force HTTPS en production cookie_httponly: true cookie_samesite: 'lax' gc_probability: 1 gc_divisor: 100 gc_maxlifetime: 3600 # 1 heure name: 'SECURE_SESSION_ID' # Nom personnalisé ``` ### Régénération de l'ID de session après connexion : ```php use Symfony\Component\HttpFoundation\RequestStack; class SecurityService { public function __construct( private RequestStack $requestStack ) {} public function regenerateSession(): void { $session = $this->requestStack->getSession(); // Sauvegarde des données importantes $data = $session->all(); // Régénère l'ID $session->invalidate(); // Restaure les données foreach ($data as $key => $value) { $session->set($key, $value); } } } ``` ## 9. Logging et monitoring de sécurité ### Configuration Monolog pour les événements de 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_errors: type: stream path: '%kernel.logs_dir%/security_errors.log' level: error channels: ['security'] ``` ### Event Listener pour logger les tentatives de connexion : ```php // src/EventListener/SecurityEventsListener.php namespace App\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; #[AsEventListener(event: LoginSuccessEvent::class)] #[AsEventListener(event: LoginFailureEvent::class)] class SecurityEventsListener { public function __construct( private LoggerInterface $securityLogger ) {} public function onLoginSuccess(LoginSuccessEvent $event): void { $user = $event->getUser(); $request = $event->getRequest(); $this->securityLogger->info('Connexion réussie', [ 'username' => $user->getUserIdentifier(), 'ip' => $request->getClientIp(), 'user_agent' => $request->headers->get('User-Agent'), 'timestamp' => new \DateTime(), ]); } public function onLoginFailure(LoginFailureEvent $event): void { $request = $event->getRequest(); $exception = $event->getException(); $this->securityLogger->warning('Échec de connexion', [ 'username' => $request->request->get('_username'), 'ip' => $request->getClientIp(), 'reason' => $exception->getMessage(), 'user_agent' => $request->headers->get('User-Agent'), 'timestamp' => new \DateTime(), ]); } } ``` ## 10. Sécurisation des API ### Authentification JWT : ```bash composer require lexik/jwt-authentication-bundle ``` ### Configuration : ```yaml # config/packages/lexik_jwt_authentication.yaml lexik_jwt_authentication: secret_key: '%env(resolve:JWT_SECRET_KEY)%' public_key: '%env(resolve:JWT_PUBLIC_KEY)%' pass_phrase: '%env(JWT_PASSPHRASE)%' token_ttl: 3600 ``` ### Configuration du firewall : ```yaml # config/packages/security.yaml security: firewalls: api: pattern: ^/api stateless: true jwt: ~ ``` ### Contrôleur d'authentification API : ```php namespace App\Controller\Api; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\Security\Core\User\UserInterface; class AuthController extends AbstractController { #[Route('/api/login', name: 'api_login', methods: ['POST'])] public function login( Request $request, UserRepository $userRepository, UserPasswordHasherInterface $passwordHasher, JWTTokenManagerInterface $JWTManager ): JsonResponse { $data = json_decode($request->getContent(), true); $user = $userRepository->findOneBy(['email' => $data['email'] ?? '']); if (!$user || !$passwordHasher->isPasswordValid($user, $data['password'] ?? '')) { return new JsonResponse(['error' => 'Invalid credentials'], 401); } $token = $JWTManager->create($user); return new JsonResponse([ 'token' => $token, 'refresh_token' => $this->generateRefreshToken($user), ]); } } ``` ## Checklist de sécurité Symfony ### Avant de mettre en production : - [ ] Activer HTTPS avec certificat SSL/TLS valide - [ ] Configurer les headers de sécurité (CSP, HSTS, X-Frame-Options) - [ ] Vérifier que `APP_ENV=prod` et `APP_DEBUG=false` - [ ] Désactiver le profiler et le Web Debug Toolbar - [ ] Utiliser des secrets sécurisés (Vault, variables d'environnement) - [ ] Configurer le CSRF pour tous les formulaires - [ ] Implémenter le rate limiting sur les endpoints sensibles - [ ] Valider et sanitiser toutes les entrées utilisateur - [ ] Utiliser les requêtes préparées pour toutes les requêtes SQL - [ ] Configurer correctement les permissions de fichiers (var/, config/) - [ ] Activer les logs de sécurité et configurer le monitoring - [ ] Mettre à jour régulièrement Symfony et ses dépendances - [ ] Effectuer un audit de sécurité avec `symfony security:check` - [ ] Configurer des sauvegardes automatiques de la base de données - [ ] Implémenter une politique de mots de passe forte - [ ] Tester avec OWASP ZAP ou un scanner de vulnérabilités ## Outils de sécurité recommandés ### Scanner de vulnérabilités : ```bash # Symfony CLI security checker symfony security:check # Local PHP Security Checker composer require --dev local-php-security-checker local-php-security-checker # Audit des dépendances composer audit ``` ### Analyse statique du code : ```bash composer require --dev phpstan/phpstan vendor/bin/phpstan analyse src composer require --dev vimeo/psalm vendor/bin/psalm --init vendor/bin/psalm ``` ## Conclusion La sécurité est un processus continu, pas une fonctionnalité qu'on ajoute à la fin. Symfony offre des outils puissants pour sécuriser vos applications, mais ils doivent être correctement configurés et utilisés. Les points clés à retenir : 1. **Toujours valider et sanitiser** les entrées utilisateur 2. **Ne jamais faire confiance** aux données côté client 3. **Utiliser les composants Symfony** plutôt que de réinventer la roue 4. **Maintenir à jour** vos dépendances régulièrement 5. **Logger les événements de sécurité** pour détecter les attaques 6. **Tester régulièrement** vos applications avec des outils d'audit 7. **Former votre équipe** aux bonnes pratiques de sécurité La sécurité doit être intégrée dès le début du développement et réévaluée régulièrement. Utilisez les audits de sécurité, effectuez des tests de pénétration et restez informé des nouvelles vulnérabilités. N'oubliez pas : une application sécurisée est une application qui protège à la fois vos utilisateurs et votre entreprise.Sécuriser votre application Symfony : Guide complet contre les vulnérabilités OWASP Top 10
# Introduction La sécurité des applications web est devenue une préoccupation majeure en 2024. Symfony, l'un des frameworks PHP les plus robustes, offre de nom
Sécuriser votre application Symfony : Guide complet contre les vulnérabilités OWASP Top 10
15/01/2024