Tous les articles

Comment les endpoints JSONP contournent votre CSP

CentralCSP Team ·

Dernière mise à jour:

Une politique de sécurité du contenu (CSP) qui allowliste un host tiers peut se retourner contre vous. Si ce host sert un endpoint JSONP, un attaquant capable d'injecter une balise <script> dans votre page peut exécuter du JavaScript arbitraire, et le navigateur l'autorise parce que le host est sur votre liste. La politique que vous avez ajoutée pour stopper le cross-site scripting (XSS) finit par laisser passer l'attaque.

La version courte : les allowlists de hosts et de schémas sont la partie faible de la CSP. Le correctif est d'arrêter de faire confiance aux hosts pour les scripts et de faire confiance aux nonces à la place, avec 'nonce-...' plus 'strict-dynamic'. Le reste de cet article montre comment fonctionne JSONP, exactement comment le contournement s'exécute, et comment abandonner les allowlists sans casser votre site.

Ce header vous est nouveau ? Démarrer avec la Content Security Policy couvre les bases d'abord.

Qu'est-ce que JSONP ?

JSONP (« JSON with padding ») est une ancienne façon de récupérer des données entre origines. Elle fonctionne parce que l'élément HTML <script> n'est pas soumis à la same-origin policy : un src de script peut pointer vers n'importe quel domaine, et le navigateur exécute le JavaScript qui revient.

Le client et le serveur s'accordent sur un paramètre de requête, conventionnellement callback ou jsonp. Le serveur prend cette valeur et la préfixe à sa réponse, enveloppant la charge JSON dans un appel de fonction pour que toute la réponse soit un programme JavaScript valide. La page ajoute alors dynamiquement un <script> dont le src est l'endpoint plus le nom de callback :

// The client adds a script that points at a cross-origin endpoint
const script = document.createElement('script');
script.src = 'https://cdn.example.com/api?callback=handleData';
document.body.appendChild(script);

// The function the server will call back into
function handleData(data) {
  console.log(data.name);
}

Le serveur répond avec le nom de callback enveloppé autour des données, et le navigateur l'exécute :

handleData({"name": "John"})

Cette requête et cette réponse ressemblent à ceci :

GET https://cdn.example.com/api?callback=handleData

handleData({"name":"John"})

Les données arrivent en exécutant une fonction que le serveur nomme pour vous. C'est ce détail qui rend JSONP dangereux.

Comment un endpoint JSONP contourne la CSP

Imaginez une politique typique. Vous allowlistez votre propre origine plus un CDN de confiance :

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

La directive script-src dit que les scripts peuvent se charger depuis votre origine et depuis cdn.example.com. Cela semble raisonnable. Le navigateur bloquera un script de tout autre host.

Supposons maintenant que cdn.example.com expose un endpoint JSONP, et qu'un attaquant a un pied dans la porte via une injection HTML sur votre page (un point d'entrée XSS réfléchi ou stocké qui n'a pas encore mené à une exécution de script à cause de votre politique). L'attaquant injecte une balise de script qui pointe vers le host allowlisté :

<script src="https://cdn.example.com/api?callback=alert(document.cookie)//"></script>

L'endpoint reflète la valeur de callback au début de son corps de réponse, généralement sans la nettoyer, donc la réponse devient du JavaScript contrôlé par l'attaquant servi depuis un host que vous faites confiance :

alert(document.cookie)//({"name":"John"})

Le // final commente le reste de la ligne pour que la charge restante ne provoque pas d'erreur de syntaxe. Le navigateur l'exécute. La CSP ne s'y oppose jamais, parce que la requête venait d'un host allowlisté. La politique est satisfaite et l'attaque réussit.

L'OWASP formule le test directement : quand une politique allowliste des domaines tiers comme des CDN, vérifiez si ces domaines exposent des endpoints JSONP ou du contenu contrôlé par l'utilisateur, car un attaquant peut utiliser l'injection de callback pour exécuter du JavaScript arbitraire tout en respectant la politique.

Une réserve mérite d'être dite clairement. Beaucoup d'endpoints JSONP restreignent le callback à des caractères valides d'identifiant JavaScript, ce qui bloque les charges les plus simples comme celle ci-dessus. Le vrai risque, ce sont les endpoints avec une validation de callback insuffisante, pas tout endpoint JSONP. L'exemple ici est illustratif ; la charge exacte qui défait un filtre donné dépend de l'endpoint.

Pourquoi les allowlists de hosts sont la cause racine

Ce n'est pas une bizarrerie d'un seul CDN. C'est la faiblesse structurelle des politiques basées sur des allowlists. Si vous faites confiance à un host, vous faites confiance à tout ce que ce host sert, y compris des endpoints que vous ne contrôlez pas et n'avez pas audités. Un gestionnaire de tags de confiance est le même genre de problème vu de l'autre côté ; voyez comment les attaquants détournent Google Tag Manager.

