.htaccess
Ce document explique comment utiliser un fichier .htaccess Apache pour réduire la surface d’attaque d’une application PHP MVC.
Important : un fichier .htaccess ne corrige pas toutes les failles de sécurité. Il ne remplace pas :
Il sert plutôt à renforcer la configuration Apache et à éviter certaines expositions dangereuses.
Un fichier .htaccess est un fichier de configuration Apache placé dans un dossier de l’application.
Il permet notamment de :
uploads/
telecharger/
En revanche, il ne connaît pas la logique métier de l’application. Il ne peut donc pas savoir si un utilisateur a vraiment le droit d’accéder à une facture, un profil ou un document.
Dans une application PHP MVC, la bonne structure est généralement (comme pour Symfony) :
mon-projet/ ├── app/ ├── config/ ├── database/ ├── vendor/ └── public/ ├── index.php ├── .htaccess └── assets/
Le serveur Apache doit pointer vers le dossier : public/
public/
Le fichier .htaccess doit donc être placé ici :
public/.htaccess
Cela évite d’exposer directement :
app/ config/ database/ vendor/ .env composer.json docker-compose.yml
# ============================================================ # .htaccess de sécurité pour application PHP MVC # Apache 2.4+ # À placer dans le dossier public/ # ============================================================ # ------------------------------------------------------------ # 1. Activer le moteur de réécriture d’URL # ------------------------------------------------------------ <IfModule mod_rewrite.c> RewriteEngine On </IfModule> # ------------------------------------------------------------ # 2. Forcer HTTPS # À activer uniquement en production avec un certificat valide. # En local Wamp/Docker sans HTTPS, laisser commenté. # ------------------------------------------------------------ # <IfModule mod_rewrite.c> # RewriteCond %{HTTPS} !=on # RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] # </IfModule> # ------------------------------------------------------------ # 3. Rediriger les requêtes MVC vers index.php dans notre cas # ------------------------------------------------------------ <IfModule mod_rewrite.c> RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^ - [L] RewriteRule ^ index.php [L] </IfModule> # ------------------------------------------------------------ # 4. Désactiver le listing des dossiers # ------------------------------------------------------------ Options -Indexes # ------------------------------------------------------------ # 5. Bloquer les fichiers sensibles # ------------------------------------------------------------ <FilesMatch "^(\.env|\.git|\.htaccess|composer\.json|composer\.lock|package\.json|package-lock\.json|yarn\.lock|docker-compose\.yml|Dockerfile|README\.md)$"> Require all denied </FilesMatch> # ------------------------------------------------------------ # 6. Bloquer les fichiers techniques ou de sauvegarde # ------------------------------------------------------------ <FilesMatch "\.(ini|log|conf|sql|bak|backup|old|orig|dist|sh|bat|cmd|ps1|yaml|yml)$"> Require all denied </FilesMatch> # ------------------------------------------------------------ # 7. Bloquer les dossiers techniques s’ils sont exposés # ------------------------------------------------------------ <IfModule mod_rewrite.c> RewriteRule ^(app|src|vendor|database|config|tests|storage|var|logs)/ - [F,L] </IfModule> # ------------------------------------------------------------ # 8. Limiter les méthodes HTTP autorisées # Attention : à adapter pour une API REST. # ------------------------------------------------------------ <LimitExcept GET POST HEAD> Require all denied </LimitExcept> # ------------------------------------------------------------ # 9. Ajouter des en-têtes HTTP de sécurité # ------------------------------------------------------------ <IfModule mod_headers.c> Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" </IfModule> # ------------------------------------------------------------ # 10. Content Security Policy basique # À adapter si vous utilisez Tailwind, Flowbite ou un CDN. # ------------------------------------------------------------ <IfModule mod_headers.c> Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" </IfModule> # ------------------------------------------------------------ # 11. HSTS # À activer uniquement si HTTPS fonctionne parfaitement. # ------------------------------------------------------------ # <IfModule mod_headers.c> # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" # </IfModule> # ------------------------------------------------------------ # 12. Masquer certains en-têtes techniques # ------------------------------------------------------------ <IfModule mod_headers.c> Header unset X-Powered-By Header unset Server </IfModule> # ------------------------------------------------------------ # 13. Bloquer quelques requêtes grossièrement suspectes # Attention : cela ne remplace pas une correction dans le code. # ------------------------------------------------------------ <IfModule mod_rewrite.c> RewriteCond %{REQUEST_URI} "\.\." [NC,OR] RewriteCond %{QUERY_STRING} "(php://|data://|expect://|input://)" [NC,OR] RewriteCond %{QUERY_STRING} "(union.*select|select.*from|insert.*into|drop.*table|sleep\(|benchmark\()" [NC,OR] RewriteCond %{QUERY_STRING} "(<script|%3Cscript|javascript:|onerror=|onload=)" [NC] RewriteRule ^ - [F,L] </IfModule> # ------------------------------------------------------------ # 14. Bloquer l’exécution PHP dans le dossier uploads/ # ------------------------------------------------------------ <IfModule mod_rewrite.c> RewriteRule ^uploads/.*\.php$ - [F,L] RewriteRule ^uploads/.*\.phtml$ - [F,L] RewriteRule ^uploads/.*\.phar$ - [F,L] </IfModule> # ------------------------------------------------------------ # 15. Limiter la taille et le temps des requêtes PHP # Fonctionne surtout avec mod_php. # Avec PHP-FPM/FCGI, configurer php.ini ou le pool PHP. # ------------------------------------------------------------ <IfModule mod_php.c> php_value upload_max_filesize 5M php_value post_max_size 6M php_value max_execution_time 30 php_value max_input_time 30 </IfModule> # ------------------------------------------------------------ # 16. Désactiver l’affichage des erreurs PHP en production # ------------------------------------------------------------ <IfModule mod_php.c> php_flag display_errors Off php_flag log_errors On </IfModule> # ------------------------------------------------------------ # 17. Encodage par défaut # ------------------------------------------------------------ AddDefaultCharset UTF-8
RewriteEngine On
Active le moteur de réécriture d’URL Apache. Il permet de rediriger toutes les URL applicatives vers index.php, qui joue le rôle de contrôleur frontal en MVC.
index.php
Exemple :
/failles/A05
peut être traité par :
public/index.php
RewriteCond %{HTTPS} !=on RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Cette règle redirige automatiquement HTTP vers HTTPS.
À utiliser uniquement lorsque :
En local, il faut généralement laisser cette règle commentée.
RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^ - [L] RewriteRule ^ index.php [L]
Signification :
C’est le fonctionnement classique d’une application MVC.
Options -Indexes
Sans cette directive, Apache peut afficher le contenu d’un dossier si aucun fichier index.php ou index.html n’est présent.
index.html
Exemples dangereux :
/uploads/ /logs/ /backup/
Avec Options -Indexes, Apache refuse d’afficher la liste des fichiers.
<FilesMatch "^(\.env|\.git|\.htaccess|composer\.json|composer\.lock|docker-compose\.yml|Dockerfile)$"> Require all denied </FilesMatch>
Cette règle bloque l’accès à des fichiers qui ne doivent jamais être lus depuis un navigateur.
Exemple de fichier dangereux :
.env
Il peut contenir :
DB_PASSWORD=secret APP_SECRET=abc123
<FilesMatch "\.(ini|log|conf|sql|bak|backup|old|yaml|yml)$"> Require all denied </FilesMatch>
Cette règle bloque les extensions sensibles.
.sql
.log
.bak
.yml
.ini
RewriteRule ^(app|src|vendor|database|config|tests|storage|var|logs)/ - [F,L]
Cette règle bloque l’accès direct à des dossiers internes. Dans une application bien structurée, ces dossiers ne devraient pas être accessibles depuis le web.
<LimitExcept GET POST HEAD> Require all denied </LimitExcept>
Cette directive autorise uniquement :
GET POST HEAD
Elle bloque :
PUT PATCH DELETE TRACE OPTIONS
Pour une application MVC classique, c’est souvent acceptable. Pour une API REST, il faut adapter cette règle, car les méthodes PUT, PATCH, DELETE et OPTIONS peuvent être nécessaires.
PUT
PATCH
DELETE
OPTIONS
X-Content-Type-Options
Header always set X-Content-Type-Options "nosniff"
Empêche le navigateur de deviner le type d’un fichier. Cela limite certains comportements dangereux où un fichier pourrait être interprété comme un script.
X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"
Protège contre le clickjacking (je ne sais pas letraduire en français ;) ). Le clickjacking consiste à intégrer une page dans une iframe invisible pour piéger l’utilisateur.
Valeurs possibles :
Referrer-Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Réduit les informations transmises dans l’en-tête Referer. Cela évite d’envoyer des URLs complètes à des sites externes.
Referer
Permissions-Policy
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
Désactive certaines API navigateur. Si une application n’a pas besoin de caméra, micro, géolocalisation ou paiement, il vaut mieux les interdire.
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;"
La CSP limite les ressources que le navigateur peut charger.
Quelques directives importantes :
default-src 'self'
script-src 'self'
style-src 'self'
object-src 'none'
frame-ancestors 'self'
form-action 'self'
Attention : unsafe-inline est pratique, mais moins sécurisé. Il faut l’éviter en production si possible.
unsafe-inline
Si vous utilisez Tailwind ou Flowbite depuis un CDN, la CSP stricte peut bloquer les ressources.
Exemple plus permissif pour un TP local :
<IfModule mod_headers.c> Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data:; font-src 'self' data: https://cdnjs.cloudflare.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';" </IfModule>
En production, il vaut mieux :
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
HSTS indique au navigateur :
Utilise toujours HTTPS pour ce domaine.
Très utile en production, mais à activer seulement si HTTPS est parfaitement fonctionnel.
Header unset X-Powered-By Header unset Server
Cela tente de masquer certaines informations techniques.
Attention : l’en-tête Server ne peut pas toujours être supprimé depuis .htaccess.
Server
Il faut parfois configurer Apache globalement :
ServerTokens Prod ServerSignature Off
RewriteCond %{QUERY_STRING} "(union.*select|drop.*table|<script|javascript:)" [NC] RewriteRule ^ - [F,L]
Cela bloque quelques attaques très visibles. Mais ce n’est pas une vraie protection contre les injections.
Une injection SQL se corrige avec une requête préparée :
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?'); $stmt->execute([$email]);
Une XSS se corrige avec un échappement :
echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');
RewriteRule ^uploads/.*\.php$ - [F,L] RewriteRule ^uploads/.*\.phtml$ - [F,L] RewriteRule ^uploads/.*\.phar$ - [F,L]
Très important si l’application accepte l’envoi de fichiers.
Un attaquant peut essayer d’envoyer :
shell.php avatar.phtml image.phar
Puis tenter d’appeler :
/uploads/shell.php
Cette règle bloque l’exécution de scripts PHP dans uploads/.
php_value upload_max_filesize 5M php_value post_max_size 6M
Ces directives limitent la taille des fichiers envoyés.
Attention : elles fonctionnent surtout avec mod_php.
mod_php
Avec PHP-FPM ou FCGI, il faut configurer php.ini ou le pool PHP.
php.ini
php_flag display_errors Off php_flag log_errors On
En production, les erreurs ne doivent pas être affichées au navigateur.
Mauvais exemple :
Fatal error: Uncaught PDOException in /var/www/app/Database.php line 12
Cela révèle trop d’informations techniques.
Pour un TP local, cette version est plus simple et limite les risques de casser Tailwind, Flowbite ou Swagger.
<IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^ - [L] RewriteRule ^ index.php [L] </IfModule> Options -Indexes <FilesMatch "^(\.env|\.git|\.htaccess|composer\.json|composer\.lock|docker-compose\.yml|Dockerfile)$"> Require all denied </FilesMatch> <FilesMatch "\.(ini|log|conf|sql|bak|backup|old|yml|yaml)$"> Require all denied </FilesMatch> <IfModule mod_rewrite.c> RewriteRule ^(app|src|vendor|database|config|tests|logs)/ - [F,L] RewriteRule ^uploads/.*\.(php|phtml|phar)$ - [F,L] </IfModule> <LimitExcept GET POST HEAD> Require all denied </LimitExcept> <IfModule mod_headers.c> Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" </IfModule> AddDefaultCharset UTF-8
À corriger dans le code :
À corriger à l’affichage :
À corriger avec un token :
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { http_response_code(403); exit('CSRF invalide'); }
À corriger avec :
$hash = password_hash($password, PASSWORD_DEFAULT); if (password_verify($passwordSaisi, $hash)) { echo "Connexion acceptée"; }
À corriger côté serveur :
$stmt = $pdo->prepare( 'SELECT * FROM documents WHERE id = ? AND owner_id = ?' ); $stmt->execute([$documentId, $userId]);
Un utilisateur ne doit pas pouvoir accéder à une ressource simplement parce qu’il connaît son identifiant.
Un .htaccess sécurisé sert à :
Mais la vraie sécurité applicative repose surtout sur :
Le .htaccess, c’est le vigile à l’entrée. Mais si l’application laisse les clés du coffre sur le comptoir, le vigile ne pourra pas faire de miracle.