fix(security): échapper < avant injection JSON dans un <script> (#SEC-001) - #85
Merged
Conversation
…#SEC-001) Deux emplacements sérialisaient du contenu avec `JSON.stringify` puis l'injectaient via `set:html` dans un `<script>` — le JSON-LD de `BareLayout` et les données de `SeriesLightbox`. `JSON.stringify` ne touche ni `<` ni `/` : une valeur portant `</script>` fermait la balise, et la suite devenait du HTML actif. Le vecteur large est le JSON-LD : il sérialise `title`, champ obligatoire de toute série, et `BareLayout` est le layout servi par défaut quand le site n'en fournit pas. Le modèle de menace d'un SSG — l'auteur écrit son propre contenu — ne suffit pas à classer cela bénin : `images.json` (§1.5.1) est produit par des outils, et un texte alternatif peut arriver d'un champ IPTC sans relecture humaine. `<` est un échappement JSON valide : `JSON.parse` restitue le `<`, la donnée est intacte, seule la séquence littérale disparaît. Aucun changement d'API, aucun contenu rejeté. Le `.replace` est dupliqué à dessein. Le factoriser demanderait un module `.ts` de plus, donc une entry tsup de plus — le mode de panne exact qui a livré quatre composants non importables en v0.8.0 et deux vocabulaires manquants en 0.17.1. Une ligne dupliquée coûte moins qu'une surface de packaging. Les tests portent sur le HTML réellement produit : c'est le seul niveau où le bug existait, un test unitaire ne vérifierait que la ligne de correctif. Deux fixtures les alimentent — la lightbox n'étant rendue par aucune route, le second site serait resté hors de portée sans une page dédiée. 271 tests verts (215 unitaires + 56 e2e), dont 5 nouveaux. Validés par mutation : neutraliser les deux `.replace` fait tomber ces 5 tests, et eux seuls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AgEyJQRrNvugcuu9WsujGy
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Trouvé par l'audit
blackemperordu 2026-08-24 — rapport complet dansdocs/reports/audit-summary-2026-08-24.md.Le défaut
Deux emplacements sérialisaient du contenu avec
JSON.stringifypuis l'injectaient viaset:htmldans un<script>.JSON.stringifyne touche ni<ni/: une valeur portant</script>fermait la balise, et tout ce qui suivait devenait du HTML actif.src/layouts/BareLayout.astrotitle,description(JSON-LD)src/components/SeriesLightbox.astroimages[].altLe premier est le plus large :
titleest obligatoire sur chaque série, etBareLayoutest le layout servi par défaut quand le site n'en fournit pas.Confirmé par
astro buildréel avant correction, pas déduit. La mutation ci-dessous rejoue cette preuve à chaque exécution de la suite.Pourquoi ce n'est pas du simple auto-XSS
L'objection normale — « le contenu d'un SSG est écrit par l'auteur du site » — ne couvre pas les deux cas suivants :
images.json(§1.5.1) est produit par des outils.hyperfocale-exporterlit les métadonnées des fichiers,hyperfocale-cmsécrit le frontmatter : un texte alternatif peut venir d'un champ IPTC sans jamais passer sous les yeux d'un humain.Le correctif
.replace(/</g, '\u003c')sur les deux sites.\u003cest un échappement JSON valide :JSON.parserestitue le<d'origine, la donnée est intacte, seule la séquence littérale ne peut plus apparaître. Aucun changement d'API, aucun contenu rejeté.Le
.replaceest dupliqué à dessein. Le factoriser demanderait un module.tsde plus, donc une entrytsupde plus — le mode de panne exact qui a livré quatre composants non importables en v0.8.0 et deux vocabulaires manquants en 0.17.1. Une ligne dupliquée coûte moins cher qu'une surface de packaging supplémentaire.Les tests
Cinq tests e2e, portant sur le HTML réellement produit : c'est le seul niveau où le bug existait — un test unitaire ne vérifierait que la ligne de correctif.
Deux fixtures les alimentent.
SeriesLightboxn'étant rendu par aucune route du plugin, le second site serait resté hors de portée sans une page dédiée dans le demo-site.Validés par mutation — neutraliser les deux
.replacefait tomber ces 5 tests, et eux seuls (51 autres restent verts).Vérification
npm run typecheck— 0 erreurnpm test— 271 tests verts (215 unitaires + 56 e2e), dont 5 nouveauxReste ouvert
#SEC-002— encodage deembeds[].iddans les URL de lecture (P1, sans lien de cause avec celui-ci).🤖 Generated with Claude Code
https://claude.ai/code/session_01AgEyJQRrNvugcuu9WsujGy