
Laboratorio local que reproduce XSS almacenado en la generación de documentación OpenAPI de oRPC (CVE-2026-33331), con versiones vulnerable y parcheada para comparación y un PoC independiente.
Laboratorio de prueba de concepto para XSS almacenado en la generación de documentación OpenAPI de orpc
oRPC construye páginas de documentación de API a partir de una especificación OpenAPI. Antes de la v1.13.9, volcaba la especificación directamente en una etiqueta <script> usando JSON.stringify() sin codificación HTML. Cualquier campo controlado por un atacante, como info.description, podía cerrar esa etiqueta antes de tiempo y ejecutar script.
Este repositorio te ofrece un laboratorio local para reproducir el error, además de una versión parcheada para comparar.
| Campo | Valor |
|---|---|
| ID CVE | CVE-2026-33331 |
| GHSA | GHSA-7f6v-3gx7-27q8 |
| CWE | CWE-79 (Neutralización incorrecta de la entrada durante la generación de páginas web) |
| Producto | middleapi/orpc |
| Afectado | Todas las versiones anteriores a la v1.13.9 |
| Parcheado | v1.13.9 |
| Descubrimiento | Abhay Kumar (@abhayclasher) |
El código vulnerable en packages/openapi/src/plugins/openapi-reference.ts renderiza la especificación de esta manera:
<script id="spec" type="application/json">${JSON.stringify(spec)}</script>
JSON.stringify() produce JSON válido, pero no escapa < ni >. El navegador sigue tratando </script> como una etiqueta de cierre HTML, incluso dentro de una etiqueta <script type="application/json">.
Un payload en info.description que desencadena la ejecución:
"</script><script>alert('XSS')</script>"
El navegador ve:
<script id="spec" type="application/json">{"info":{"description":"
</script><script>alert('XSS')</script>"}}
</script>
El primer </script> cierra el bloque. Todo lo que viene después se ejecuta como JavaScript en línea.
Esto se aplica cuando una aplicación construye especificaciones OpenAPI a partir de entradas controladas por el usuario, que es exactamente el caso que oRPC habilita a través de sus definiciones de router.
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
Después de que el servidor se inicie, visita estas URLs:
| URL | Descripción |
|---|---|
http://localhost:3000/docs | Página de documentación vulnerable: el payload XSS se ejecuta al cargar |
http://localhost:3000/docs/safe | La misma página con el parche v1.13.9 aplicado |
http://localhost:3000/spec.json | Especificación OpenAPI JSON sin procesar |
La versión vulnerable disparará un alert() al cargar la página, confirmando la ejecución del script. La versión parcheada renderiza la misma especificación maliciosa de forma segura: el payload se muestra como texto plano.
También puedes ejecutar el PoC independiente:
cd poc
node exploit.js
Esto inicia un servidor mínimo en el puerto 3000 que demuestra el mismo error sin el contexto completo de la aplicación.
1. El atacante controla un campo en la especificación OpenAPI (p. ej., description)
2. oRPC genera el HTML de la documentación con JSON.stringify(spec)
3. El </script> del payload cierra la etiqueta <script> antes de tiempo
4. El navegador ejecuta lo que sigue como JavaScript en línea
┌─────────────────────────────────────────────────────┐
│ 1. Especificación maliciosa creada: │
│ │
│ info.description = │
│ "</script><script>alert('XSS')</script>" │
│ │
│ 2. orpc incrusta la especificación en HTML: │
│ │
│ <script id="spec" type="application/json"> │
│ {"info":{"description":"</script> <-- etiqueta │
│ <script>alert('XSS')</script> <-- payload │
│ "}} │
│ </script> │
│ │
│ 3. El navegador ejecuta alert('XSS') │
└─────────────────────────────────────────────────────┘
La versión 1.13.9 introduce escapeJsonForHtml(), que reemplaza los caracteres sensibles a HTML con escapes Unicode antes de incrustar el JSON en la etiqueta script:
const escapeJsonForHtml = (obj) => JSON.stringify(obj)
.replace(/&/g, '\\u0026')
.replace(/'/g, '\\u0027')
.replace(/</g, '\\u003C')
.replace(/>/g, '\\u003E')
.replace(/\//g, '\\u002F');
Los escapes Unicode funcionan porque siguen siendo JSON válido: JSON.parse() reconstruye la cadena original, pero nunca parecen HTML al analizador del navegador.
El parche también pasó de leer el JSON de un atributo dataset.config del DOM (que se rompía cuando los valores contenían paréntesis) a asignar el JSON escapado directamente a una variable en línea.
Consulta el commit completo: 4f0efa8
CVE-2026-33331/
├── README.md # Este archivo
├── docker-compose.yml # Configuración del laboratorio Docker
├── app/
│ ├── Dockerfile # Configuración de construcción del contenedor
│ ├── package.json # Dependencias de Node.js
│ └── server.js # Renderizador de documentación vulnerable + parcheado
└── poc/
└── exploit.js # Prueba de concepto mínima independiente