
Laboratoire local reproduisant une XSS stockée dans la génération de documentation OpenAPI d'oRPC (CVE-2026-33331), avec les versions vulnérable et corrigée à des fins de comparaison ainsi qu'un PoC autonome.
Laboratoire de preuve de concept pour le XSS stocké dans la génération de documentation OpenAPI d'orpc
oRPC construit les pages de documentation API à partir d'une spécification OpenAPI. Avant la v1.13.9, il injectait directement la spécification dans une balise <script> en utilisant JSON.stringify() sans encodage HTML. Tout champ contrôlé par un attaquant, comme , pouvait fermer cette balise prématurément et exécuter du script.
info.descriptionCe dépôt vous fournit un laboratoire local pour reproduire le bug, ainsi qu'une version corrigée pour comparer.
| Champ | Valeur |
|---|---|
| ID CVE | CVE-2026-33331 |
| GHSA | GHSA-7f6v-3gx7-27q8 |
| CWE | CWE-79 (Neutralisation incorrecte de l'entrée lors de la génération de pages Web) |
| Produit | middleapi/orpc |
| Versions concernées | Toutes les versions antérieures à la v1.13.9 |
| Version corrigée | v1.13.9 |
| Découverte | Abhay Kumar (@abhayclasher) |
Le code vulnérable dans packages/openapi/src/plugins/openapi-reference.ts rend la spécification comme ceci :
<script id="spec" type="application/json">${JSON.stringify(spec)}</script>
JSON.stringify() produit du JSON valide, mais il n'échappe pas les caractères < ou >. Le navigateur traite toujours </script> comme une balise de fermeture HTML, même à l'intérieur d'un <script type="application/json">.
Une charge utile dans info.description qui déclenche l'exécution :
"</script><script>alert('XSS')</script>"
Le navigateur voit :
<script id="spec" type="application/json">{"info":{"description":"
</script><script>alert('XSS')</script>"}}
</script>
Le premier </script> ferme le bloc. Tout ce qui suit s'exécute comme JavaScript en ligne.
Cela s'applique lorsqu'une application construit des spécifications OpenAPI à partir d'entrées contrôlées par l'utilisateur, ce qui est exactement le cas que oRPC permet via ses définitions de routeurs.
git clone https://github.com/abhayclasher/CVE-2026-33331.git
cd CVE-2026-33331/app
npm install
node server.js
git clone https://github.com/abhayclasher/CVE-2026-33331.git
cd CVE-2026-33331
docker compose up -d
Une fois le serveur démarré, visitez ces URL :
| URL | Description |
|---|---|
http://localhost:3000/docs | Page de documentation vulnérable — la charge utile XSS s'exécute au chargement |
http://localhost:3000/docs/safe | Même page avec le correctif v1.13.9 appliqué |
http://localhost:3000/spec.json | Spécification OpenAPI brute en JSON |
La version vulnérable déclenchera un alert() au chargement de la page, confirmant l'exécution de script. La version corrigée rend la même spécification malveillante en toute sécurité — la charge utile s'affiche comme texte brut.
Vous pouvez également exécuter la preuve de concept autonome :
cd poc
node exploit.js
Cela démarre un serveur minimal sur le port 3000 qui démontre le même bug sans le contexte complet de l'application.
1. L'attaquant contrôle un champ dans la spécification OpenAPI (ex. description)
2. oRPC génère le HTML de la documentation avec JSON.stringify(spec)
3. Le </script> dans la charge utile ferme la balise <script> prématurément
4. Le navigateur exécute ce qui suit comme JavaScript en ligne
┌─────────────────────────────────────────────────────┐
│ 1. Spécification malveillante créée : │
│ │
│ info.description = │
│ "</script><script>alert('XSS')</script>" │
│ │
│ 2. orpc intègre la spécification dans le HTML : │
│ │
│ <script id="spec" type="application/json"> │
│ {"info":{"description":"</script> <-- balise │
│ <script>alert('XSS')</script> <-- charge │
│ "}} │
│ </script> │
│ │
│ 3. Le navigateur exécute alert('XSS') │
└─────────────────────────────────────────────────────┘
La version 1.13.9 introduit escapeJsonForHtml(), qui remplace les caractères sensibles au HTML par des échappements Unicode avant d'intégrer le JSON dans la balise script :
const escapeJsonForHtml = (obj) => JSON.stringify(obj)
.replace(/&/g, '\\u0026')
.replace(/'/g, '\\u0027')
.replace(/</g, '\\u003C')
.replace(/>/g, '\\u003E')
.replace(/\//g, '\\u002F');
Les échappements Unicode fonctionnent car ils restent du JSON valide — JSON.parse() reconstruit la chaîne d'origine — mais ils ne ressemblent jamais à du HTML pour l'analyseur du navigateur.
Le correctif a également déplacé la lecture du JSON d'un attribut dataset.config du DOM (qui échouait lorsque les valeurs contenaient des parenthèses) vers l'assignation directe du JSON échappé à une variable en ligne.
Consultez le commit complet : 4f0efa8
CVE-2026-33331/
├── README.md # Ce fichier
├── docker-compose.yml # Configuration du laboratoire Docker
├── app/
│ ├── Dockerfile # Configuration de construction du conteneur
│ ├── package.json # Dépendances Node.js
│ └── server.js # Rendu de documentation vulnérable + corrigé
└── poc/
└── exploit.js # Preuve de concept minimale autonome
Signalé par Abhay Kumar. Ce dépôt est destiné à un usage éducatif local uniquement. Ne déployez pas le serveur vulnérable sur un réseau public.
À des fins éducatives uniquement — à utiliser dans des environnements isolés