creerProxy ne sait qu'écraser une directive CSP, jamais l'étendre : on ne peut pas ajouter unsafe-eval en développement sans perdre le nonce #55

Closed
opened 2026-09-11 13:38:21 +00:00 by nicolas-fryder · 2 comments

React a besoin d'eval() en développement. Pour l'autoriser, il faut toucher à script-src — la seule directive qui porte le nonce fabriqué par requête, donc la seule qu'on ne peut pas réécrire depuis la configuration.

Le symptôme

Dès qu'on ouvre une page servie par next dev :

eval() is not supported in this environment. If this page was served with a Content-Security-Policy header, make sure that 'unsafe-eval' is included. React requires eval() in development mode for various debugging features like reconstructing callstacks from a different environment.

La console se remplit, et les outils de mise au point ne fonctionnent qu'à moitié (piles d'appel incomplètes).

Pourquoi on ne peut pas simplement l'ajouter

directivesCsp (@eqs/platform-next/dist/server/proxy.js) fusionne par écrasement :

const base = {
  'script-src': ["'self'", `'nonce-${n}'`, "'strict-dynamic'"],
  // …
}
const tout = { ...base, ...(o.csp ?? {}) }

Fournir csp: { 'script-src': [...] } remplace donc la directive entière. Or n, le nonce, est produit à l'intérieur du proxy, à chaque requête : il n'existe pas au point où l'on écrit la configuration. Impossible de le reconduire.

Le choix est donc binaire : garder le nonce sans unsafe-eval, ou avoir unsafe-eval sans nonce. Il n'y a pas de troisième voie par l'API actuelle.

Ce que nous avons fait, et qui ne nous plaît qu'à moitié

const enDeveloppement = process.env.NODE_ENV !== 'production'

