oRPC: Stored XSS in OpenAPI Reference Plugin via unescaped JSON.stringify
Description
oRPC is an tool that helps build APIs that are end-to-end type-safe and adhere to OpenAPI standards. Prior to version 1.13.9, a stored cross-site scripting (XSS) vulnerability exists in the OpenAPI documentation generation of orpc. If an attacker can control any field within the OpenAPI specification (such as info.description), they can break out of the JSON context and execute arbitrary JavaScript when a user views the generated API documentation. This issue has been patched in version 1.13.9.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A stored XSS vulnerability in oRPC's OpenAPI documentation generation allows attackers to inject arbitrary JavaScript via unescaped JSON fields.
Vulnerability
Overview
A stored cross-site scripting (XSS) vulnerability exists in the OpenAPI documentation generation of oRPC, a tool for building type-safe APIs that are end-to-end type-safe and adhere to OpenAPI standards [1]. The vulnerability affects versions prior to 1.13.9. The root cause is that the renderDocsHtml() function in the OpenAPI reference plugin embeds the OpenAPI specification object directly into an HTML template using JSON.stringify() without escaping HTML characters [4]. Specifically, the code uses a template literal: `. Since JSON.stringify() does not escape characters like < or >`, an attacker can inject a malicious string that breaks out of the JSON context [4].
Exploitation
An attacker who can control any field within the OpenAPI specification that is user-controlled (such as info.description) can inject a payload like ` [4]. When a user views the generated API documentation page, the browser parses the HTML, prematurely closes the application/json` script block, and executes the attacker's JavaScript [4]. The attack requires the ability to control part of the OpenAPI spec, which could occur if the application dynamically generates specs based on user input or if a malicious developer modifies the spec [4].
Impact
Successful exploitation leads to stored XSS, allowing arbitrary JavaScript execution in the context of the user viewing the documentation [1][4]. This could result in session hijacking, unauthorized API calls, or other malicious actions performed on behalf of the victim [4]. The impact is considered high because the script executes when an administrator or developer views the API docs [4].
Mitigation
The vulnerability has been patched in oRPC version 1.13.9 [1]. The fix introduces a new function escapeJsonForHtml() that uses Unicode escapes (e.g., \u003C for <) instead of HTML entities, ensuring that JSON data embedded in `` tags is safely serialized without breaking the HTML context [3]. Users should upgrade to version 1.13.9 or later. No workarounds are mentioned in the advisory [4].
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@orpc/openapinpm | < 1.13.9 | 1.13.9 |
Affected products
2Patches
11 file changed · +38 −10
packages/openapi/src/plugins/openapi-reference.ts+38 −10 modified@@ -49,6 +49,8 @@ export interface OpenAPIReferencePluginOptions<T extends Context> extends OpenAP /** * HTML to inject into the <head> of the docs page. * + * @warning This is not escaped special characters, so must be used with caution to avoid XSS vulnerabilities. + * * @default '' */ docsHead?: Value<Promisable<string>, [StandardHandlerInterceptorOptions<T>]> @@ -121,7 +123,25 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle this.specPath = options.specPath ?? '/spec.json' this.generator = new OpenAPIGenerator(options) - const esc = (s: string) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>') + /** Escapes a string for safe embedding in an HTML attribute value. */ + const escapeHtmlEntities = (s: string) => s + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>') + + /** + * Serialises a value to JSON safe for HTML embedding (attribute or <script>). + * Uses Unicode escapes instead of HTML entities so JSON.parse reconstructs + * the original values without corruption. Cannot be merged with `esc` — + * HTML entities inside <script> are not decoded by the JS engine. + */ + const escapeJsonForHtml = (obj: object) => stringifyJSON(obj) + .replace(/&/g, '\\u0026') + .replace(/'/g, '\\u0027') + .replace(/</g, '\\u003C') + .replace(/>/g, '\\u003E') + .replace(/\//g, '\\u002F') this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec, docsProvider, cssUrl) => { let body: string @@ -145,11 +165,15 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle <body> <div id="app"></div> - <script src="${esc(scriptUrl)}"></script> + <script src="${escapeHtmlEntities(scriptUrl)}"></script> + <!-- IMPORTANT: assign to a variable first to prevent ), ( in values breaking the call expression. --> + <!-- IMPORTANT: escapeJsonForHtml ensures <, > cannot terminate the </script> tag prematurely. --> <script> + const swaggerConfig = ${escapeJsonForHtml(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, '$1')} + window.onload = () => { - window.ui = SwaggerUIBundle(${stringifyJSON(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, '$1')}) + window.ui = SwaggerUIBundle(swaggerConfig) } </script> </body> @@ -163,12 +187,16 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle body = ` <body> - <div id="app" data-config="${esc(stringifyJSON(scalarConfig))}"></div> - - <script src="${esc(scriptUrl)}"></script> - + <div id="app"></div> + + <script src="${escapeHtmlEntities(scriptUrl)}"></script> + + <!-- IMPORTANT: assign to a variable first to prevent ), ( in values breaking the call expression. --> + <!-- IMPORTANT: escapeJsonForHtml ensures <, > cannot terminate the </script> tag prematurely. --> <script> - Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config)) + const scalarConfig = ${escapeJsonForHtml(scalarConfig)} + + Scalar.createApiReference('#app', scalarConfig) </script> </body> ` @@ -180,8 +208,8 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <title>${esc(title)}</title> - ${cssUrl ? `<link rel="stylesheet" type="text/css" href="${esc(cssUrl)}" />` : ''} + <title>${escapeHtmlEntities(title)}</title> + ${cssUrl ? `<link rel="stylesheet" type="text/css" href="${escapeHtmlEntities(cssUrl)}" />` : ''} ${head} </head> ${body}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-7f6v-3gx7-27q8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33331ghsaADVISORY
- github.com/middleapi/orpc/commit/4f0efa8a1d3fa8e8317a4b03cc3945a5dfd68addghsax_refsource_MISCWEB
- github.com/middleapi/orpc/releases/tag/v1.13.9ghsax_refsource_MISCWEB
- github.com/middleapi/orpc/security/advisories/GHSA-7f6v-3gx7-27q8ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.