Skip to content

fix(swagger): context-correct escaping for attribute and JS string values - #506

Open
FumingPower3925 wants to merge 3 commits into
mainfrom
fix/swagger-context-escaping
Open

fix(swagger): context-correct escaping for attribute and JS string values#506
FumingPower3925 wants to merge 3 commits into
mainfrom
fix/swagger-context-escaping

Conversation

@FumingPower3925

@FumingPower3925 FumingPower3925 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What

middleware/swagger/swagger.go interpolated developer configuration (SpecURL, UI.Title, UI.DocExpansion, UI.OAuth2RedirectURL, the OAuth2 fields, Options, AssetsPath) into the generated UI pages with fmt.Sprintf, using Go's %q for the attribute and JS-string values. %q is the wrong escaper for both embedding contexts. This PR replaces the three hand-built pages with html/template — Go's context-aware escaper — so every value is escaped for the exact context it lands in:

Context Before After (html/template)
<title> html.EscapeString RCDATA escaper
href / src asset URLs html.EscapeString (AssetsPath only) URL attribute: percent-normalised, HTML-escaped, non-http(s) schemes neutralised
Scalar data-url %q URL attribute (as above)
Scalar data-configuration html.EscapeString(json.Marshal(options)) attribute escaper over the marshalled JSON
Swagger UI url: / docExpansion: / oauth2RedirectUrl: / ui.initOAuth({...}) values, ReDoc Redoc.init(<url>, ...) %q JS value escaper: a JSON string literal with <, >, &, U+2028 and U+2029 escaped
ReDoc options object raw json.Marshal text JS value escaper marshals the map itself

Bool and int literals (deepLinking, persistAuthorization, defaultModelsExpandDepth, usePkceWithAuthorizationCodeGrant) are passed as template.JS so the template does not space-pad them; they are never configuration strings. The pages are still built once in New(); a template execution error (impossible for these inputs) panics there like any other startup misconfiguration.

Why

  • In an HTML attribute, %q escapes " as \", which HTML does not honour, and leaves & raw — a SpecURL containing " terminates the attribute.
  • In a JS string literal inside <script>, %q does not neutralise </script> (the HTML parser closes the block regardless of quoting) and emits escapes JavaScript lacks (\a, \x, \U0001xxxx).
  • html/template is the standard library's answer to exactly this problem, and — unlike json.Marshal and html.EscapeString — CodeQL models it as a sanitizer.

Not exploitable today: every value is startup configuration (swagger.Config), never request data. This is the hardening the issue asks for.

CodeQL

