Introduction : Comprendre les Menaces Web Modernes
L’OWASP (Open Web Application Security Project) publie tous les 3-4 ans son célèbre Top 10 des vulnérabilités web les plus critiques. En 2025, ces vulnérabilités restent responsables de plus de 80% des incidents de sécurité. Pour tout développeur web, maîtriser l’OWASP Top 10 n’est plus optionnel, c’est une compétence fondamentale.
Ce guide détaille chaque vulnérabilité du Top 10 avec des exemples concrets, du code vulnérable et sécurisé, et des outils de détection. Vous apprendrez non seulement à identifier ces failles, mais aussi à les prévenir systématiquement dans vos applications.
OWASP Top 10 2021-2025 : Vue d’Ensemble
Classification des Vulnérabilités
| Rang | Vulnérabilité | Impact | Prévalence |
|---|---|---|---|
| A01 | Broken Access Control | Critique | 94% |
| A02 | Cryptographic Failures | Élevé | 60% |
| A03 | Injection | Critique | 94% |
| A04 | Insecure Design | Élevé | 40% |
| A05 | Security Misconfiguration | Élevé | 90% |
| A06 | Vulnerable Components | Élevé | 80% |
| A07 | Authentication Failures | Critique | 75% |
| A08 | Software & Data Integrity | Élevé | 55% |
| A09 | Security Logging Failures | Moyen | 70% |
| A10 | Server-Side Request Forgery | Élevé | 45% |
A01 : Broken Access Control (Contrôle d’Accès Défaillant)
Qu’est-ce que c’est ?
Le contrôle d’accès défaillant survient lorsqu’un utilisateur peut accéder à des ressources ou effectuer des actions pour lesquelles il n’a pas les permissions nécessaires.
Exemple Vulnérable : Exposition IDOR
prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
if ($*SERVER['REQUEST_METHOD'] === 'POST') {
// N'importe qui peut modifier n'importe quel profil !
$stmt = $pdo->prepare("UPDATE users SET email = ?, phone = ? WHERE id = ?");
$stmt->execute([$*POST['email'], $*POST['phone'], $user_id]);
}
Exploitation :
# L'attaquant change simplement l'ID dans l'URL
# Son profil : https://example.com/profile.php?id=42
# Profil admin : https://example.com/profile.php?id=1
curl -X POST https://example.com/profile.php?id=1
-d "email=hacker@evil.com&phone=123456789"
# L'administrateur reçoit désormais les notifications à hacker@evil.com !
Solution Sécurisée : Validation d’Autorisation
pdo = $pdo;
}
/**
* Vérifie si l'utilisateur courant peut modifier un profil
*/
public function canEditProfile(int $targetUserId): bool {
// Vérifier l'authentification
if (!isset($*SESSION['user_id'])) {
return false;
}
$currentUserId = (int)$*SESSION['user_id'];
// Un utilisateur peut modifier son propre profil
if ($currentUserId === $targetUserId) {
return true;
}
// Vérifier si l'utilisateur a le rôle admin
$stmt = $this->pdo->prepare(
"SELECT role FROM users WHERE id = ?"
);
$stmt->execute([$currentUserId]);
$user = $stmt->fetch();
return $user && $user['role'] === 'admin';
}
/**
* Vérifie les permissions et lève une exception si refusé
*/
public function enforceEditProfile(int $targetUserId): void {
if (!$this->canEditProfile($targetUserId)) {
// Logger la tentative d'accès non autorisé
$this->logUnauthorizedAccess([
'user_id' => $*SESSION['user_id'] ?? null,
'target_id' => $targetUserId,
'ip' => $*SERVER['REMOTE_ADDR'],
'timestamp' => date('Y-m-d H:i:s')
]);
http_response_code(403);
throw new AccessDeniedException(
"Vous n'êtes pas autorisé à modifier ce profil"
);
}
}
private function logUnauthorizedAccess(array $details): void {
error_log(sprintf(
"SECURITY: Unauthorized access attempt - User %s tried to access user %s from IP %s",
$details['user_id'] ?? 'anonymous',
$details['target_id'],
$details['ip']
));
}
}
// Utilisation sécurisée
session_start();
$accessControl = new AccessControl($pdo);
$targetUserId = (int)($*GET['id'] ?? 0);
try {
// Vérification obligatoire avant toute action
$accessControl->enforceEditProfile($targetUserId);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$targetUserId]);
$user = $stmt->fetch();
if ($*SERVER['REQUEST_METHOD'] === 'POST') {
// Sanitisation des entrées
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$phone = filter_input(INPUT_POST, 'phone', FILTER_SANITIZE_STRING);
if (!$email) {
throw new ValidationException("Email invalide");
}
$stmt = $pdo->prepare(
"UPDATE users SET email = ?, phone = ?, updated_at = NOW() WHERE id = ?"
);
$stmt->execute([$email, $phone, $targetUserId]);
header('Location: /profile.php?id=' . $targetUserId . '&success=1');
exit;
}
} catch (AccessDeniedException $e) {
// Afficher une page d'erreur 403
include 'errors/403.php';
exit;
}
A02 : Cryptographic Failures (Défaillances Cryptographiques)
Stockage de Mots de Passe : Bon vs Mauvais
prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$username, $password]); // Mot de passe en clair !
}
// ❌ VULNÉRABLE - MD5 (cassable en secondes)
function registerUser_BAD($username, $password) {
global $pdo;
$hashedPassword = md5($password); // MD5 est obsolète et non sécurisé
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$username, $hashedPassword]);
}
// ✅ SÉCURISÉ - Argon2id (recommandation 2025)
class PasswordManager {
/**
* Hash un mot de passe avec Argon2id
*/
public function hashPassword(string $password): string {
// Argon2id : meilleur algorithme en 2025
// Résistant aux attaques GPU et side-channel
$hash = password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4, // 4 itérations
'threads' => 3 // 3 threads parallèles
]);
if ($hash === false) {
throw new CryptographyException("Échec du hashage du mot de passe");
}
return $hash;
}
/**
* Vérifie un mot de passe
*/
public function verifyPassword(string $password, string $hash): bool {
return password_verify($password, $hash);
}
/**
* Vérifie si le hash doit être régénéré (algorithme obsolète)
*/
public function needsRehash(string $hash): bool {
return password_needs_rehash($hash, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
]);
}
}
// Utilisation
$passwordManager = new PasswordManager();
// Enregistrement
$hashedPassword = $passwordManager->hashPassword($*POST['password']);
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$*POST['username'], $hashedPassword]);
// Connexion avec mise à jour automatique du hash si obsolète
$stmt = $pdo->prepare("SELECT id, password FROM users WHERE username = ?");
$stmt->execute([$*POST['username']]);
$user = $stmt->fetch();
if ($user && $passwordManager->verifyPassword($*POST['password'], $user['password'])) {
// Connexion réussie
// Mettre à jour le hash s'il est obsolète
if ($passwordManager->needsRehash($user['password'])) {
$newHash = $passwordManager->hashPassword($*POST['password']);
$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$newHash, $user['id']]);
}
$*SESSION['user_id'] = $user['id'];
}
Chiffrement de Données Sensibles
key = $*ENV['ENCRYPTION_KEY'] ?? throw new RuntimeException(
"ENCRYPTION_KEY non définie"
);
if (strlen($this->key) !== 32) {
throw new InvalidArgumentException(
"La clé de chiffrement doit faire 32 octets"
);
}
}
/**
* Chiffre des données avec AES-256-GCM
*
* @return string Données chiffrées encodées en base64
*/
public function encrypt(string $plaintext): string {
// Générer un IV (Initialization Vector) aléatoire
$ivLength = openssl_cipher_iv_length('aes-256-gcm');
$iv = openssl_random_pseudo_bytes($ivLength);
// Chiffrer avec authentification (GCM fournit AEAD)
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
'aes-256-gcm',
$this->key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($ciphertext === false) {
throw new CryptographyException("Échec du chiffrement");
}
// Combiner IV + tag + ciphertext et encoder en base64
return base64_encode($iv . $tag . $ciphertext);
}
/**
* Déchiffre des données
*/
public function decrypt(string $encrypted): string {
$data = base64_decode($encrypted, true);
if ($data === false) {
throw new CryptographyException("Données chiffrées invalides");
}
$ivLength = openssl_cipher_iv_length('aes-256-gcm');
$tagLength = 16; // GCM tag fait toujours 16 octets
// Extraire IV, tag et ciphertext
$iv = substr($data, 0, $ivLength);
$tag = substr($data, $ivLength, $tagLength);
$ciphertext = substr($data, $ivLength + $tagLength);
// Déchiffrer avec vérification d'authenticité
$plaintext = openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$this->key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($plaintext === false) {
throw new CryptographyException(
"Échec du déchiffrement - Données corrompues ou clé invalide"
);
}
return $plaintext;
}
}
// Exemple : Chiffrer des données de carte bancaire
$encryption = new EncryptionManager();
// Stockage
$cardNumber = '4111111111111111';
$encryptedCard = $encryption->encrypt($cardNumber);
$stmt = $pdo->prepare("INSERT INTO payments (user_id, card_encrypted) VALUES (?, ?)");
$stmt->execute([$userId, $encryptedCard]);
// Récupération
$stmt = $pdo->prepare("SELECT card_encrypted FROM payments WHERE id = ?");
$stmt->execute([$paymentId]);
$encrypted = $stmt->fetchColumn();
$cardNumber = $encryption->decrypt($encrypted);
A03 : Injection (SQL, NoSQL, Command)
SQL Injection : La Vulnérabilité Classique
query($sql)->fetchAll();
}
// Exploitation
// URL : /search.php?q=' UNION SELECT id,password,email,NULL FROM admins--
// Résultat : L'attaquant récupère tous les mots de passe admin !
// ✅ SÉCURISÉ - Requêtes préparées
function searchUsers_SECURE($searchTerm) {
global $pdo;
// Les requêtes préparées séparent SQL et données
$stmt = $pdo->prepare(
"SELECT id, username, email FROM users WHERE username LIKE ?"
);
// Le moteur de BDD échappe automatiquement les caractères dangereux
$stmt->execute(['%' . $searchTerm . '%']);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Command Injection : Exécution de Commandes
['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$process = proc_open(
"ping -c 4 {$safeHost}",
$descriptors,
$pipes
);
if (!is_resource($process)) {
throw new RuntimeException("Échec de l'exécution de ping");
}
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$returnCode = proc_close($process);
if ($returnCode !== 0) {
throw new RuntimeException("Ping échoué : {$errors}");
}
return $output;
}
// Meilleure approche : Utiliser une bibliothèque dédiée
// composer require symfony/process
use SymfonyComponentProcessProcess;
function pingHostModern($host) {
// Validation
if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) &&
!filter_var($host, FILTER_VALIDATE_IP)) {
throw new InvalidArgumentException("Hôte invalide");
}
// Process gère automatiquement l'échappement
$process = new Process(['ping', '-c', '4', $host]);
$process->setTimeout(10);
$process->run();
if (!$process->isSuccessful()) {
throw new RuntimeException($process->getErrorOutput());
}
return $process->getOutput();
}
A07 : Identification and Authentication Failures
Système d’Authentification Robuste
pdo = $pdo;
$this->passwordManager = $passwordManager;
}
/**
* Authentifie un utilisateur
*/
public function login(string $username, string $password, string $ip): bool {
// 1. Vérifier si l'IP est bloquée
if ($this->isIpLockedOut($ip)) {
$remainingTime = $this->getRemainingLockoutTime($ip);
throw new AuthenticationException(
"Trop de tentatives échouées. Réessayez dans {$remainingTime} minutes."
);
}
// 2. Récupérer l'utilisateur
$stmt = $this->pdo->prepare(
"SELECT id, username, password, is_active, require_password_change
FROM users WHERE username = ?"
);
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 3. Vérifier que l'utilisateur existe et est actif
if (!$user || !$user['is_active']) {
$this->recordFailedAttempt($ip, $username);
// Message générique pour ne pas révéler si le username existe
throw new AuthenticationException("Identifiants invalides");
}
// 4. Vérifier le mot de passe
if (!$this->passwordManager->verifyPassword($password, $user['password'])) {
$this->recordFailedAttempt($ip, $username);
throw new AuthenticationException("Identifiants invalides");
}
// 5. Réinitialiser les tentatives échouées
$this->clearFailedAttempts($ip);
// 6. Créer la session
$this->createSession($user);
// 7. Logger la connexion réussie
$this->logSuccessfulLogin($user['id'], $ip);
return true;
}
/**
* Vérifie si une IP est bloquée
*/
private function isIpLockedOut(string $ip): bool {
$stmt = $this->pdo->prepare(
"SELECT COUNT(*) as attempts, MAX(attempted_at) as last_attempt
FROM login_attempts
WHERE ip = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL ? SECOND)"
);
$stmt->execute([$ip, $this->lockoutDuration]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['attempts'] >= $this->maxLoginAttempts;
}
/**
* Calcule le temps restant de blocage
*/
private function getRemainingLockoutTime(string $ip): int {
$stmt = $this->pdo->prepare(
"SELECT MAX(attempted_at) as last_attempt
FROM login_attempts WHERE ip = ?"
);
$stmt->execute([$ip]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$lastAttempt = strtotime($result['last_attempt']);
$unlockTime = $lastAttempt + $this->lockoutDuration;
return ceil(($unlockTime - time()) / 60);
}
/**
* Enregistre une tentative échouée
*/
private function recordFailedAttempt(string $ip, string $username): void {
$stmt = $this->pdo->prepare(
"INSERT INTO login_attempts (ip, username, attempted_at) VALUES (?, ?, NOW())"
);
$stmt->execute([$ip, $username]);
}
/**
* Réinitialise les tentatives échouées
*/
private function clearFailedAttempts(string $ip): void {
$stmt = $this->pdo->prepare("DELETE FROM login_attempts WHERE ip = ?");
$stmt->execute([$ip]);
}
/**
* Crée une session sécurisée
*/
private function createSession(array $user): void {
// Régénérer l'ID de session pour prévenir la fixation de session
session_regenerate_id(true);
$*SESSION['user_id'] = $user['id'];
$*SESSION['username'] = $user['username'];
$*SESSION['login_time'] = time();
$*SESSION['ip'] = $*SERVER['REMOTE_ADDR'];
$*SESSION['user_agent'] = $*SERVER['HTTP_USER_AGENT'];
// Marquer si l'utilisateur doit changer son mot de passe
if ($user['require_password_change']) {
$*SESSION['must_change_password'] = true;
}
}
/**
* Logger la connexion réussie
*/
private function logSuccessfulLogin(int $userId, string $ip): void {
$stmt = $this->pdo->prepare(
"INSERT INTO login_history (user_id, ip, user_agent, logged_in_at)
VALUES (?, ?, ?, NOW())"
);
$stmt->execute([$userId, $ip, $*SERVER['HTTP_USER_AGENT']]);
}
/**
* Vérifie la validité de la session
*/
public function validateSession(): bool {
if (!isset($*SESSION['user_id'])) {
return false;
}
// Vérifier que l'IP n'a pas changé (prévient le vol de session)
if ($*SESSION['ip'] !== $*SERVER['REMOTE_ADDR']) {
$this->logout();
throw new SecurityException("Session invalide - IP changée");
}
// Vérifier le timeout de session (30 minutes d'inactivité)
if (isset($*SESSION['last_activity']) &&
(time() - $*SESSION['last_activity'] > 1800)) {
$this->logout();
throw new SessionExpiredException("Session expirée");
}
$*SESSION['last_activity'] = time();
// Régénérer l'ID toutes les 15 minutes
if (!isset($*SESSION['last_regeneration']) ||
(time() - $*SESSION['last_regeneration'] > 900)) {
session_regenerate_id(true);
$*SESSION['last_regeneration'] = time();
}
return true;
}
/**
* Déconnexion
*/
public function logout(): void {
$*SESSION = [];
if (isset($*COOKIE[session_name()])) {
setcookie(session_name(), '', time() - 3600, '/');
}
session_destroy();
}
}
// Configuration de session sécurisée (à mettre dans votre bootstrap)
ini_set('session.cookie_httponly', '1'); // Empêche l'accès JavaScript aux cookies
ini_set('session.cookie_secure', '1'); // HTTPS uniquement
ini_set('session.cookie_samesite', 'Strict'); // Protection CSRF
ini_set('session.use_strict_mode', '1'); // Rejette les IDs de session non initialisés
ini_set('session.use_only_cookies', '1'); // Pas d'ID dans l'URL
Outils de Détection : Scanner les Vulnérabilités OWASP
OWASP ZAP : Scan Automatisé
#!/bin/bash
# Script de scan de sécurité avec OWASP ZAP
# Installation de ZAP
# wget https://github.com/zaproxy/zaproxy/releases/download/v2.14.0/ZAP_2.14.0_Linux.tar.gz
# tar -xvf ZAP_2.14.0_Linux.tar.gz
ZAP_PATH="/opt/zaproxy/zap.sh"
TARGET_URL="https://example.com"
REPORT_DIR="./security-reports"
# Créer le répertoire de rapports
mkdir -p "$REPORT_DIR"
echo "Démarrage du scan OWASP ZAP sur $TARGET_URL..."
# Scan actif (détection de vulnérabilités)
$ZAP_PATH -cmd
-quickurl "$TARGET_URL"
-quickprogress
-quickout "$REPORT_DIR/zap-report-$(date +%Y%m%d-%H%M%S).html"
echo "Scan terminé. Rapport généré dans $REPORT_DIR"
# Analyser les résultats
echo -e "n=== Résumé des vulnérabilités ==="
grep -i "high risk" "$REPORT_DIR"/*.html | wc -l | xargs echo "Risque élevé:"
grep -i "medium risk" "$REPORT_DIR"/*.html | wc -l | xargs echo "Risque moyen:"
grep -i "low risk" "$REPORT_DIR"/*.html | wc -l | xargs echo "Risque faible:"
Checklist de Sécurité OWASP
# Checklist de Sécurité Web OWASP
## A01 : Contrôle d'Accès
- [ ] Vérifications de permissions sur toutes les actions sensibles
- [ ] Tokens CSRF sur tous les formulaires
- [ ] Validation côté serveur (jamais uniquement côté client)
- [ ] Principe du moindre privilège appliqué
- [ ] Logs des tentatives d'accès non autorisées
## A02 : Cryptographie
- [ ] HTTPS activé (TLS 1.3)
- [ ] Mots de passe hashés avec Argon2id ou bcrypt
- [ ] Données sensibles chiffrées au repos (AES-256-GCM)
- [ ] Clés de chiffrement stockées dans variables d'environnement
- [ ] Pas de données sensibles dans les URLs
## A03 : Injection
- [ ] Requêtes SQL préparées (prepared statements)
- [ ] Validation et sanitisation de toutes les entrées
- [ ] Pas d'exécution de commandes shell avec input utilisateur
- [ ] ORM utilisé correctement (pas de raw queries)
- [ ] Validation des uploads de fichiers
## A04 : Conception Sécurisée
- [ ] Modélisation des menaces effectuée
- [ ] Architecture de sécurité documentée
- [ ] Séparation des environnements (dev/staging/prod)
- [ ] Limites de taux (rate limiting) sur les API
- [ ] Tests de sécurité dans le CI/CD
## A05 : Configuration
- [ ] Messages d'erreur génériques en production
- [ ] Désactivation des fonctionnalités inutilisées
- [ ] Headers de sécurité configurés (CSP, HSTS, etc.)
- [ ] Versions de logiciels à jour
- [ ] Permissions de fichiers restrictives
## A06 : Composants Vulnérables
- [ ] Inventaire des dépendances maintenu
- [ ] Scan régulier avec npm audit / composer audit
- [ ] Mises à jour de sécurité appliquées rapidement
- [ ] Sources de téléchargement vérifiées
- [ ] Composants non utilisés supprimés
## A07 : Authentification
- [ ] Politique de mots de passe forts (12+ caractères)
- [ ] Authentification multi-facteurs (2FA/MFA)
- [ ] Protection contre le brute force (rate limiting)
- [ ] Sessions sécurisées (HttpOnly, Secure, SameSite)
- [ ] Timeouts de session configurés
## A08 : Intégrité des Données
- [ ] Vérification des signatures de packages
- [ ] Intégrité du code vérifiée (checksums)
- [ ] Pipeline CI/CD sécurisé
- [ ] Pas de désérialisation de données non fiables
- [ ] Validation des mises à jour auto
## A09 : Logging
- [ ] Événements de sécurité loggés
- [ ] Pas de données sensibles dans les logs
- [ ] Logs centralisés et protégés
- [ ] Alertes sur événements critiques
- [ ] Rétention des logs définie
## A10 : SSRF
- [ ] Validation des URLs fournies par l'utilisateur
- [ ] Liste blanche de domaines autorisés
- [ ] Pas d'accès aux IPs privées depuis les requêtes utilisateur
- [ ] Timeouts sur les requêtes HTTP sortantes
- [ ] Désactivation des redirections automatiques
Cas d’Étude : Sécurisation d’une Application E-commerce
Contexte
Une boutique en ligne française traitant 10 000 commandes/mois a subi une attaque par injection SQL qui a exposé 50 000 numéros de cartes bancaires.
Problèmes Identifiés
Remédiation Mise en Place
rateLimiter->attempt("search:{$userId}", 20, 60)) {
throw new RateLimitException("Trop de recherches. Réessayez dans 1 minute.");
}
// 2. Validation et sanitisation
$query = trim($query);
if (strlen($query) < 2 || strlen($query) > 100) {
throw new ValidationException("Recherche invalide");
}
// 3. Requête préparée (protection SQL injection)
$stmt = $this->pdo->prepare(
"SELECT id, name, price, description
FROM products
WHERE name LIKE ? OR description LIKE ?
AND is_active = 1
LIMIT 50"
);
$searchTerm = '%' . $query . '%';
$stmt->execute([$searchTerm, $searchTerm]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
/**
* Gestionnaire de paiement sécurisé
*/
class SecurePaymentManager {
private PDO $pdo;
private EncryptionManager $encryption;
/**
* Stocke les informations de paiement de manière sécurisée
*/
public function storePaymentMethod(int $userId, array $cardData): int {
// Valider le numéro de carte
if (!$this->isValidCardNumber($cardData['number'])) {
throw new ValidationException("Numéro de carte invalide");
}
// Chiffrer les données sensibles
$encryptedNumber = $this->encryption->encrypt($cardData['number']);
$encryptedCvv = $this->encryption->encrypt($cardData['cvv']);
// Stocker uniquement les 4 derniers chiffres en clair (pour affichage)
$lastFour = substr($cardData['number'], -4);
$stmt = $this->pdo->prepare(
"INSERT INTO payment_methods
(user_id, card_number_encrypted, cvv_encrypted, last_four, expiry_date, created_at)
VALUES (?, ?, ?, ?, ?, NOW())"
);
$stmt->execute([
$userId,
$encryptedNumber,
$encryptedCvv,
$lastFour,
$cardData['expiry']
]);
return (int)$this->pdo->lastInsertId();
}
/**
* Validation Luhn (algorithme de carte bancaire)
*/
private function isValidCardNumber(string $number): bool {
$number = preg_replace('/D/', '', $number);
if (strlen($number) < 13 || strlen($number) > 19) {
return false;
}
$sum = 0;
$numDigits = strlen($number);
$parity = $numDigits % 2;
for ($i = 0; $i < $numDigits; $i++) {
$digit = (int)$number[$i];
if ($i % 2 == $parity) {
$digit *= 2;
}
if ($digit > 9) {
$digit -= 9;
}
$sum += $digit;
}
return ($sum % 10) === 0;
}
}
Résultats
Ressources et Outils
Outils de Scan
Dépendances Sécurité PHP
# Installer les outils de sécurité PHP
composer require --dev roave/security-advisories:dev-latest
composer require paragonie/random_compat # Générateur aléatoire sécurisé
composer require symfony/security-csrf # Protection CSRF
composer require ramsey/uuid # Génération d'UUID sécurisés
Documentation
Conclusion
La sécurité web n’est pas une fonctionnalité à ajouter, c’est un état d’esprit à adopter dès la conception. L’OWASP Top 10 fournit une base solide pour identifier et corriger les vulnérabilités les plus critiques.
Points Clés à Retenir
La sécurité est un processus continu, pas une destination. Testez régulièrement, formez vos équipes, et restez à jour sur les nouvelles menaces.