Quand on lance un site, la première préoccupation c'est le rendu et la performance. La sécurité des headers HTTP passe souvent au second plan. Pourtant, un audit de sécurité révèle vite deux problèmes récurrents : l'absence de Content-Security-Policy, et des scripts inline sans protection. Dans cet article, je vous explique comment mettre en place CSP avec des nonces en PHP, et comment optimiser le chargement de vos pages avec preload, preconnect et dns-prefetch.
Content-Security-Policy : pourquoi c'est indispensable
Le header Content-Security-Policy (CSP) contrôle quelles ressources le navigateur a le droit de charger sur votre page. Sans CSP, n'importe quel script injecté (via une faille XSS, un CDN compromis, ou une extension malveillante) peut s'exécuter librement.
Concrètement, CSP définit une liste blanche par type de ressource :
- script-src : d'où peuvent venir les scripts JavaScript
- style-src : d'où peuvent venir les feuilles de style
- img-src : d'où peuvent venir les images
- font-src : d'où peuvent venir les polices
- connect-src : quelles URLs peuvent être appelées en AJAX/fetch
- frame-src : quels domaines peuvent être intégrés en iframe
- default-src : la politique par défaut pour tout ce qui n'est pas spécifié
Tout ce qui n'est pas explicitement autorisé est bloqué par le navigateur. C'est radical, mais c'est exactement ce qu'on veut.
Implémenter CSP avec des nonces en PHP
L'approche la plus sûre pour autoriser les scripts inline est d'utiliser un nonce (number used once) : un jeton aléatoire généré à chaque requête. Seuls les scripts portant ce nonce seront exécutés.
Générer le nonce et envoyer le header
Le principe est simple : on génère un nonce unique, on l'inclut dans le header CSP, puis on l'ajoute à chaque balise <script> ou <style> inline :
<?php
// Générer un nonce unique pour cette requête
$nonce = base64_encode(random_bytes(16));
// Construire la politique CSP
$policy = implode('; ', [
"default-src 'self'",
"script-src 'self' 'nonce-{$nonce}' https://cdn.jsdelivr.net",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' data: https:",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
]);
// Envoyer le header avant tout output HTML
header("Content-Security-Policy: {$policy}");L'idée clé : random_bytes(16) génère 16 octets cryptographiquement sûrs, encodés en base64. Chaque page servie a un nonce différent. Un attaquant ne peut pas le deviner.
Utiliser le nonce dans les templates
Dans vos templates, chaque script ou style inline doit porter l'attribut nonce :
<!-- Le CSS inline utilise le nonce -->
<style nonce="<?= $nonce ?>">
body { margin: 0; background: #111; }
</style>
<!-- Les scripts inline aussi -->
<script nonce="<?= $nonce ?>">
document.addEventListener('DOMContentLoaded', function() {
// Ce script est autorisé grâce au nonce
console.log('Page chargée');
});
</script>Sans l'attribut nonce, le navigateur bloque le script. Un script injecté par un attaquant ne connaît pas le nonce et sera donc systématiquement refusé.
Pourquoi un nonce plutôt que 'unsafe-inline' ?
Beaucoup de tutoriels proposent 'unsafe-inline' dans script-src pour éviter de casser les scripts inline. C'est une fausse bonne idée : cela revient à désactiver CSP pour les scripts, puisque n'importe quel script injecté sera aussi considéré comme "inline".
- 'unsafe-inline' : autorise TOUS les scripts inline → inutile contre XSS
- 'nonce-xxx' : autorise UNIQUEMENT les scripts avec le bon nonce → protège contre XSS
Note : Pour style-src, l'usage de 'unsafe-inline' reste un compromis courant, notamment avec des frameworks CSS utilitaires comme Tailwind qui génèrent des styles inline. L'injection de CSS est beaucoup moins dangereuse que l'injection de JavaScript.
Les directives CSP essentielles
Voici les directives les plus importantes à configurer, et leur rôle :
| Directive | Valeur recommandée | Rôle |
|---|---|---|
default-src | 'self' | Par défaut, n'autorise que son propre domaine |
script-src | 'self' 'nonce-...' | Scripts locaux et inline noncés uniquement |
style-src | 'self' + CDN autorisés | Styles locaux et sources de confiance |
object-src | 'none' | Bloque Flash, Java et autres plugins — vecteurs d'attaque historiques |
base-uri | 'self' | Empêche le détournement de la balise <base> |
form-action | 'self' | Les formulaires ne peuvent poster que vers votre domaine |
Ajoutez ensuite les domaines tiers dont vous avez besoin (Google Fonts, analytics, CDN, etc.) dans les directives concernées.
Resource Hints : accélérer le chargement
CSP sécurise. Les resource hints accélèrent. Ce sont des indications données au navigateur pour qu'il prépare les connexions et les ressources avant d'en avoir besoin. Il en existe trois, et ils ne font pas la même chose.
dns-prefetch : résoudre le DNS en avance
Quand le navigateur rencontre une URL externe, il doit d'abord résoudre le nom de domaine en adresse IP via le DNS. Cette opération prend entre 20 et 120 ms.
dns-prefetch déclenche cette résolution DNS dès que le navigateur lit le <head>, sans attendre de rencontrer la ressource :
<!-- Le navigateur résout le DNS immédiatement -->
<link rel="dns-prefetch" href="https://cdn.example.com">Coût : quasi nul (un paquet DNS). Gain : 20-120 ms par domaine. À utiliser pour les domaines externes non critiques (analytics, publicités, CDN secondaires).
preconnect : établir la connexion complète
preconnect va plus loin que dns-prefetch. Il effectue trois étapes en avance :
- Résolution DNS (comme dns-prefetch)
- Handshake TCP (établissement de la connexion)
- Négociation TLS/SSL (pour HTTPS)
Ces trois étapes peuvent représenter 200 à 400 ms cumulées. Avec preconnect, tout est prêt quand le navigateur a besoin de la ressource :
<!-- Google Fonts : deux domaines à préconnecter -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- CDN externe -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>L'attribut crossorigin : Il est indispensable pour les domaines qui servent des ressources avec CORS (polices, scripts de CDN). Sans lui, le navigateur ouvrira une seconde connexion au moment du téléchargement, annulant le bénéfice du preconnect. Pour fonts.googleapis.com (le CSS), pas besoin. Pour fonts.gstatic.com (les fichiers de polices), il est obligatoire.
preload : télécharger la ressource immédiatement
preload est le plus agressif des trois. Il dit au navigateur : "télécharge cette ressource maintenant, elle est critique pour cette page". La ressource est téléchargée en haute priorité, en parallèle du parsing HTML :
<!-- Précharger un script critique -->
<link rel="preload" href="/assets/js/app.min.js" as="script">
<!-- L'attribut "as" est obligatoire et doit correspondre au type -->
<link rel="preload" href="/assets/css/app.min.css" as="style">
<link rel="preload" href="/assets/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>Attention : preload est puissant mais doit être utilisé avec parcimonie. Chaque preload consomme de la bande passante. Si vous préchargez trop de ressources, vous ralentissez le chargement de celles qui sont vraiment critiques.
En général, on preload uniquement les ressources découvertes tardivement par le navigateur (un JS en bas du <body>, une police référencée dans un CSS externe, etc.).
Récapitulatif : quand utiliser quoi
| Resource Hint | Ce qu'il fait | Gain typ. | Quand l'utiliser |
|---|---|---|---|
dns-prefetch | Résolution DNS | 20-120 ms | Domaines tiers non critiques (analytics, pubs) |
preconnect | DNS + TCP + TLS | 200-400 ms | Domaines tiers critiques (fonts, CDN principal) |
preload | Téléchargement complet | Variable | Ressources critiques découvertes tard (JS, fonts) |
Exemple complet dans le <head>
Voici un exemple de configuration complète. L'ordre de déclaration compte car le navigateur traite les hints séquentiellement :
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 1. Preconnect aux domaines critiques (priorité haute) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 2. DNS Prefetch + Preconnect pour CDN secondaire -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- 3. Preload des ressources critiques -->
<link rel="preload" href="/assets/js/app.min.js" as="script">
<!-- 4. CSS -->
<link rel="stylesheet" href="/assets/css/app.min.css">
</head>Pourquoi dns-prefetch ET preconnect ensemble ?
Vous avez remarqué qu'on utilise parfois les deux pour un même domaine. C'est un fallback : les navigateurs anciens qui ne supportent pas preconnect utiliseront au moins le dns-prefetch. Les navigateurs modernes ignoreront le dns-prefetch au profit du preconnect (qui inclut déjà la résolution DNS).
Les pièges courants
1. CSP trop restrictive au déploiement
Déployer une CSP stricte d'un coup, c'est la garantie de casser quelque chose : Google Fonts bloqué, analytics muet, scripts tiers en erreur. La console se remplit de messages Refused to load....
Leçon : déployez CSP en mode Content-Security-Policy-Report-Only d'abord. Ce mode journalise les violations dans la console sans rien bloquer. Vous pouvez ainsi construire votre politique progressivement :
// Phase 1 : observer sans bloquer
header("Content-Security-Policy-Report-Only: default-src 'self'");
// Phase 2 : une fois la politique stabilisée, appliquer
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-...' ...");2. Le piège du double header
Si votre application a plusieurs layouts (front, admin, API), attention à ne pas envoyer le header CSP deux fois avec deux nonces différents. Le navigateur appliquera le plus restrictif des deux, et vos scripts inline seront bloqués. La solution : un mécanisme de garde qui empêche l'envoi du header en doublon.
3. Trop de preload = contre-productif
Preloader le CSS, le JS, les polices et les images hero en même temps, c'est tentant. Mais Chrome vous préviendra :
The resource was preloaded using link preload but not used within a few seconds.Si une ressource preloadée n'est pas utilisée dans les ~3 secondes, c'est du gaspillage. Trop de preloads crée de la contention réseau et ralentit le chargement global. Limitez-vous au strict nécessaire.
Vérifier que tout fonctionne
Quelques outils pour valider votre implémentation :
- DevTools > Console : les violations CSP apparaissent en rouge
- DevTools > Network : vérifiez les headers de réponse (
Content-Security-Policy) - DevTools > Network > Initiator : les ressources preconnect/prefetch sont marquées
- CSP Evaluator (Google) : analyse et note votre politique CSP
- SecurityHeaders.com : audit complet des headers de sécurité
# Vérifier les headers CSP depuis le terminal
curl -sI https://votre-site.com | grep -i "content-security-policy"Conclusion
La sécurité et la performance ne sont pas opposées. CSP protège vos utilisateurs contre les injections, et les resource hints accélèrent leur expérience. Les deux s'implémentent dans le <head> et ne coûtent rien en maintenance une fois en place.
Dans mon expérience, l'ajout de CSP avec nonces a permis de résoudre des alertes de sécurité sans rien casser, et les resource hints ont amélioré le temps de chargement de manière mesurable — notamment le LCP (Largest Contentful Paint), en anticipant le téléchargement des polices et des scripts tiers.
Le plus important : commencez en mode Report-Only, ajoutez vos sources légitimes une par une, et ne preloadez que ce qui est vraiment critique. Mieux vaut une CSP stricte bien testée qu'une CSP permissive déployée à la hâte.