Pourquoi votre serveur de développement casse sous script-src
Par Thomas Poyeton
Votre politique n'a rien d'anormal. Les serveurs de développement reposent
précisément sur ce qu'une directive script-src stricte est censée empêcher :
exécuter des chaînes de caractères comme du code et injecter des scripts en
ligne. Webpack exécute les modules recompilés via eval. Vite s'en passe,
mais injecte un script en ligne pour React Fast Refresh, et les deux poussent
leurs mises à jour par une websocket. La production n'a besoin de rien de tout
cela : la solution est une courte liste d'autorisations réservées au
développement, pas une politique de production affaiblie.
L'erreur eval vient du devtool de webpack
La première chose que l'on voit après avoir ajouté script-src à un projet
webpack est une page blanche et ce message :
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an
allowed source of script in the following Content Security Policy directive:
"script-src 'self'".
Aucun code du projet n'appelle eval. C'est webpack qui le fait. En
mode: 'development', l'option devtool vaut eval par défaut : chaque
module est compilé en chaîne de caractères puis exécuté avec eval(), avec un
commentaire //# sourceURL ajouté pour que les traces d'erreur pointent
toujours vers le fichier d'origine. Les recompilations sont rapides parce que
seule la chaîne du module modifié est réévaluée. Next.js et Vue CLI butent sur
le même mur en développement, car leurs builds de dev passent par le même
outillage fondé sur eval.
Vous avez deux portes de sortie. La première : garder le comportement par
défaut de webpack et ajouter 'unsafe-eval' à la politique envoyée par votre
serveur de développement, et à celle-là uniquement. La seconde : choisir un
devtool qui n'utilise pas eval :
// webpack.config.js
module.exports = {
devtool: 'cheap-module-source-map',
};
Toute valeur commençant par eval- réintroduit l'exigence, et les variantes
sans eval recompilent plus lentement sur les gros projets. C'est ce
ralentissement qui fait de l'autorisation 'unsafe-eval' limitée au dev le
meilleur compromis dans la plupart des cas.
Vite échoue sur un script en ligne
Vite sert vos fichiers source comme des modules ES natifs, donc le code
applicatif s'exécute sans eval. Ce qui casse est plus discret et plus dur à
diagnostiquer. Avec @vitejs/plugin-react, le serveur de développement
injecte dans la page un court script de module en ligne qui installe le
runtime React Fast Refresh. Une directive script-src sans 'unsafe-inline'
bloque ce script, et le symptôme visible n'est pas une erreur CSP mais ceci :
@vitejs/plugin-react can't detect preamble. Something is wrong.
La vraie cause, un message "Refused to execute inline script", se trouve
quelques lignes plus haut dans la console. En développement, le correctif
pratique est 'unsafe-inline' dans la politique de dev. Vite 5.3 a ajouté une
option html.cspNonce qui appose un attribut nonce sur chaque balise
injectée par Vite, mais un nonce n'aide que si quelque chose sert le HTML en y
substituant une valeur neuve à chaque réponse, ce qu'un simple vite dev ne
fait pas. Vite insère aussi les styles sous forme de balises <style> en
ligne pendant le développement, donc style-src a besoin de la même
autorisation de dev.
Si les mises à jour à chaud cessent de s'appliquer et que la console montre
une connexion refusée vers ws://localhost:5173/, ce n'est pas du tout
script-src. Le canal HMR est une websocket, régie par connect-src.
Deux serveurs, donc deux origines
Le montage le plus piégeux est un backend (Rails, Django, Express) qui rend le
HTML et envoie l'en-tête CSP, pendant que Vite ou webpack-dev-server sert le
JavaScript depuis un autre port. 'self' ne couvre plus vos propres scripts,
car un port différent est une origine différente. La politique de dev doit
nommer explicitement le serveur de développement :
Content-Security-Policy: script-src 'self' 'unsafe-eval' http://localhost:5173; connect-src 'self' ws://localhost:5173
Remplacez le port par celui de votre serveur de dev (webpack-dev-server utilise 8080 par défaut). Aucune de ces entrées n'a sa place dans l'en-tête de production, ce qui amène au dernier point.
Séparer la politique par environnement, puis vérifier ce qui part en production
Gardez une politique de production stricte et une politique de développement assouplie, choisies à l'endroit où l'en-tête est défini :
const dev = process.env.NODE_ENV !== 'production';
const csp = dev
? "script-src 'self' 'unsafe-eval' http://localhost:5173; connect-src 'self' ws://localhost:5173"
: `script-src 'nonce-${nonce}' 'strict-dynamic'`;
res.setHeader('Content-Security-Policy', csp);
Le scénario d'échec à surveiller est l'expédition en production de la branche
dev de cette condition. 'unsafe-eval' en production rouvre l'exécution de
chaînes comme du code pour chaque script de la page, alors que rien dans un
bundle de production n'en a besoin : webpack comme Vite produisent du code
sans eval au moment du build. Avant une mise en production, collez l'en-tête
de production dans
l'évaluateur de CentralCSP. Il signale
'unsafe-eval' et 'unsafe-inline' dès qu'ils s'échappent du développement.