Déployer WordPress en production avec GitHub Actions et SSH
Pendant deux ans, j'ai déployé à la main. scp, rsync dans le terminal, parfois un…
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.
| 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% |
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.
id = $GET['id']; // Provient de l'URL
// Pas de vérification si l'utilisateur connecté peut modifier ce profil
$stmt = $pdo->prepare("SELECT FROM users WHERE id = ?");
$stmt->execute([$userid]);
$user = $stmt->fetch();
if ($SERVER['REQUESTMETHOD'] === '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'], $userid]);
}
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 !
Système de contrôle d'accès robuste
/
class AccessControl {
private PDO $pdo;
public function construct(PDO $pdo) {
$this->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['userid'])) {
return false;
}
$currentUserId = (int)$SESSION['userid'];
// 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([
'userid' => $SESSION['userid'] ?? null,
'targetid' => $targetUserId,
'ip' => $SERVER['REMOTEADDR'],
'timestamp' => date('Y-m-d H:i:s')
]);
httpresponsecode(403);
throw new AccessDeniedException(
"Vous n'êtes pas autorisé à modifier ce profil"
);
}
}
private function logUnauthorizedAccess(array $details): void {
errorlog(sprintf(
"SECURITY: Unauthorized access attempt - User %s tried to access user %s from IP %s",
$details['userid'] ?? 'anonymous',
$details['targetid'],
$details['ip']
));
}
}
// Utilisation sécurisée
sessionstart();
$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['REQUESTMETHOD'] === 'POST') {
// Sanitisation des entrées
$email = filterinput(INPUTPOST, 'email', FILTERVALIDATEEMAIL);
$phone = filterinput(INPUTPOST, 'phone', FILTERSANITIZESTRING);
if (!$email) {
throw new ValidationException("Email invalide");
}
$stmt = $pdo->prepare(
"UPDATE users SET email = ?, phone = ?, updatedat = 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;
}
VULNERABLE($username, $password) {
global $pdo;
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$username, $password]); // Mot de passe en clair !
}
// ❌ VULNÉRABLE - MD5 (cassable en secondes)
function registerUserBAD($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 = passwordhash($password, PASSWORDARGON2ID, [
'memorycost' => 65536, // 64 MB
'timecost' => 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 passwordverify($password, $hash);
}
/
Vérifie si le hash doit être régénéré (algorithme obsolète)
/
public function needsRehash(string $hash): bool {
return passwordneedsrehash($hash, PASSWORDARGON2ID, [
'memorycost' => 65536,
'timecost' => 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['userid'] = $user['id'];
}
Gestionnaire de chiffrement AES-256-GCM
/
class EncryptionManager {
private string $key;
public function construct() {
// Clé de chiffrement de 32 octets (256 bits)
// À stocker dans variable d'environnement, JAMAIS dans le code
$this->key = $ENV['ENCRYPTIONKEY'] ?? throw new RuntimeException(
"ENCRYPTIONKEY 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 = opensslcipherivlength('aes-256-gcm');
$iv = opensslrandompseudobytes($ivLength);
// Chiffrer avec authentification (GCM fournit AEAD)
$tag = '';
$ciphertext = opensslencrypt(
$plaintext,
'aes-256-gcm',
$this->key,
OPENSSLRAWDATA,
$iv,
$tag
);
if ($ciphertext === false) {
throw new CryptographyException("Échec du chiffrement");
}
// Combiner IV + tag + ciphertext et encoder en base64
return base64encode($iv . $tag . $ciphertext);
}
/
Déchiffre des données
/
public function decrypt(string $encrypted): string {
$data = base64decode($encrypted, true);
if ($data === false) {
throw new CryptographyException("Données chiffrées invalides");
}
$ivLength = opensslcipherivlength('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 = openssldecrypt(
$ciphertext,
'aes-256-gcm',
$this->key,
OPENSSLRAWDATA,
$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 (userid, cardencrypted) VALUES (?, ?)");
$stmt->execute([$userId, $encryptedCard]);
// Récupération
$stmt = $pdo->prepare("SELECT cardencrypted FROM payments WHERE id = ?");
$stmt->execute([$paymentId]);
$encrypted = $stmt->fetchColumn();
$cardNumber = $encryption->decrypt($encrypted);
VULNERABLE($searchTerm) {
global $pdo;
// Un attaquant peut injecter : ' OR '1'='1
$sql = "SELECT FROM users WHERE username LIKE '%{$searchTerm}%'";
return $pdo->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 searchUsersSECURE($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::FETCHASSOC);
}
VULNERABLE($host) {
// Un attaquant peut injecter : google.com; rm -rf /
$output = shellexec("ping -c 4 {$host}");
return $output;
}
// ✅ SÉCURISÉ - Validation et échappement
function pingHostSECURE($host) {
// 1. Validation stricte du format
if (!filtervar($host, FILTERVALIDATEDOMAIN, FILTERFLAGHOSTNAME)) {
if (!filtervar($host, FILTERVALIDATEIP)) {
throw new InvalidArgumentException("Hôte invalide");
}
}
// 2. Échappement avec escapeshellarg()
$safeHost = escapeshellarg($host);
// 3. Utiliser procopen pour plus de contrôle
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$process = procopen(
"ping -c 4 {$safeHost}",
$descriptors,
$pipes
);
if (!isresource($process)) {
throw new RuntimeException("Échec de l'exécution de ping");
}
$output = streamgetcontents($pipes[1]);
$errors = streamgetcontents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$returnCode = procclose($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 (!filtervar($host, FILTERVALIDATEDOMAIN, FILTERFLAGHOSTNAME) &&
!filtervar($host, FILTERVALIDATEIP)) {
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();
}
Gestionnaire d'authentification sécurisé
/
class AuthenticationManager {
private PDO $pdo;
private PasswordManager $passwordManager;
private int $maxLoginAttempts = 5;
private int $lockoutDuration = 900; // 15 minutes en secondes
public function _construct(PDO $pdo, PasswordManager $passwordManager) {
$this->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, isactive, requirepasswordchange
FROM users WHERE username = ?"
);
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCHASSOC);
// 3. Vérifier que l'utilisateur existe et est actif
if (!$user || !$user['isactive']) {
$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(attemptedat) as lastattempt
FROM loginattempts
WHERE ip = ? AND attemptedat > DATESUB(NOW(), INTERVAL ? SECOND)"
);
$stmt->execute([$ip, $this->lockoutDuration]);
$result = $stmt->fetch(PDO::FETCHASSOC);
return $result['attempts'] >= $this->maxLoginAttempts;
}
/
Calcule le temps restant de blocage
/
private function getRemainingLockoutTime(string $ip): int {
$stmt = $this->pdo->prepare(
"SELECT MAX(attemptedat) as lastattempt
FROM loginattempts WHERE ip = ?"
);
$stmt->execute([$ip]);
$result = $stmt->fetch(PDO::FETCHASSOC);
$lastAttempt = strtotime($result['lastattempt']);
$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 loginattempts (ip, username, attemptedat) VALUES (?, ?, NOW())"
);
$stmt->execute([$ip, $username]);
}
/
Réinitialise les tentatives échouées
/
private function clearFailedAttempts(string $ip): void {
$stmt = $this->pdo->prepare("DELETE FROM loginattempts 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
sessionregenerateid(true);
$SESSION['userid'] = $user['id'];
$SESSION['username'] = $user['username'];
$SESSION['logintime'] = time();
$SESSION['ip'] = $SERVER['REMOTEADDR'];
$SESSION['useragent'] = $SERVER['HTTPUSERAGENT'];
// Marquer si l'utilisateur doit changer son mot de passe
if ($user['requirepasswordchange']) {
$SESSION['mustchangepassword'] = true;
}
}
/
Logger la connexion réussie
/
private function logSuccessfulLogin(int $userId, string $ip): void {
$stmt = $this->pdo->prepare(
"INSERT INTO loginhistory (userid, ip, useragent, loggedinat)
VALUES (?, ?, ?, NOW())"
);
$stmt->execute([$userId, $ip, $SERVER['HTTPUSERAGENT']]);
}
/
Vérifie la validité de la session
/
public function validateSession(): bool {
if (!isset($SESSION['userid'])) {
return false;
}
// Vérifier que l'IP n'a pas changé (prévient le vol de session)
if ($SESSION['ip'] !== $SERVER['REMOTEADDR']) {
$this->logout();
throw new SecurityException("Session invalide - IP changée");
}
// Vérifier le timeout de session (30 minutes d'inactivité)
if (isset($SESSION['lastactivity']) &&
(time() - $SESSION['lastactivity'] > 1800)) {
$this->logout();
throw new SessionExpiredException("Session expirée");
}
$SESSION['lastactivity'] = time();
// Régénérer l'ID toutes les 15 minutes
if (!isset($SESSION['lastregeneration']) ||
(time() - $SESSION['lastregeneration'] > 900)) {
sessionregenerateid(true);
$SESSION['lastregeneration'] = time();
}
return true;
}
/
Déconnexion
/
public function logout(): void {
$SESSION = [];
if (isset($COOKIE[sessionname()])) {
setcookie(sessionname(), '', time() - 3600, '/');
}
sessiondestroy();
}
}
// Configuration de session sécurisée (à mettre dans votre bootstrap)
iniset('session.cookiehttponly', '1'); // Empêche l'accès JavaScript aux cookies
iniset('session.cookiesecure', '1'); // HTTPS uniquement
iniset('session.cookiesamesite', 'Strict'); // Protection CSRF
iniset('session.usestrictmode', '1'); // Rejette les IDs de session non initialisés
iniset('session.useonlycookies', '1'); // Pas d'ID dans l'URL
#!/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/ZAP2.14.0Linux.tar.gz
# tar -xvf ZAP2.14.0Linux.tar.gz
ZAPPATH="/opt/zaproxy/zap.sh"
TARGETURL="https://example.com"
REPORTDIR="./security-reports"
# Créer le répertoire de rapports
mkdir -p "$REPORTDIR"
echo "Démarrage du scan OWASP ZAP sur $TARGETURL..."
# Scan actif (détection de vulnérabilités)
$ZAPPATH -cmd
-quickurl "$TARGETURL"
-quickprogress
-quickout "$REPORTDIR/zap-report-$(date +%Y%m%d-%H%M%S).html"
echo "Scan terminé. Rapport généré dans $REPORTDIR"
# Analyser les résultats
echo -e "n=== Résumé des vulnérabilités ==="
grep -i "high risk" "$REPORTDIR"/.html | wc -l | xargs echo "Risque élevé:"
grep -i "medium risk" "$REPORTDIR"/.html | wc -l | xargs echo "Risque moyen:"
grep -i "low risk" "$REPORTDIR"/.html | wc -l | xargs echo "Risque faible:"
# 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
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.
Système de recherche sécurisé
/
class SecureProductSearch {
private PDO $pdo;
private EncryptionManager $encryption;
private RateLimiter $rateLimiter;
public function search(string $query, int $userId): array {
// 1. Rate limiting : 20 recherches par minute
if (!$this->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 isactive = 1
LIMIT 50"
);
$searchTerm = '%' . $query . '%';
$stmt->execute([$searchTerm, $searchTerm]);
return $stmt->fetchAll(PDO::FETCHASSOC);
}
}
/
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 paymentmethods
(userid, cardnumberencrypted, cvvencrypted, lastfour, expirydate, createdat)
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 = pregreplace('/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;
}
}
# Installer les outils de sécurité PHP
composer require --dev roave/security-advisories:dev-latest
composer require paragonie/randomcompat # 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
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.
La sécurité est un processus continu, pas une destination. Testez régulièrement, formez vos équipes, et restez à jour sur les nouvelles menaces.
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.