REST API : Design patterns et meilleures pratiques
Introduction Les API REST (Representational State Transfer) sont devenues le standard de facto pour la…
Les headers HTTP de sécurité constituent la première ligne de défense de toute application web moderne. Souvent négligés, ces en-têtes peuvent prévenir des attaques courantes comme le Cross-Site Scripting (XSS), le clickjacking, et les fuites de données. En 2025, avec l’augmentation des attaques web sophistiquées, configurer correctement ces headers n’est plus optionnel.
Ce guide complet explore les headers de sécurité essentiels, CORS (Cross-Origin Resource Sharing) et CSP (Content Security Policy) avec des exemples pratiques et des configurations production-ready.
| Header | Protection | Priorité |
|---|---|---|
| Content-Security-Policy | XSS, injection | Critique |
| Strict-Transport-Security | Man-in-the-middle | Critique |
| X-Frame-Options | Clickjacking | Élevée |
| X-Content-Type-Options | MIME sniffing | Élevée |
| Referrer-Policy | Fuite d’informations | Moyenne |
| Permissions-Policy | Accès fonctionnalités | Moyenne |
# .htaccess - Configuration complète des headers de sécurité
headers.c>
# 1. HTTP Strict Transport Security (HSTS)
# Force HTTPS pour 2 ans, incluant sous-domaines
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# 2. X-Frame-Options
# Empêche le site d'être chargé dans une iframe (protection clickjacking)
Header always set X-Frame-Options "DENY"
# 3. X-Content-Type-Options
# Empêche le navigateur de deviner le type MIME
Header always set X-Content-Type-Options "nosniff"
# 4. X-XSS-Protection (déprécié mais utile pour anciens navigateurs)
# Mode block : bloque la page si XSS détecté
Header always set X-XSS-Protection "1; mode=block"
# 5. Referrer-Policy
# Ne transmet l'URL complète qu'aux mêmes origines
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# 6. Permissions-Policy (anciennement Feature-Policy)
# Désactive les fonctionnalités non utilisées
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"
# 7. X-Permitted-Cross-Domain-Policies
# Empêche Adobe Flash/PDF d'accéder aux données
Header always set X-Permitted-Cross-Domain-Policies "none"
# 8. Cache-Control pour pages sensibles
Header always set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Header always set Pragma "no-cache"
Header always set Expires "0"
# nginx.conf ou dans votre bloc server
server {
listen 443 ssl http2;
servername example.com;
# Configuration SSL/TLS
sslcertificate /path/to/cert.pem;
sslcertificatekey /path/to/key.pem;
sslprotocols TLSv1.2 TLSv1.3;
sslciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
sslpreferserverciphers on;
# Headers de sécurité
addheader Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
addheader X-Frame-Options "DENY" always;
addheader X-Content-Type-Options "nosniff" always;
addheader X-XSS-Protection "1; mode=block" always;
addheader Referrer-Policy "strict-origin-when-cross-origin" always;
addheader Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Content Security Policy (détaillé plus loin)
addheader Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; 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'; base-uri 'self'; form-action 'self'" always;
# Désactiver les méthodes HTTP non utilisées
if ($requestmethod !~ ^(GET|HEAD|POST|PUT|DELETE)$ ) {
return 405;
}
location / {
tryfiles $uri $uri/ /index.php?$querystring;
}
location ~ .php$ {
fastcgipass unix:/var/run/php/php8.2-fpm.sock;
fastcgiindex index.php;
include fastcgiparams;
fastcgiparam SCRIPTFILENAME $documentroot$fastcgiscriptname;
# Headers de sécurité pour pages dynamiques
fastcgihideheader X-Powered-By;
}
}
Gestionnaire de headers de sécurité
/
class SecurityHeadersManager {
private bool $isHttps;
private string $environment;
public function construct(string $environment = 'production') {
$this->environment = $environment;
$this->isHttps = (
(!empty($SERVER['HTTPS']) && $SERVER['HTTPS'] !== 'off') ||
$SERVER['SERVERPORT'] == 443 ||
(!empty($SERVER['HTTPXFORWARDEDPROTO']) && $SERVER['HTTPXFORWARDEDPROTO'] === 'https')
);
}
/
Applique tous les headers de sécurité
/
public function applySecurityHeaders(): void {
// Supprimer les headers révélant des informations
headerremove('X-Powered-By');
headerremove('Server');
// HSTS (uniquement en HTTPS)
if ($this->isHttps) {
header(
'Strict-Transport-Security: max-age=63072000; includeSubDomains; preload',
true
);
}
// Protection clickjacking
header('X-Frame-Options: DENY', true);
// Empêcher le MIME sniffing
header('X-Content-Type-Options: nosniff', true);
// XSS Protection (navigateurs anciens)
header('X-XSS-Protection: 1; mode=block', true);
// Referrer Policy
header('Referrer-Policy: strict-origin-when-cross-origin', true);
// Permissions Policy
header('Permissions-Policy: geolocation=(), microphone=(), camera=()', true);
// CSP (Content Security Policy)
$this->applyCSP();
// Cache control pour pages sensibles
if ($this->isSensitivePage()) {
$this->applySensitivePageHeaders();
}
}
/
Applique le Content Security Policy
/
private function applyCSP(): void {
$nonce = $this->generateCSPNonce();
// Stocker le nonce pour utilisation dans les templates
define('CSPNONCE', $nonce);
$cspDirectives = [
"default-src 'self'",
"script-src 'self' 'nonce-{$nonce}' https://cdn.jsdelivr.net",
"style-src 'self' 'nonce-{$nonce}' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests"
];
// En développement, ajouter des directives plus permissives
if ($this->environment === 'development') {
$cspDirectives[] = "script-src 'self' 'unsafe-eval' 'nonce-{$nonce}'";
}
header('Content-Security-Policy: ' . implode('; ', $cspDirectives), true);
// CSP en mode report uniquement (pour tester avant déploiement)
// header('Content-Security-Policy-Report-Only: ' . implode('; ', $cspDirectives), true);
}
/
Génère un nonce aléatoire pour CSP
/
private function generateCSPNonce(): string {
return base64encode(randombytes(16));
}
/
Vérifie si la page est sensible (login, checkout, etc.)
/
private function isSensitivePage(): bool {
$uri = $SERVER['REQUESTURI'] ?? '';
$sensitivePaths = ['/login', '/checkout', '/account', '/admin'];
foreach ($sensitivePaths as $path) {
if (strpos($uri, $path) !== false) {
return true;
}
}
return false;
}
/
Headers spécifiques pour pages sensibles
/
private function applySensitivePageHeaders(): void {
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true);
header('Pragma: no-cache', true);
header('Expires: 0', true);
}
}
// Utilisation dans votre bootstrap/index.php
$securityHeaders = new SecurityHeadersManager($ENV['APPENV'] ?? 'production');
$securityHeaders->applySecurityHeaders();
Le CSP est le header le plus puissant contre les attaques XSS. Il définit quelles sources sont autorisées pour chaque type de contenu.
Générateur de CSP configuré
/
class CSPBuilder {
private array $directives = [];
private ?string $nonce = null;
private bool $reportOnly = false;
public function construct() {
$this->nonce = base64encode(randombytes(16));
}
/
Configure la directive default-src
/
public function setDefaultSrc(array $sources): self {
$this->directives['default-src'] = $sources;
return $this;
}
/
Configure les sources JavaScript
/
public function setScriptSrc(array $sources, bool $allowInline = false): self {
$scriptSources = $sources;
if ($allowInline) {
$scriptSources[] = "'nonce-{$this->nonce}'";
// Alternative : 'unsafe-inline' (déconseillé)
}
$this->directives['script-src'] = $scriptSources;
return $this;
}
/
Configure les sources CSS
/
public function setStyleSrc(array $sources, bool $allowInline = false): self {
$styleSources = $sources;
if ($allowInline) {
$styleSources[] = "'nonce-{$this->nonce}'";
}
$this->directives['style-src'] = $styleSources;
return $this;
}
/
Configure les sources d'images
/
public function setImgSrc(array $sources): self {
$this->directives['img-src'] = $sources;
return $this;
}
/
Configure les sources de fonts
/
public function setFontSrc(array $sources): self {
$this->directives['font-src'] = $sources;
return $this;
}
/
Configure les endpoints de connexion (fetch, XHR, WebSocket)
/
public function setConnectSrc(array $sources): self {
$this->directives['connect-src'] = $sources;
return $this;
}
/
Empêche le chargement en iframe
/
public function disableFraming(): self {
$this->directives['frame-ancestors'] = ["'none'"];
return $this;
}
/
Configure l'URL de reporting des violations
/
public function setReportUri(string $uri): self {
$this->directives['report-uri'] = [$uri];
return $this;
}
/
Active le mode report-only (test sans bloquer)
/
public function setReportOnly(bool $reportOnly = true): self {
$this->reportOnly = $reportOnly;
return $this;
}
/
Récupère le nonce pour utilisation dans les templates
/
public function getNonce(): string {
return $this->nonce;
}
/
Construit et envoie le header CSP
/
public function build(): string {
$cspParts = [];
foreach ($this->directives as $directive => $sources) {
$cspParts[] = $directive . ' ' . implode(' ', $sources);
}
$csp = implode('; ', $cspParts);
$headerName = $this->reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
header("{$headerName}: {$csp}", true);
return $csp;
}
}
// Configuration pour une application moderne
$csp = new CSPBuilder();
$csp
->setDefaultSrc(["'self'"])
->setScriptSrc([
"'self'",
'https://cdn.jsdelivr.net',
'https://www.google-analytics.com'
], true) // allowInline = true pour utiliser le nonce
->setStyleSrc([
"'self'",
'https://fonts.googleapis.com'
], true)
->setImgSrc([
"'self'",
'data:',
'https:',
'https://www.google-analytics.com'
])
->setFontSrc([
"'self'",
'https://fonts.gstatic.com'
])
->setConnectSrc([
"'self'",
'https://api.example.com',
'wss://websocket.example.com'
])
->disableFraming()
->setReportUri('/csp-report-endpoint')
->build();
// Utilisation du nonce dans les templates
define('CSPNONCE', $csp->getNonce());
Application Sécurisée
# Contenu Sécurisé
Endpoint de Rapport CSP
Endpoint pour recevoir les rapports de violation CSP
/
class CSPReportHandler {
private PDO $pdo;
private string $logFile;
public function construct(PDO $pdo, string $logFile = '/var/log/csp-violations.log') {
$this->pdo = $pdo;
$this->logFile = $logFile;
}
/
Traite les rapports de violation CSP
/
public function handleReport(): void {
// Lire le JSON du corps de la requête
$json = filegetcontents('php://input');
if (empty($json)) {
httpresponsecode(400);
return;
}
$report = jsondecode($json, true);
if (jsonlasterror() !== JSONERRORNONE || !isset($report['csp-report'])) {
httpresponsecode(400);
return;
}
$violation = $report['csp-report'];
// Stocker en base de données
$this->storeViolation($violation);
// Logger dans un fichier
$this->logViolation($violation);
// Alerter si violation critique
if ($this->isCriticalViolation($violation)) {
$this->alertSecurity($violation);
}
httpresponsecode(204);
}
/
Stocke la violation en base de données
/
private function storeViolation(array $violation): void {
$stmt = $this->pdo->prepare(
"INSERT INTO cspviolations
(documenturi, blockeduri, violateddirective, sourcefile, linenumber, useragent, ip, createdat)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())"
);
$stmt->execute([
$violation['document-uri'] ?? null,
$violation['blocked-uri'] ?? null,
$violation['violated-directive'] ?? null,
$violation['source-file'] ?? null,
$violation['line-number'] ?? null,
$SERVER['HTTPUSERAGENT'] ?? null,
$SERVER['REMOTEADDR'] ?? null
]);
}
/
Logger la violation
/
private function logViolation(array $violation): void {
$logEntry = sprintf(
"[%s] CSP Violation: %s blocked %s on %s (User-Agent: %s, IP: %s)n",
date('Y-m-d H:i:s'),
$violation['violated-directive'] ?? 'unknown',
$violation['blocked-uri'] ?? 'unknown',
$violation['document-uri'] ?? 'unknown',
$SERVER['HTTPUSERAGENT'] ?? 'unknown',
$SERVER['REMOTEADDR'] ?? 'unknown'
);
errorlog($logEntry, 3, $this->logFile);
}
/
Détermine si une violation est critique
/
private function isCriticalViolation(array $violation): bool {
$directive = $violation['violated-directive'] ?? '';
// Les violations script-src sont critiques (potentiel XSS)
if (strpos($directive, 'script-src') !== false) {
return true;
}
// Vérifier si c'est une tentative d'injection
$blockedUri = $violation['blocked-uri'] ?? '';
if (strpos($blockedUri, 'data:') === 0 || strpos($blockedUri, 'blob:') === 0) {
return true;
}
return false;
}
/
Alerte l'équipe de sécurité
/
private function alertSecurity(array $violation): void {
// Implémenter notification (email, Slack, PagerDuty, etc.)
$message = "CSP Critical Violation Detected:n" . jsonencode($violation, JSONPRETTYPRINT);
// Exemple : envoyer un email
mail(
'security@example.com',
'CSP Critical Violation',
$message,
'From: csp-monitor@example.com'
);
}
}
// Route pour recevoir les rapports
// /csp-report-endpoint
if ($SERVER['REQUESTURI'] === '/csp-report-endpoint') {
$handler = new CSPReportHandler($pdo);
$handler->handleReport();
exit;
}
CORS : Cross-Origin Resource Sharing
Comprendre CORS
CORS permet à une application web d’accéder à des ressources d’une autre origine (domaine, protocole ou port différent). Sans configuration CORS, les navigateurs bloquent ces requêtes par sécurité.
Scénarios CORS
# Même origine (pas de CORS nécessaire)
https://example.com/api → https://example.com/page
✅ Autorisé
# Origines différentes (CORS requis)
https://app.example.com → https://api.example.com
❌ Bloqué sans CORS
https://example.com → https://example.org
❌ Bloqué sans CORS
http://example.com → https://example.com (protocole différent)
❌ Bloqué sans CORS
Configuration CORS Sécurisée
Gestionnaire CORS configuré
/
class CORSManager {
private array $allowedOrigins = [];
private array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
private array $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'];
private bool $allowCredentials = false;
private int $maxAge = 3600; // Cache preflight 1h
/
Configure les origines autorisées
/
public function setAllowedOrigins(array $origins): self {
$this->allowedOrigins = $origins;
return $this;
}
/
Configure les méthodes HTTP autorisées
/
public function setAllowedMethods(array $methods): self {
$this->allowedMethods = $methods;
return $this;
}
/
Configure les headers autorisés
/
public function setAllowedHeaders(array $headers): self {
$this->allowedHeaders = $headers;
return $this;
}
/
Active/désactive les credentials (cookies, auth)
/
public function setAllowCredentials(bool $allow): self {
$this->allowCredentials = $allow;
return $this;
}
/
Applique les headers CORS
/
public function handleCORS(): void {
$origin = $SERVER['HTTPORIGIN'] ?? '';
// Vérifier si l'origine est autorisée
if (!$this->isOriginAllowed($origin)) {
// Ne pas envoyer de headers CORS si origine non autorisée
// Cela bloquera la requête côté navigateur
return;
}
// Access-Control-Allow-Origin
header("Access-Control-Allow-Origin: {$origin}", true);
// Access-Control-Allow-Credentials
if ($this->allowCredentials) {
header('Access-Control-Allow-Credentials: true', true);
}
// Traiter requête preflight (OPTIONS)
if ($SERVER['REQUESTMETHOD'] === 'OPTIONS') {
$this->handlePreflight();
exit(0);
}
}
/
Vérifie si une origine est autorisée
/
private function isOriginAllowed(string $origin): bool {
if (empty($origin)) {
return false;
}
// Wildcard (ATTENTION : dangereux en production)
if (inarray('', $this->allowedOrigins)) {
return true;
}
// Vérification exacte
if (inarray($origin, $this->allowedOrigins)) {
return true;
}
// Vérification par pattern (ex: .example.com)
foreach ($this->allowedOrigins as $allowed) {
if ($this->matchOriginPattern($origin, $allowed)) {
return true;
}
}
return false;
}
/
Matche un pattern d'origine (support wildcards)
/
private function matchOriginPattern(string $origin, string $pattern): bool {
// Convertir le pattern en regex
$regex = '/^' . strreplace(
['', '.'],
['[^.]+', '.'],
$pattern
) . '$/';
return pregmatch($regex, $origin) === 1;
}
/
Traite une requête preflight (OPTIONS)
/
private function handlePreflight(): void {
// Access-Control-Allow-Methods
header(
'Access-Control-Allow-Methods: ' . implode(', ', $this->allowedMethods),
true
);
// Access-Control-Allow-Headers
header(
'Access-Control-Allow-Headers: ' . implode(', ', $this->allowedHeaders),
true
);
// Access-Control-Max-Age
header("Access-Control-Max-Age: {$this->maxAge}", true);
httpresponsecode(204);
}
}
// Configuration pour une API
$cors = new CORSManager();
$cors
->setAllowedOrigins([
'https://app.example.com',
'https://admin.example.com',
'https://.example.com' // Tous les sous-domaines
])
->setAllowedMethods(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
->setAllowedHeaders(['Content-Type', 'Authorization', 'X-API-Key'])
->setAllowCredentials(true) // Autoriser cookies/auth
->handleCORS();
CORS : Configurations par Cas d’Usage
setAllowedOrigins(['']) // Toutes origines
->setAllowedMethods(['GET', 'OPTIONS'])
->setAllowCredentials(false)
->handleCORS();
// Cas 2 : API privée (authentification requise)
$privateAPICors = new CORSManager();
$privateAPICors
->setAllowedOrigins(['https://app.example.com'])
->setAllowedMethods(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
->setAllowedHeaders(['Content-Type', 'Authorization'])
->setAllowCredentials(true) // Pour envoyer cookies de session
->handleCORS();
// Cas 3 : Développement local
if ($ENV['APPENV'] === 'development') {
$devCors = new CORSManager();
$devCors
->setAllowedOrigins([
'http://localhost:3000',
'http://localhost:5173', // Vite
'http://127.0.0.1:3000'
])
->setAllowedMethods(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
->setAllowCredentials(true)
->handleCORS();
}
Cas d’Étude : Sécurisation d’une SPA + API
Contexte
Une Single Page Application (Vue.js) consommant une API REST PHP doit être sécurisée contre XSS, CSRF et injections.
Architecture
Frontend (SPA) Backend (API)
https://app.example.com → https://api.example.com
Configuration Complète
Configuration de sécurité complète pour API
/
class APISecurityConfig {
private SecurityHeadersManager $headers;
private CORSManager $cors;
private CSPBuilder $csp;
public function construct() {
$this->initializeHeaders();
$this->initializeCORS();
$this->initializeCSP();
}
private function initializeHeaders(): void {
$this->headers = new SecurityHeadersManager('production');
}
private function initializeCORS(): void {
$this->cors = new CORSManager();
$this->cors
->setAllowedOrigins([
'https://app.example.com',
'https://admin.example.com'
])
->setAllowedMethods(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
->setAllowedHeaders([
'Content-Type',
'Authorization',
'X-CSRF-Token'
])
->setAllowCredentials(true);
}
private function initializeCSP(): void {
$this->csp = new CSPBuilder();
$this->csp
->setDefaultSrc(["'self'"])
->setScriptSrc(["'self'"], false) // Pas de inline scripts
->setStyleSrc(["'self'"], false)
->setConnectSrc(["'self'"])
->disableFraming();
}
/
Applique toute la configuration de sécurité
/
public function apply(): void {
$this->headers->applySecurityHeaders();
$this->cors->handleCORS();
$this->csp->build();
}
}
// Appliquer la configuration
$security = new APISecurityConfig();
$security->apply();
Tests de Sécurité
#!/bin/bash
# test-security-headers.sh
URL="https://api.example.com"
echo "=== Test des Headers de Sécurité ==="
echo
# Tester HSTS
echo "1. Strict-Transport-Security:"
curl -sI "$URL" | grep -i "strict-transport-security"
echo
# Tester X-Frame-Options
echo "2. X-Frame-Options:"
curl -sI "$URL" | grep -i "x-frame-options"
echo
# Tester CSP
echo "3. Content-Security-Policy:"
curl -sI "$URL" | grep -i "content-security-policy"
echo
# Tester CORS
echo "4. CORS (requête OPTIONS):"
curl -X OPTIONS "$URL/api/users"
-H "Origin: https://app.example.com"
-H "Access-Control-Request-Method: GET"
-i | grep -i "access-control"
echo
# Tester X-Content-Type-Options
echo "5. X-Content-Type-Options:"
curl -sI "$URL" | grep -i "x-content-type-options"
echo
echo "=== Test terminé ==="
Ressources et Outils
Outils de Validation
- SecurityHeaders.com : Scanner de headers de sécurité
- Mozilla Observatory : Audit de sécurité web complet
- CSP Evaluator (Google) : Validateur de CSP
- CORS Tester : Tester les configurations CORS
Checklist de Sécurité Headers
# Checklist Headers de Sécurité
## Configuration Serveur
[ ] HTTPS activé (TLS 1.3)
[ ] Certificat SSL valide
[ ] Redirection HTTP vers HTTPS
[ ] HSTS configuré (max-age 2 ans minimum)
## Headers Essentiels
[ ] Strict-Transport-Security présent
[ ] X-Frame-Options configuré (DENY ou SAMEORIGIN)
[ ] X-Content-Type-Options: nosniff
[ ] Referrer-Policy défini
[ ] Permissions-Policy configuré
## Content Security Policy
[ ] CSP défini (pas de 'unsafe-inline' ou 'unsafe-eval')
[ ] Nonces utilisés pour scripts/styles inline
[ ] Endpoint de rapport CSP configuré
[ ] CSP testé en mode report-only d'abord
## CORS (si applicable)
[ ] Origines autorisées définies (pas de wildcard)
[ ] Méthodes HTTP limitées au nécessaire
[ ] Headers autorisés minimaux
[ ] Credentials correctement configurés
## Autres
[ ] Headers sensibles supprimés (X-Powered-By, Server)
[ ] Cache-Control configuré pour pages sensibles
[ ] Tests automatisés dans CI/CD
Conclusion
Les headers de sécurité, CORS et CSP forment un bouclier essentiel contre les attaques web modernes. Bien configurés, ils peuvent prévenir la majorité des vulnérabilités côté client sans impacter les performances.
Points Clés
HTTPS d’abord : Aucun header de sécurité ne remplace une connexion chiffrée
CSP strict : Évitez ‘unsafe-inline’ et ‘unsafe-eval’
CORS restrictif : Whitelistez uniquement les origines nécessaires
Testez progressivement : Utilisez report-only avant de bloquer
Automatisez les tests* : Intégrez la validation dans votre CI/CD
Une configuration de sécurité robuste est un processus itératif. Commencez par les headers essentiels, puis raffinez progressivement votre CSP et CORS en fonction de vos besoins réels.
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.