The first revision of this PR (a hand-rolled jsString + html.EscapeString fed into fmt.Sprintf) tripped three new critical go/unsafe-quoting alerts (#12, #13, #14: "If this JSON value contains a double quote, it could break out of the enclosing quotes"). They were false positives — json.Marshal emits \" inside the JS literal and html.EscapeString emits &#34;/&#39; inside the attribute — but CodeQL does not model either as a quote sanitizer, so after merge they would have sat on main as three open critical alerts, the same shape as the previously dismissed #10/#11. The html/template revision removes the pattern instead of dismissing the alerts: no Sprintf with a quoted placeholder remains in the package.

Result on this revision (77c0a21): the CodeQL check concludes success — "No new alerts in code changed by this pull request" — and #12, #13 and #14 are in state fixed on the PR ref (not dismissed), so nothing lands on main and nothing needs triage after merge.

Fail-first evidence

middleware/swagger/escaping_test.go builds every renderer, with and without AssetsPath, using a hostile value x"y'z</script>& + BEL (U+0007) + U+2028 + end, and asserts that <script / </script> tags stay balanced, the data-url attribute contains no raw "/'/</>/space/control/U+2028 and decodes back to the original, and every JS string literal is valid JSON that round-trips to the original value.

The test file at this revision, run against main's swagger.go:

--- PASS: TestPlainConfigUnchanged (0.00s)
--- FAIL: TestScalarNeutralisesUnsafeSpecURLScheme (0.00s)
    escaping_test.go:187: data-url attribute "javascript:alert(1)" carries the unsafe scheme
--- FAIL: TestScalarEscapesDataURLAttribute (0.00s)          (cdn + assets)
    escaping_test.go:149: unbalanced script tags: 2 <script vs 3 </script>
--- FAIL: TestReDocEscapesJSStringLiteral (0.00s)            (cdn + assets)
    escaping_test.go:202: unbalanced script tags: 2 <script vs 3 </script>
--- FAIL: TestSwaggerUIEscapesJSStringLiterals (0.00s)       (cdn + assets)
    escaping_test.go:125: unbalanced script tags: 3 <script vs 9 </script>
FAIL	github.com/goceleris/celeris/middleware/swagger	0.176s

The raw interpolations that produced those failures (note the \a, the unescaped </script> and the \" inside an HTML attribute):

  url: "x\"y'z</script>&\a
end",
<script id="api-reference" data-url="x\"y'z</script>&\a
end" data-configuration='{&#34;theme&#34;:&#34;default&#34;}'></script>
Redoc.init("x\"y'z</script>&\a
end", {}, document.getElementById("redoc-container"));
ui.initOAuth({clientId: "x\"y'z</script>&\a
end", realm: "x\"y'z</script>&\a
end", ...});

( above stands for the raw U+2028 byte sequence in the page.)

With the fix, all escaping tests pass, and the full package passes under -race:

--- PASS: TestSwaggerUIEscapesJSStringLiterals (0.00s)
--- PASS: TestPlainConfigUnchanged (0.00s)
--- PASS: TestScalarEscapesDataURLAttribute (0.00s)
--- PASS: TestScalarNeutralisesUnsafeSpecURLScheme (0.00s)
--- PASS: TestReDocEscapesJSStringLiteral (0.00s)
ok  	github.com/goceleris/celeris/middleware/swagger	1.458s   (go test -race -count=1)

Ordinary inputs are unchanged

TestPlainConfigUnchanged passes on both the unfixed and the fixed code and pins the exact url: "/swagger/spec", docExpansion: "list", oauth2RedirectUrl: "...", ui.initOAuth({...}), data-url="https://example.com/openapi.json" and Redoc.init("/swagger/spec", {}, ...) renderings; every pre-existing assertion in swagger_test.go (deepLinking: true, defaultModelsExpandDepth: -1, clientId: "my-client", ...) is untouched and passes.

In addition, I rendered 648 ordinary configurations — 3 renderers × 4 asset sources (CDN, /assets, /assets/, https://cdn.example.com/ui) × 3 spec sources (SpecContent, absolute SpecURL with a query string, relative SpecURL) × 3 option sets (nil, empty, nested theme/expandResponses/hideDownloadButton/scrollYOffset) × 6 UI configurations (defaults, full customisation, OAuth2 with redirect, partial OAuth2, empty OAuth2, PKCE-only) — with the Sprintf builder and the html/template builder; diff -r reports the pages byte-identical.

Two intentional differences, for unusual inputs only:

  • a SpecURL containing &, < or > renders as & / < / > inside the JS literals — semantically identical to the browser, and required to neutralise </script>;
  • Scalar's data-url is a URL-typed attribute to html/template: characters that are not valid in a URL (quotes, angle brackets, spaces, controls, U+2028) are percent-encoded before HTML escaping, and a scheme other than http/https (relative URLs are fine) is neutralised. Both are what a browser does before fetching. Config.SpecURL documents the scheme rule and TestScalarNeutralisesUnsafeSpecURLScheme pins it; Swagger UI and ReDoc receive SpecURL as a JS string, unchanged.

Checks

Fixes #504

…g values

The generated UI pages interpolated developer configuration (SpecURL,
OAuth2 fields, DocExpansion) with Go's %q, which is the wrong escaper
for both embedding contexts it was used in:

- HTML attribute value (Scalar `data-url`): %q escapes `"` as `\"`,
  which HTML does not honour, and leaves `&` raw, so a SpecURL
  containing `"` terminates the attribute. Now rendered as
  `data-url="%s"` with html.EscapeString.
- JavaScript string literal inside an inline <script> (Swagger UI
  `url:`/`docExpansion:`/`oauth2RedirectUrl:`/initOAuth fields and
  `Redoc.init(...)`): %q leaves `</script>` intact (terminating the
  block) and emits escapes JS lacks (`\a`, `\x`, `\U`). Now rendered via
  json.Marshal, which escapes `<`, `>`, `&`, U+2028 and U+2029.

The existing html.EscapeString(json.Marshal(options)) for the Scalar
data-configuration attribute is unchanged. Ordinary inputs render
byte-identically (verified by diffing the served pages for seven plain
configs across all three renderers, with and without AssetsPath).

Not exploitable today (all values are startup configuration and the
page is built once in New()); this is hardening surfaced by the CodeQL
go/unsafe-quoting triage.

Regression tests build every renderer with a hostile value containing
`"`, `'`, `</script>`, `&`, BEL and U+2028, with and without AssetsPath,
and assert that script tags stay balanced, the data-url attribute holds
no raw quotes/angle brackets and unescapes to the original, and each JS
string literal is valid JSON that round-trips to the original value.

Fixes #504
Comment thread middleware/swagger/swagger.go Fixed
Comment thread middleware/swagger/swagger.go Fixed
Comment thread middleware/swagger/swagger.go Fixed
CodeQL flagged the fmt.Sprintf page builders three times (go/unsafe-quoting
#12/#13/#14, all false positives: the interpolated values were already
escaped by json.Marshal or html.EscapeString, which CodeQL does not model as
quote sanitizers). Rather than dismiss the alerts, remove the pattern: the
Swagger UI, Scalar and ReDoc pages are now html/template templates, so the
context-correct escaper is chosen by the template engine - RCDATA for
<title>, URL attribute for href/src/data-url, plain attribute for
data-configuration, JSON string literal inside <script>. The hand-rolled
jsString helper and every quoted %s placeholder are gone; marshalOptions
remains only to produce the JSON text for the Scalar attribute, and ReDoc's
options map is marshalled by html/template itself.

Bool and int literals (deepLinking, persistAuthorization,
defaultModelsExpandDepth, usePkceWithAuthorizationCodeGrant) are passed as
template.JS so html/template does not space-pad them; they are never
configuration strings. Rendering 648 ordinary configurations (3 renderers x
4 asset sources x 3 spec sources x 3 option sets x 6 UI configs) with the
previous and the new builder yields byte-identical pages.

One behaviour is stricter: Scalar's data-url is a URL-typed attribute to
html/template, so characters invalid in a URL are percent-encoded and
schemes other than http/https are neutralised. TestScalarEscapesDataURLAttribute
now decodes HTML entities then percent-encoding (what the browser does before
fetching), TestScalarNeutralisesUnsafeSpecURLScheme pins the scheme rule and
Config.SpecURL documents it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

swagger: use context-correct escaping instead of Go %q for HTML attributes and JS string literals in the generated UI pages

2 participants