Live on the deployed slug, unauthenticated
GET /oauth/callback?error=x&error_description=a&error_description=b
Accept: text/html
400, and the body is Express's error page:
TypeError: text.replace is not a function
at escapeHtml2 (file:///app/dist/http-server.js:1701:15)
at renderOAuthErrorPage (file:///app/dist/http-server.js:1694:10)
at sendOAuthError (file:///app/dist/http-server.js:1746:49)
Absolute container paths and the dependency tree, to anyone who crafts the URL.
No login required — /oauth/callback is unauthenticated by design, and
error_description comes straight off req.query, where a repeated parameter
parses as an array.
master @ 28b9b26 reproduces it locally too, so this is not a deployment
artefact.
The JSON path is fine — it is the HTML path only
same URL, no Accept: text/html -> {"error":"x","error_description":["a","b"]} clean
?error=a&error=b -> clean page (error is switched on, never escaped raw)
?error=x -> clean page (control)
So it is one field, on the browser navigation that the callback always is.
This is a regression the rewrite introduced, and the reason is worth stating
Production (mcp.insforge.dev, 1.0.0) does not leak — but not because it
handles the array. It has no HTML error page at all, and answers JSON
regardless of Accept:
mcp.insforge.dev same request -> {"error":"weird","error_description":["a","b"]}
The repair page — added to help stuck users — is what introduced the
disclosure. Cutting mcp.insforge.dev over to this build ships the leak to
production.
Previously reported and shipped anyway
I raised this exact TypeError: text.replace is not a function against PR #84
on 2026-07-31, as a review finding. #84 merged without it being fixed, and it
has been live since. A finding recorded only as a comment on an unmerged PR has
no owner once that PR merges — hence this issue, so it is tracked independently
of whichever PR happens to carry the fix.
Fix
Normalising in humanFormOf (PR #103, b03a467) covers it, because
sendOAuthError spreads humanFormOf(body) straight into the renderer, so
nothing reaches escapeHtml unnormalised. Worth also making escapeHtml
defensive about non-strings, since it owns every browser-visible field and
currently trusts its input.
Filed separately from #103 so the leak does not depend on that PR landing.
Live on the deployed slug, unauthenticated
Absolute container paths and the dependency tree, to anyone who crafts the URL.
No login required —
/oauth/callbackis unauthenticated by design, anderror_descriptioncomes straight offreq.query, where a repeated parameterparses as an array.
master@28b9b26reproduces it locally too, so this is not a deploymentartefact.
The JSON path is fine — it is the HTML path only
So it is one field, on the browser navigation that the callback always is.
This is a regression the rewrite introduced, and the reason is worth stating
Production (
mcp.insforge.dev, 1.0.0) does not leak — but not because ithandles the array. It has no HTML error page at all, and answers JSON
regardless of
Accept:The repair page — added to help stuck users — is what introduced the
disclosure. Cutting
mcp.insforge.devover to this build ships the leak toproduction.
Previously reported and shipped anyway
I raised this exact
TypeError: text.replace is not a functionagainst PR #84on 2026-07-31, as a review finding. #84 merged without it being fixed, and it
has been live since. A finding recorded only as a comment on an unmerged PR has
no owner once that PR merges — hence this issue, so it is tracked independently
of whichever PR happens to carry the fix.
Fix
Normalising in
humanFormOf(PR #103,b03a467) covers it, becausesendOAuthErrorspreadshumanFormOf(body)straight into the renderer, sonothing reaches
escapeHtmlunnormalised. Worth also makingescapeHtmldefensive about non-strings, since it owns every browser-visible field and
currently trusts its input.
Filed separately from #103 so the leak does not depend on that PR landing.