Skip to content

Commit 4f0efa8

Browse files
authored
Merge commit from fork
1 parent 903c452 commit 4f0efa8

File tree

1 file changed

+38
-10
lines changed

1 file changed

+38
-10
lines changed

packages/openapi/src/plugins/openapi-reference.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export interface OpenAPIReferencePluginOptions<T extends Context> extends OpenAP
4949
/**
5050
* HTML to inject into the <head> of the docs page.
5151
*
52+
* @warning This is not escaped special characters, so must be used with caution to avoid XSS vulnerabilities.
53+
*
5254
* @default ''
5355
*/
5456
docsHead?: Value<Promisable<string>, [StandardHandlerInterceptorOptions<T>]>
@@ -121,7 +123,25 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle
121123
this.specPath = options.specPath ?? '/spec.json'
122124
this.generator = new OpenAPIGenerator(options)
123125

124-
const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
126+
/** Escapes a string for safe embedding in an HTML attribute value. */
127+
const escapeHtmlEntities = (s: string) => s
128+
.replace(/&/g, '&amp;')
129+
.replace(/"/g, '&quot;')
130+
.replace(/</g, '&lt;')
131+
.replace(/>/g, '&gt;')
132+
133+
/**
134+
* Serialises a value to JSON safe for HTML embedding (attribute or <script>).
135+
* Uses Unicode escapes instead of HTML entities so JSON.parse reconstructs
136+
* the original values without corruption. Cannot be merged with `esc` —
137+
* HTML entities inside <script> are not decoded by the JS engine.
138+
*/
139+
const escapeJsonForHtml = (obj: object) => stringifyJSON(obj)
140+
.replace(/&/g, '\\u0026')
141+
.replace(/'/g, '\\u0027')
142+
.replace(/</g, '\\u003C')
143+
.replace(/>/g, '\\u003E')
144+
.replace(/\//g, '\\u002F')
125145

126146
this.renderDocsHtml = options.renderDocsHtml ?? ((specUrl, title, head, scriptUrl, config, spec, docsProvider, cssUrl) => {
127147
let body: string
@@ -145,11 +165,15 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle
145165
<body>
146166
<div id="app"></div>
147167
148-
<script src="${esc(scriptUrl)}"></script>
168+
<script src="${escapeHtmlEntities(scriptUrl)}"></script>
149169
170+
<!-- IMPORTANT: assign to a variable first to prevent ), ( in values breaking the call expression. -->
171+
<!-- IMPORTANT: escapeJsonForHtml ensures <, > cannot terminate the </script> tag prematurely. -->
150172
<script>
173+
const swaggerConfig = ${escapeJsonForHtml(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, '$1')}
174+
151175
window.onload = () => {
152-
window.ui = SwaggerUIBundle(${stringifyJSON(swaggerConfig).replace(/"(SwaggerUIBundle\.[^"]+)"/g, '$1')})
176+
window.ui = SwaggerUIBundle(swaggerConfig)
153177
}
154178
</script>
155179
</body>
@@ -163,12 +187,16 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle
163187

164188
body = `
165189
<body>
166-
<div id="app" data-config="${esc(stringifyJSON(scalarConfig))}"></div>
167-
168-
<script src="${esc(scriptUrl)}"></script>
169-
190+
<div id="app"></div>
191+
192+
<script src="${escapeHtmlEntities(scriptUrl)}"></script>
193+
194+
<!-- IMPORTANT: assign to a variable first to prevent ), ( in values breaking the call expression. -->
195+
<!-- IMPORTANT: escapeJsonForHtml ensures <, > cannot terminate the </script> tag prematurely. -->
170196
<script>
171-
Scalar.createApiReference('#app', JSON.parse(document.getElementById('app').dataset.config))
197+
const scalarConfig = ${escapeJsonForHtml(scalarConfig)}
198+
199+
Scalar.createApiReference('#app', scalarConfig)
172200
</script>
173201
</body>
174202
`
@@ -180,8 +208,8 @@ export class OpenAPIReferencePlugin<T extends Context> implements StandardHandle
180208
<head>
181209
<meta charset="utf-8" />
182210
<meta name="viewport" content="width=device-width, initial-scale=1" />
183-
<title>${esc(title)}</title>
184-
${cssUrl ? `<link rel="stylesheet" type="text/css" href="${esc(cssUrl)}" />` : ''}
211+
<title>${escapeHtmlEntities(title)}</title>
212+
${cssUrl ? `<link rel="stylesheet" type="text/css" href="${escapeHtmlEntities(cssUrl)}" />` : ''}
185213
${head}
186214
</head>
187215
${body}

0 commit comments

Comments
 (0)