Bonnes pratiques 4 février 2026 12 min de lecture

CSP et Resource Hints : sécuriser et accélérer son site PHP

Gabriel Janez

Gabriel Janez

Développeur Full-Stack PHP

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 :

DirectiveValeur recommandéeRô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ésStyles 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 :

  1. Résolution DNS (comme dns-prefetch)
  2. Handshake TCP (établissement de la connexion)
  3. 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 HintCe qu'il faitGain typ.Quand l'utiliser
dns-prefetchRésolution DNS20-120 msDomaines tiers non critiques (analytics, pubs)
preconnectDNS + TCP + TLS200-400 msDomaines tiers critiques (fonts, CDN principal)
preloadTéléchargement completVariableRessources 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.

Tags

CSP dns-prefetch preconnect preload

Partager

Articles similaires

Besoin d'aide sur votre projet ?

Discutons de vos besoins et trouvons ensemble la meilleure solution technique.

Me contacter