Skip to content

Unauthenticated stack-trace disclosure on /oauth/callback (live on the slug) #106

Description

@tonychang04

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions