# config/packages/security.yaml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
logout:
path: app_logout
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }
<!-- URL malveillante -->
https://site.com/search?q=<script>alert('XSS')</script>
<!-- Affichage dangereux -->
<p>Résultats : <?php echo $_GET['q']; ?></p>
{# Template Twig - Échappement automatique #}
<p>Résultats : {{ query }}</p>
{# Échappement explicite #}
<p>Résultats : {{ query|e }}</p>
// src/EventListener/SecurityHeadersListener.php
class SecurityHeadersListener
{
public function onKernelResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
// CSP stricte avec nonce pour les scripts
$nonce = base64_encode(random_bytes(16));
$csp = "default-src 'self'; " .
"script-src 'self' 'nonce-{$nonce}'; " .
"style-src 'self' 'unsafe-inline'; " .
"img-src 'self' data: https:; " .
"connect-src 'self'; " .
"font-src 'self' https://fonts.gstatic.com; " .
"object-src 'none'; " .
"base-uri 'self'; " .
"form-action 'self'; " .
"frame-ancestors 'none'";
$response->headers->set('Content-Security-Policy', $csp);
// Headers de sécurité additionnels
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Stocker le nonce pour les templates
$event->getRequest()->attributes->set('csp_nonce', $nonce);
}
}
#[Route('/search')]
public function search(Request $request, ValidatorInterface $validator): Response
{
$query = $request->query->get('q', '');
// Validation avec contraintes Symfony
$violations = $validator->validate($query, [
new Assert\Length(['max' => 100]),
new Assert\NotBlank(),
new Assert\Regex([
'pattern' => '/^[a-zA-Z0-9\s\-_]*$/',
'message' => 'Caractères non autorisés'
])
]);
if (count($violations) > 0) {
throw new BadRequestException('Données invalides');
}
// Sanitisation supplémentaire
$query = strip_tags($query);
$query = htmlspecialchars($query, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return $this->render('search.html.twig', [
'query' => $query // Échappement automatique Twig
]);
}
// Concaténation directe - DANGEREUX !
$email = $_POST['email'];
$password = $_POST['password'];
$query = "SELECT * FROM users
WHERE email = '".$email."'
AND password = '".$password."'";
$result = mysqli_query($connection, $query);
// Doctrine utilise AUTOMATIQUEMENT les requêtes préparées
public function findByCredentials(string $email): ?User
{
return $this->createQueryBuilder('u')
->where('u.email = :email')
->setParameter('email', $email) // Paramètre préparé
->getQuery()
->getOneOrNullResult();
}
// Doctrine DBAL - Requêtes préparées manuelles
public function findUsersByCity(string $city): array
{
$conn = $this->getEntityManager()->getConnection();
// 1. Préparation : structure SQL avec placeholders
$sql = 'SELECT * FROM users WHERE city = :city AND active = :active';
$stmt = $conn->prepare($sql);
// 2. Exécution : paramètres liés séparément
$stmt->bindValue('city', $city, \PDO::PARAM_STR);
$stmt->bindValue('active', true, \PDO::PARAM_BOOL);
return $stmt->executeQuery()->fetchAllAssociative();
}
// FormType avec protection CSRF
class TransferType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('amount', MoneyType::class)
->add('toAccount', TextType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_protection' => true, // Activé par défaut
'csrf_field_name' => '_token',
'csrf_token_id' => 'transfer_form',
]);
}
}
{# Génération automatique du token #}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
{# Génération manuelle #}
<form method="post">
<input type="hidden" name="_token" value="{{ csrf_token('delete-item') }}">
<button type="submit">Supprimer</button>
</form>
// Stockage en clair - DANGEREUX !
$password = 'monMotDePasse123';
// Hash MD5/SHA1 - OBSOLÈTE !
$password = md5('monMotDePasse123');
// Sans salt - FAIBLE !
$password = sha1('monMotDePasse123');
// Service de hashage sécurisé
public function __construct(
private UserPasswordHasherInterface $hasher
) {}
public function hashPassword(User $user, string $plain): string
{
return $this->hasher->hashPassword($user, $plain);
}
# config/packages/security.yaml
security:
password_hashers:
App\Entity\User:
algorithm: auto # Utilise le meilleur algorithme
cost: 12 # Coût de calcul
memory_cost: 65536 # Mémoire pour Argon2
time_cost: 4 # Temps pour Argon2
// Entity avec contraintes
class User
{
#[Assert\NotBlank(message: 'Email obligatoire')]
#[Assert\Email(message: 'Email invalide')]
private string $email;
#[Assert\Length(
min: 8,
max: 255,
minMessage: 'Mot de passe trop court ({{ limit }} min)',
maxMessage: 'Mot de passe trop long ({{ limit }} max)'
)]
#[Assert\Regex(
pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/',
message: 'Doit contenir majuscule, minuscule et chiffre'
)]
private string $password;
#[Assert\Choice(choices: ['draft', 'published', 'archived'])]
private string $status = 'draft';
}
#[Route('/user/create', methods: ['POST'])]
public function create(Request $request, ValidatorInterface $validator): Response
{
$user = new User();
$user->setEmail($request->request->get('email'));
$user->setPassword($request->request->get('password'));
// Validation
$errors = $validator->validate($user);
if (count($errors) > 0) {
$errorMessages = [];
foreach ($errors as $error) {
$errorMessages[] = $error->getMessage();
}
return $this->json(['errors' => $errorMessages], 400);
}
// Traitement sécurisé...
}
// EventListener pour headers automatiques
public function onKernelResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
// Force HTTPS pour les futures requêtes
$response->headers->set('Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload');
// Politique de sécurité du contenu
$response->headers->set('Content-Security-Policy',
"default-src 'self'; script-src 'self'");
// Protection contre le clickjacking
$response->headers->set('X-Frame-Options', 'DENY');
// Empêcher le sniffing MIME
$response->headers->set('X-Content-Type-Options', 'nosniff');
// Référer policy
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
}
# config/packages/framework.yaml
framework:
session:
cookie_secure: auto
cookie_httponly: true
cookie_samesite: lax
cookie_lifetime: 3600
Contrôle d'accès défaillant - Accès non autorisé aux données/fonctions
⚠️ 94% des applications testéesDéfaillances cryptographiques - Données sensibles non protégées
⚠️ Exposition de données personnellesInjections SQL, NoSQL, LDAP, OS... - Exécution de code malveillant
💉 Classic mais toujours présentConception non sécurisée - Failles architecturales
🏗️ Nouveau dans le Top 10Mauvaise configuration - Permissions par défaut, erreurs exposées
⚙️ 90% des applicationsComposants vulnérables - Librairies non mises à jour
📦 Dépendances tiercesDéfaillances d'identification et d'authentification
🔐 Sessions, mots de passeDéfaillances d'intégrité des logiciels et données
🔒 Nouveau dans le Top 10Défaillances de logging et de monitoring
📊 Détection des attaquesSSRF - Falsification de requêtes côté serveur
🌐 Nouveau dans le Top 10# config/packages/security.yaml
security:
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
public function register(UserPasswordHasherInterface $hasher): Response
{
$user = new User();
$plainPassword = $request->request->get('password');
$hashedPassword = $hasher->hashPassword($user, $plainPassword);
$user->setPassword($hashedPassword);
}
class User
{
#[Assert\NotBlank(message: 'Email obligatoire')]
#[Assert\Email(message: 'Email invalide')]
private string $email;
#[Assert\Length(min: 8, max: 255)]
#[Assert\Regex(pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/')]
private string $password;
}
# config/packages/security.yaml
security:
firewalls:
main:
requires_channel: https
# Headers de sécurité HTTPS
$response->headers->set('Strict-Transport-Security',
'max-age=31536000; includeSubDomains');
{# Échappement automatique (par défaut) #}
{{ user.name }} {# Échappé automatiquement #}
{{ user.bio }} {# Échappé automatiquement #}
{# Contrôle manuel si nécessaire #}
{{ user.description|e }} {# Échappement explicite #}
{{ user.richContent|raw }} {# Contenu HTML non échappé #}
// EventListener pour headers de sécurité
public function onKernelResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
// Content Security Policy
$response->headers->set('Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'");
// Protection XSS
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Empêcher le sniffing MIME
$response->headers->set('X-Content-Type-Options', 'nosniff');
// Empêcher l'embedding
$response->headers->set('X-Frame-Options', 'DENY');
}
// EventListener pour surveiller l'authentification
public function onAuthenticationSuccess(AuthenticationEvent $event): void
{
$user = $event->getAuthenticationToken()->getUser();
$this->logger->info('Connexion réussie', [
'user' => $user->getUserIdentifier(),
'ip' => $event->getRequest()->getClientIp(),
'user_agent' => $event->getRequest()->headers->get('User-Agent')
]);
}
// Installation: composer require symfony/rate-limiter
public function login(Request $request, RateLimiterFactory $loginLimiter): Response
{
$limiter = $loginLimiter->create($request->getClientIp());
if (!$limiter->consume(1)->isAccepted()) {
throw new TooManyRequestsHttpException(
'Trop de tentatives de connexion. Réessayez plus tard.'
);
}
// Logique de connexion...
}