Modern PHP Engineering
Chapitre 11
11 — Sécurité PHP
11 — Sécurité PHP
Course : Sécurité PHP
1. Introduction à la Sécurité Web
1.1 Pourquoi la sécurité ?
La sécurité n'est pas une fonctionnalité optionnelle. C'est une propriété émergente de l'architecture. Une faille peut entraîner : vol de données (RGPD : jusqu'à 4% du CA), perte de confiance, atteinte à la réputation, coûts de remédiation.
1.2 OWASP Top 10 (2021)
- Broken Access Control — 94% des applications testées
- Cryptographic Failures — données sensibles non chiffrées
- Injection — SQL, NoSQL, OS, LDAP
- Insecure Design — architecture non sécurisée
- Security Misconfiguration — defaults faibles
- Vulnerable Components — dépendances obsolètes
- Auth Failures — mauvaise gestion d'identité
- Data Integrity Failures — sérialisation non sécurisée
- Logging & Monitoring Failures — absence de détection
- SSRF — Server-Side Request Forgery
1.3 Principes fondamentaux
- Defense in Depth : multiples couches de sécurité
- Least Privilege : donner les droits minimaux nécessaires
- Secure by Default : la configuration par défaut doit être sécurisée
- Fail Secure : en cas d'erreur, fermer l'accès
- Never Trust User Input : toute entrée est hostile
- Principle of Least Astonishment : comportements prévisibles
2. SQL Injection
2.1 Comprendre l'injection SQL
// ❌ VULNÉRABLE — concaténation directe
$sql = "SELECT * FROM users WHERE email = '{$_POST['email']}'";
// POST: email = ' OR 1=1 --
// Résultat : SELECT * FROM users WHERE email = '' OR 1=1 --'
// Tous les utilisateurs sont retournés !
// ❌ VULNÉRABLE — échappement insuffisant
$sql = "SELECT * FROM users WHERE id = " . mysqli_real_escape_string($conn, $_GET['id']);
// L'attaquant peut contourner avec du jeu de caractères
2.2 PDO Prepared Statements
// ✅ SÉCURISÉ — requêtes préparées PDO
$pdo = new PDO(
'mysql:host=localhost;dbname=blog;charset=utf8mb4',
$user,
$password,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND active = :active');
$stmt->execute([
':email' => $_POST['email'],
':active' => true,
]);
$user = $stmt->fetch();
// ✅ Avec ? placeholders
$stmt = $pdo->prepare('INSERT INTO posts (title, content) VALUES (?, ?)');
$stmt->execute([$_POST['title'], $_POST['content']]);
2.3 Eloquent ORM
// ✅ SÉCURISÉ — Eloquent protège automatiquement
$user = User::where('email', request('email'))->first();
$posts = Post::create(request()->only(['title', 'content']));
// ⚠️ Attention : raw queries
User::whereRaw('email = ?', [request('email')])->get(); // OK
User::whereRaw("email = '" . request('email') . "'")->get(); // ❌ VULNÉRABLE
DB::select("SELECT * FROM users WHERE email = ?", [request('email')]); // OK
// ✅ Utiliser les scopes Eloquent
public function scopeActive(Builder $query): Builder
{
return $query->where('active', true);
}
public function scopeByEmail(Builder $query, string $email): Builder
{
return $query->where('email', $email);
}
User::active()->byEmail(request('email'))->first();
3. XSS (Cross-Site Scripting)
3.1 Types de XSS
- Stored XSS : le script malveillant est stocké en base de données
- Reflected XSS : le script est dans l'URL et reflété dans la réponse
- DOM-based XSS : le script manipule le DOM côté client
3.2 Blade Escaping
{{-- ✅ Échappement automatique (htmlspecialchars) --}}
<p>{{ $user->name }}</p>
{{-- ❌ Échappement désactivé (XSS possible) --}}
<p>{!! $user->bio !!}</p>
{{-- ✅ Échappement conditionnel --}}
<p>{{ Illuminate\Support\Stringable::of($user->bio)->sanitizeHtml() }}</p>
3.3 Content Security Policy (CSP)
// Middleware CSP
class CspMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
"script-src 'self' https://cdn.example.com 'nonce-" . $this->nonce() . "'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'",
]));
return $response;
}
private function nonce(): string
{
return base64_encode(random_bytes(16));
}
}
3.4 Purifier HTML
composer require mews/purifier
// Nettoyage HTML pour zones de confiance
$clean = Purifier::clean($request->input('content'), [
'HTML.Allowed' => 'p,br,b,i,strong,em,a[href],ul,ol,li,img[src|alt]',
]);
4. CSRF (Cross-Site Request Forgery)
4.1 Mécanisme
CSRF force un utilisateur authentifié à exécuter des actions non désirées.
<!-- ❌ L'attaquant crée une fausse requête -->
<img src="https://monsite.com/admin/delete-user/42" />
4.2 Protection Laravel
{{-- ✅ Token CSRF obligatoire --}}
<form method="POST" action="/admin/posts">
@csrf
<input name="title" />
<button type="submit">Créer</button>
</form>
// ✅ Pour les requêtes AJAX
// meta tag dans le layout
<meta name="csrf-token" content="{{ csrf_token() }}">
// Axios (auto-configuré par Laravel)
axios.post('/api/posts', data);
// Fetch manuel
fetch('/api/posts', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify(data),
});
4.3 SameSite Cookies
// config/session.php
'same_site' => 'lax', // lax, strict, none
// PHP setcookie
setcookie('session_id', $token, [
'expires' => time() + 86400,
'path' => '/',
'domain' => '.example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax', // ou 'Strict'
]);
5. Authentification
5.1 Hashing des mots de passe
// ✅ bcrypt (recommandé par défaut)
$hash = bcrypt('user-password'); // coût 10 par défaut
$hash = Hash::make('user-password');
// ✅ argon2id (plus résistant au GPU)
$hash = Hash::make('user-password', ['memory' => 1024, 'time' => 2, 'threads' => 2]);
// config/hashing.php
'driver' => 'argon2id',
// Vérification
if (Hash::check('user-password', $hash)) {
// OK
}
// Vérifier si le hash doit être recalculé
if (Hash::needsRehash($hash)) {
$hash = Hash::make('user-password');
$user->update(['password' => $hash]);
}
5.2 Two-Factor Authentication (2FA)
composer require pragmarx/google2fa-laravel
// Génération du secret
$google2fa = app('pragmarx.google2fa');
$secret = $google2fa->generateSecretKey();
// QR code pour l'utilisateur
$qrCodeUrl = $google2fa->getQRCodeUrl(
'MonSite',
$user->email,
$secret
);
// Vérification du code 2FA
$valid = $google2fa->verifyKey($user->google2fa_secret, $request->code);
5.3 Session Fixation
// ✅ Régénérer l'ID de session après login
public function authenticated(Request $request, $user): void
{
$request->session()->regenerate();
}
// ✅ Rotation du token CSRF
$request->session()->regenerateToken();
// ✅ Timeout de session
// config/session.php
'lifetime' => 120, // minutes
'expire_on_close' => true,
// ✅ Verrouillage après tentatives échouées
if (RateLimiter::tooManyAttempts('login:' . $request->ip(), 5)) {
$seconds = RateLimiter::availableIn('login:' . $request->ip());
throw new ThrottleRequestsException("Trop de tentatives. Réessayez dans $seconds secondes.");
}
6. Chiffrement
6.1 Laravel Encryption
// Configuration
// config/app.php → 'cipher' => 'AES-256-CBC'
// Chiffrement
$encrypted = Crypt::encryptString('Données sensibles');
// ou
$encrypted = encrypt('Données sensibles');
// Déchiffrement
$decrypted = Crypt::decryptString($encrypted);
$decrypted = decrypt($encrypted);
// Chiffrement avec clé dédiée
Crypt::encryptString('data', ['key' => env('SPECIAL_KEY')]);
6.2 Libsodium (NaCl)
PHP 8.3 intègre nativement Sodium :
// Chiffrement authentifié (AEAD)
$key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$ad = 'authenticated_data';
$ciphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
$message,
$ad,
$nonce,
$key
);
$plaintext = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
$ciphertext,
$ad,
$nonce,
$key
);
// Hashing pour stockage sécurisé
$hash = sodium_crypto_generichash($data); // BLAKE2b
$hash64 = sodium_crypto_pwhash_str(
$password,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
7. Security Headers
7.1 Middleware global
// App/Http/Middleware/SecurityHeaders.php
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '0'); // désuet mais safe
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
return $response;
}
}
7.2 HSTS (HTTP Strict Transport Security)
# Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Apache
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
7.3 CSP avancé
// Reporting uniquement (ne bloque pas)
$response->headers->set(
'Content-Security-Policy-Report-Only',
"default-src 'self'; report-uri /csp-violations"
);
// Rapport CSP
Route::post('/csp-violations', function (Request $request) {
Log::warning('CSP Violation', $request->all());
return response()->noContent();
});
8. PHP Secure Configuration
8.1 php.ini sécurisé
; Désactiver les fonctions dangereuses
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,show_source,phpinfo
; Limiter les ressources
max_execution_time = 30
max_input_time = 30
memory_limit = 128M
post_max_size = 8M
upload_max_filesize = 4M
; Gestion des sessions sécurisée
session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
session.gc_maxlifetime = 7200
session.sid_length = 48
session.sid_bits_per_character = 6
; Désactiver les informations
expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
; Fichiers
allow_url_fopen = Off
allow_url_include = Off
file_uploads = On
upload_tmp_dir = /tmp/secure_uploads
8.2 Validation des entrées
// Validation stricte
$validator = validator(request()->all(), [
'email' => 'required|email:rfc,dns|max:255',
'age' => 'required|integer|min:18|max:120',
'url' => 'required|url|active_url',
'ip' => 'required|ip',
'uuid' => 'required|uuid',
'json' => 'required|json',
'color' => 'required|regex:/^#[a-f0-9]{6}$/i',
]);
// Filtrage
$clean = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
$int = filter_var($_POST['count'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 100]]);
// Échappement contexte
$safe_for_html = htmlspecialchars($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safe_for_url = rawurlencode($input);
$safe_for_shell = escapeshellarg($input);
$safe_for_js = json_encode($input);
9. Dependencies et Monitoring
9.1 Audit des dépendances
# Vérifier les vulnérabilités connues
composer audit
# Mise à jour sécurisée
composer update --dry-run
# Analyse avec Symfony Security Checker
composer require --dev symfony/security-checker
php bin/console security:check
9.2 Logging et détection
// Logging des événements de sécurité
Log::channel('security')->warning('Tentative de connexion échouée', [
'email' => $request->email,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'attempts' => RateLimiter::attempts('login:' . $request->ip()),
]);
// Détection d'intrusion
if (str_contains($request->input('content'), '<?php')) {
Log::channel('security')->critical('Tentative d\'injection PHP', [
'user_id' => auth()->id(),
'ip' => $request->ip(),
]);
abort(403, 'Contenu non autorisé');
}
10. Résumé
La sécurité en PHP est un processus continu :
- SQL Injection : PDO prepared statements ou Eloquent
- XSS : Blade escaping + CSP + Purifier
- CSRF : tokens + SameSite cookies
- Auth : bcrypt/argon2 + 2FA + session regeneration
- Encryption : AES-256-CBC (Laravel) + Sodium
- Headers : HSTS, CSP, X-Frame-Options, Permissions-Policy
- Config : disable_functions, session sécurisée, validation stricte
- Monitoring : audit Composer, logging, détection d'intrusion
Règle d'or : ne faites jamais confiance à l'entrée utilisateur.