MFormations
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)

  1. Broken Access Control — 94% des applications testées
  2. Cryptographic Failures — données sensibles non chiffrées
  3. Injection — SQL, NoSQL, OS, LDAP
  4. Insecure Design — architecture non sécurisée
  5. Security Misconfiguration — defaults faibles
  6. Vulnerable Components — dépendances obsolètes
  7. Auth Failures — mauvaise gestion d'identité
  8. Data Integrity Failures — sérialisation non sécurisée
  9. Logging & Monitoring Failures — absence de détection
  10. 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.