const { proxy } = creerProxy({
  ...(enDeveloppement
    ? { csp: { 'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"] } }
    : {}),
  protectedPrefixes: ['/projets'],
  // …
})

Strictement borné au développement : en production, la directive du socle s'applique telle quelle, nonce compris. Le commentaire du fichier dit quoi relire en premier si quelqu'un touche à ce bloc.

Ça marche, mais on perd le nonce et strict-dynamic en développement — c'est-à-dire qu'on développe sous une posture de sécurité différente de celle qu'on déploie. C'est précisément ce qu'on veut éviter : un écart entre les deux environnements finit toujours par cacher quelque chose.

Ce que je proposerais

Par ordre de préférence :

  1. Une fusion additive plutôt qu'un écrasement. Par exemple, faire que csp ajoute aux valeurs de base, avec une clé explicite pour remplacer quand c'est voulu :

    csp: { 'script-src': ["'unsafe-eval'"] }          // ajoute, nonce conservé
    cspRemplace: { 'script-src': ["'self'"] }         // écrase, comme aujourd'hui
    

    C'est la forme qui rend le cas courant sûr par défaut.

  2. Ou un réglage dédié, puisque le besoin est étroit et universel : devUnsafeEval?: boolean, que le proxy n'honore que si NODE_ENV !== 'production'. Le socle garde alors la main sur la condition, plutôt que de la confier à chaque satellite — qui peut l'oublier, et servir unsafe-eval en production sans que rien ne le dise.

  3. À défaut, le documenter : que csp écrase, que script-src porte un nonce par requête, et que le seul contournement connu sacrifie le nonce. Aujourd'hui le commentaire de l'option dit « Directives CSP ajoutées ou remplacées, par nom », ce qui laisse croire que l'ajout est possible.

La deuxième voie a ma préférence : c'est le socle qui doit porter la condition NODE_ENV, pas trente satellites.

Versions

  • @eqs/platform-next : 0.8.1
  • Next 16.3.4 (Turbopack), React 19.2.8, Node v24.15.0, Windows 11 Pro 26200

Trouvé en ouvrant l'application dans un navigateur pour la première fois.

React a besoin d'`eval()` en développement. Pour l'autoriser, il faut toucher à `script-src` — la seule directive qui porte le nonce fabriqué **par requête**, donc la seule qu'on ne peut pas réécrire depuis la configuration. ## Le symptôme Dès qu'on ouvre une page servie par `next dev` : > `eval() is not supported in this environment. If this page was served with a Content-Security-Policy header, make sure that 'unsafe-eval' is included. React requires eval() in development mode for various debugging features like reconstructing callstacks from a different environment.` La console se remplit, et les outils de mise au point ne fonctionnent qu'à moitié (piles d'appel incomplètes). ## Pourquoi on ne peut pas simplement l'ajouter `directivesCsp` (`@eqs/platform-next/dist/server/proxy.js`) fusionne par écrasement : ```js const base = { 'script-src': ["'self'", `'nonce-${n}'`, "'strict-dynamic'"], // … } const tout = { ...base, ...(o.csp ?? {}) } ``` Fournir `csp: { 'script-src': [...] }` **remplace** donc la directive entière. Or `n`, le nonce, est produit à l'intérieur du proxy, à chaque requête : il n'existe pas au point où l'on écrit la configuration. Impossible de le reconduire. Le choix est donc binaire : garder le nonce **sans** `unsafe-eval`, ou avoir `unsafe-eval` **sans** nonce. Il n'y a pas de troisième voie par l'API actuelle. ## Ce que nous avons fait, et qui ne nous plaît qu'à moitié ```ts const enDeveloppement = process.env.NODE_ENV !== 'production' const { proxy } = creerProxy({ ...(enDeveloppement ? { csp: { 'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"] } } : {}), protectedPrefixes: ['/projets'], // … }) ``` Strictement borné au développement : en production, la directive du socle s'applique telle quelle, nonce compris. Le commentaire du fichier dit quoi relire en premier si quelqu'un touche à ce bloc. Ça marche, mais **on perd le nonce et `strict-dynamic` en développement** — c'est-à-dire qu'on développe sous une posture de sécurité différente de celle qu'on déploie. C'est précisément ce qu'on veut éviter : un écart entre les deux environnements finit toujours par cacher quelque chose. ## Ce que je proposerais Par ordre de préférence : 1. **Une fusion additive plutôt qu'un écrasement.** Par exemple, faire que `csp` ajoute aux valeurs de base, avec une clé explicite pour remplacer quand c'est voulu : ```ts csp: { 'script-src': ["'unsafe-eval'"] } // ajoute, nonce conservé cspRemplace: { 'script-src': ["'self'"] } // écrase, comme aujourd'hui ``` C'est la forme qui rend le cas courant sûr par défaut. 2. **Ou un réglage dédié**, puisque le besoin est étroit et universel : `devUnsafeEval?: boolean`, que le proxy n'honore que si `NODE_ENV !== 'production'`. Le socle garde alors la main sur la condition, plutôt que de la confier à chaque satellite — qui peut l'oublier, et servir `unsafe-eval` en production sans que rien ne le dise. 3. À défaut, **le documenter** : que `csp` écrase, que `script-src` porte un nonce par requête, et que le seul contournement connu sacrifie le nonce. Aujourd'hui le commentaire de l'option dit « Directives CSP ajoutées ou remplacées, par nom », ce qui laisse croire que l'ajout est possible. La deuxième voie a ma préférence : c'est le socle qui doit porter la condition `NODE_ENV`, pas trente satellites. ## Versions - `@eqs/platform-next` : 0.8.1 - Next 16.3.4 (Turbopack), React 19.2.8, Node v24.15.0, Windows 11 Pro 26200 Trouvé en ouvrant l'application dans un navigateur pour la première fois.
Author
Owner

Vrai, et j'ai pris vos options 1 et 2 — elles ne règlent pas le même problème.

Votre phrase centrale est celle qui décide : « on développe sous une posture de sécurité différente de celle qu'on déploie ». C'est ça qu'il fallait supprimer, pas seulement rendre unsafe-eval atteignable.

Option 1 : csp ajoute

const tout = { ...base, ...(o.csp ?? {}) }   // avant : écrase

Le commentaire de l'option annonçait « ajoutées ou remplacées » depuis le début. C'est maintenant vrai — vous aviez raison de relever que la documentation laissait croire l'ajout possible.

cspRemplace garde l'ancien comportement, pour le cas où l'ajout ne suffit pas, et le prix est écrit dans l'option plutôt que découvert : sur script-src, remplacer fait perdre le nonce. Précédence : base → csp → devUnsafeEval → cspRemplace. L'échappatoire gagne sur tout.

Les valeurs sont dédoublonnées, donc ajouter 'self' ne le met pas deux fois.

Option 2 : devUnsafeEval

creerProxy({ devUnsafeEval: true, protectedPrefixes: ['/projets'] })

Votre argument, que je reprends tel quel : c'est le socle qui doit porter la condition NODE_ENV, pas trente satellites — dont un l'oubliera et servira 'unsafe-eval' en production sans que rien ne le dise.

NODE_ENV est lu au moment de la requête, pas à la construction du proxy : le même binaire Edge sert en développement et en production, et c'est l'environnement d'exécution qui tranche.

Un cas éprouve exactement ça, et c'est le plus important des sept :

ne fait RIEN en production, même demandé

Ce que ça change pour vous

Votre contournement devient inutile, et vous récupérez ce que vous aviez perdu :

const { proxy } = creerProxy({
  devUnsafeEval: true,        // nonce et strict-dynamic conservés
  protectedPrefixes: ['/projets'],
})

Plus de process.env.NODE_ENV dans votre proxy.ts, plus de bloc conditionnel à relire, et le nonce reste en développement.

Un point d'honnêteté : 'unsafe-inline', que vous ajoutiez aussi, n'est pas repris par devUnsafeEval. Il n'est pas nécessaire à React en développement, et il est ignoré par les navigateurs dès que 'strict-dynamic' est présent — le remettre serait décoratif. Si vous en avez besoin pour autre chose, csp: { 'script-src': ["'unsafe-inline'"] } l'ajoute maintenant sans rien perdre.

Ce que je n'ai pas fait

Votre option 3 (documenter) devient sans objet pour l'ajout, mais la documentation du remplacement compte toujours : elle est dans le commentaire de cspRemplace, avec sa conséquence.

Je n'ai pas touché aux autres directives : style-src garde 'unsafe-inline', qui est un autre sujet et mérite sa propre mesure.

Branche chasse/knip-et-code-mort. Ticket laissé ouvert jusqu'à la publication.

Vrai, et j'ai pris vos options 1 **et** 2 — elles ne règlent pas le même problème. Votre phrase centrale est celle qui décide : « on développe sous une posture de sécurité différente de celle qu'on déploie ». C'est ça qu'il fallait supprimer, pas seulement rendre `unsafe-eval` atteignable. ## Option 1 : `csp` ajoute ```ts const tout = { ...base, ...(o.csp ?? {}) } // avant : écrase ``` Le commentaire de l'option annonçait « ajoutées ou remplacées » depuis le début. C'est maintenant vrai — vous aviez raison de relever que la documentation laissait croire l'ajout possible. `cspRemplace` garde l'ancien comportement, pour le cas où l'ajout ne suffit pas, et le prix est écrit dans l'option plutôt que découvert : sur `script-src`, remplacer fait perdre le nonce. Précédence : base → `csp` → `devUnsafeEval` → `cspRemplace`. L'échappatoire gagne sur tout. Les valeurs sont dédoublonnées, donc ajouter `'self'` ne le met pas deux fois. ## Option 2 : `devUnsafeEval` ```ts creerProxy({ devUnsafeEval: true, protectedPrefixes: ['/projets'] }) ``` Votre argument, que je reprends tel quel : c'est le socle qui doit porter la condition `NODE_ENV`, pas trente satellites — dont un l'oubliera et servira `'unsafe-eval'` en production sans que rien ne le dise. `NODE_ENV` est lu **au moment de la requête**, pas à la construction du proxy : le même binaire Edge sert en développement et en production, et c'est l'environnement d'exécution qui tranche. Un cas éprouve exactement ça, et c'est le plus important des sept : > **ne fait RIEN en production, même demandé** ## Ce que ça change pour vous Votre contournement devient inutile, et vous récupérez ce que vous aviez perdu : ```ts const { proxy } = creerProxy({ devUnsafeEval: true, // nonce et strict-dynamic conservés protectedPrefixes: ['/projets'], }) ``` Plus de `process.env.NODE_ENV` dans votre `proxy.ts`, plus de bloc conditionnel à relire, et le nonce reste en développement. Un point d'honnêteté : `'unsafe-inline'`, que vous ajoutiez aussi, n'est pas repris par `devUnsafeEval`. Il n'est pas nécessaire à React en développement, et il est ignoré par les navigateurs dès que `'strict-dynamic'` est présent — le remettre serait décoratif. Si vous en avez besoin pour autre chose, `csp: { 'script-src': ["'unsafe-inline'"] }` l'ajoute maintenant sans rien perdre. ## Ce que je n'ai pas fait Votre option 3 (documenter) devient sans objet pour l'ajout, mais la documentation du **remplacement** compte toujours : elle est dans le commentaire de `cspRemplace`, avec sa conséquence. Je n'ai pas touché aux autres directives : `style-src` garde `'unsafe-inline'`, qui est un autre sujet et mérite sa propre mesure. Branche `chasse/knip-et-code-mort`. Ticket laissé ouvert jusqu'à la publication.
nicolas-fryder referenced this issue from a commit 2026-09-12 14:30:20 +00:00
Author
Owner

Corrigé. creerProxy sait étendre une directive CSP au lieu de l'écraser : packages/platform-next/src/server/proxy.ts porte l'option devUnsafeEval et l'aide ajouter('script-src', …). On peut donc ajouter unsafe-eval en développement sans perdre le nonce.

Fermé après vérification contre le code de dev le 2026-09-21. Ce ticket restait ouvert faute d'un Closes #55 dans le commit, pas faute de correctif.

Corrigé. `creerProxy` sait **étendre** une directive CSP au lieu de l'écraser : `packages/platform-next/src/server/proxy.ts` porte l'option `devUnsafeEval` et l'aide `ajouter('script-src', …)`. On peut donc ajouter `unsafe-eval` en développement sans perdre le nonce. Fermé après vérification contre le code de `dev` le 2026-09-21. Ce ticket restait ouvert faute d'un `Closes #55` dans le commit, pas faute de correctif.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Infrastructure/eqs-platform#55
No description provided.