L'étude « CSP Is Dead, Long Live CSP! » (Weichselbaum, Spagnuolo, Lekies et Janc, de Google, présentée à l'ACM CCS 2016) a mesuré cela sur de vraies politiques. Elle a trouvé que 94.72% des politiques distinctes étaient contournables, que 75.81% utilisaient des allowlists de scripts qui laissaient les attaquants contourner la CSP, et que 14 des 15 hosts de scripts les plus couramment allowlistés contenaient des endpoints dangereux, la classe qui inclut JSONP.

La leçon que web.dev en tire : les politiques à allowlist laissent souvent la page exposée à la XSS parce qu'elles peuvent être contournées dans la plupart des configurations. Une politique construite sur des hosts n'est jamais plus forte que l'endpoint le plus faible de chaque host que vous listez.

Le correctif, nonces et strict-dynamic

Arrêtez de faire confiance aux hosts pour les scripts. Faites confiance à un nonce par réponse à la place, et laissez 'strict-dynamic' étendre cette confiance aux scripts que vos scripts de confiance chargent.

Content-Security-Policy:
  script-src 'nonce-r4nd0m' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Vous marquez ensuite vos propres scripts avec le nonce correspondant :

<script nonce="r4nd0m">
  // your trusted code
</script>

Quand 'strict-dynamic' est présent, les navigateurs qui le prennent en charge ignorent 'unsafe-inline', 'self', les listes de sources basées sur des hosts, et les listes de sources basées sur des schémas comme https:. La confiance n'est accordée qu'aux scripts qui portent un nonce ou un hash valide, et aux scripts que ces scripts déjà de confiance créent ensuite. L'allowlist sur laquelle reposait l'endpoint JSONP ne s'applique plus, donc le contournement se ferme.

Cela correspond à l'implémentation. Dans Chromium, dès que 'strict-dynamic' est présent, les entrées de host et de schéma dans la liste de sources sont ignorées quand le navigateur décide si un script peut s'exécuter, donc un host JSONP allowlisté ne compte plus.

Quelques conditions doivent être réunies pour que cela tienne :

  • Le nonce doit être imprévisible, d'au moins 128 bits, en base64, et régénéré à chaque réponse. Un nonce statique ou prévisible est lui-même un contournement.
  • 'strict-dynamic' ne traite que la classe de contournement par allowlist et JSONP. Il repose sur le fait que le nonce reste secret et qu'aucun 'unsafe-inline' ou 'unsafe-eval' n'affaiblisse la politique, ce qui est sans objet ici puisque ces mots-clés sont ignorés quand 'strict-dynamic' est présent.
  • Migrez avec soin. Ajouter le mot-clé peut casser des sites qui chargent des hosts allowlistés via un simple <script src> dans le balisage, à moins qu'un loader de confiance porteur de nonce les fasse venir. Le chemin est « mettez un nonce sur vos propres scripts et laissez 'strict-dynamic' propager la confiance », pas « ajoutez juste le mot-clé ».

'strict-dynamic' fait partie de CSP Level 3 et est largement pris en charge dans les navigateurs actuels. Dans un navigateur qui ne le comprend pas, le nonce s'applique quand même et l'allowlist de hosts s'applique quand même, donc la politique se dégrade au lieu de casser d'un coup. Si vous câblez cela dans un framework, les nonces CSP dans Next.js détaille une configuration concrète.

Préférez CORS à JSONP

Le correctif plus profond est de retirer JSONP là où vous le pouvez. CORS permet le partage de données entre origines sans exécuter de scripts arbitraires. Les navigateurs ont commencé à le livrer vers 2009 et le W3C l'a ratifié en 2014, et c'est le bon outil pour récupérer du JSON entre origines aujourd'hui :

const res = await fetch('https://cdn.example.com/api');
const data = await res.json();

Le serveur donne son accord avec un header de réponse plutôt qu'en remettant au client une fonction à exécuter :

Access-Control-Allow-Origin: https://yourdomain.com

Si un endpoint JSONP doit rester, validez strictement le nom de callback contre une allowlist de caractères d'identifiant pour que du code contrôlé par l'attaquant ne puisse pas atteindre le corps de réponse.

Trouvez votre exposition

Le plus dur en pratique est de savoir quels hosts allowlistés de votre politique en production servent du JSONP ou d'autres endpoints dangereux. Passez votre politique dans l'évaluateur CSP pour voir quelles sources l'affaiblissent et où une allowlist vous laisse exposé, puis déplacez les directives qui comptent vers des nonces et 'strict-dynamic'.

Pour aller plus loin, construire une CSP solide montre comment déployer une politique stricte en Report-Only d'abord, pour observer ce qui casserait avant de l'imposer.

L'allowlist n'est que la moitié de l'exposition. L'autre moitié, c'est ce que ces hosts servent réellement sur vos pages aujourd'hui, ce que CentralCSP reconstitue à partir du CSP hash reporting : un inventaire par site de chaque script qui s'est exécuté, avec son origine, si bien qu'un endpoint JSONP sur un CDN allowlisté apparaît comme un script que vous n'aviez pas prévu de charger plutôt que comme une ligne dans une politique que vous supposiez sûre.

Sources

Articles liés