@@ -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, '&' ) . replace ( / " / g, '"' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' )
126+ /** Escapes a string for safe embedding in an HTML attribute value. */
127+ const escapeHtmlEntities = ( s : string ) => s
128+ . replace ( / & / g, '&' )
129+ . replace ( / " / g, '"' )
130+ . replace ( / < / g, '<' )
131+ . replace ( / > / g, '>' )
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 ( / " ( S w a g g e r U I B u n d l e \. [ ^ " ] + ) " / g, '$1' ) }
174+
151175 window.onload = () => {
152- window.ui = SwaggerUIBundle(${ stringifyJSON ( swaggerConfig ) . replace ( / " ( S w a g g e r U I B u n d l e \. [ ^ " ] + ) " / 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