Skip to content

Build1 publisher2 min readPublished

cas-authentication-user's redirect fix falls back to a request path Node parses as //bad.example.com

The isSafeReturnTo guard added in 0.3.0 refuses //host and /\host correctly. It then falls back to the path the request arrived on, and url.parse reports /\bad.example.com with a pathname of //bad.example.com.

The Engineer · Build desk

Illustration accompanying cas-authentication-user's redirect fix falls back to a request path Node parses as //bad.example.com

What happened

  • cas-authentication-user 0.3.0, the release that fixed an open redirect on the login route, shipped a second open redirect in the same function.
  • Calling bounce_redirect on 0.3.0 with an authenticated session, an empty query object and an originalUrl of /\bad.example.com prints Location: //bad.example.com.
  • Reaching it takes no returnTo parameter, no service ticket and no cooperation from the CAS server, because the authenticated client is redirected before any of that happens.
  • Setting returnTo to an off-site URL makes the new guard fire, and the emitted Location is unchanged because the code falls back to the path the request arrived on.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure An attacker needs one link into the app and a user who is already logged in; nothing configured on the CAS server can gate a redirect that fires before a ticket is presented.
  • constraint Validating a parameter bounds the bug only when that parameter is the single road to the sink. A security patch's passing test cases tell an operator nothing about how much of the redirect surface is covered.
  • decision Sites running this library now have to decide whether to normalise request paths in front of it, because the released fix guards the returnTo value and leaves the request-path fallback at line 708 unguarded.
  • precedent A line inherited at fork time survived a URL-handling rewrite for mounted routers and a security patch aimed at the same function.

The redirect at line 708 of `index.js` reads `res.redirect(req.session.cas_return_to || requestPath(req))`; the one at line 318 redirects to the session value alone [11]. When the session value is empty the fallback supplies the target, and Node's `url.parse` reports the path `/\bad.example.com` with a pathname of `//bad.example.com` [10]. A Location beginning with two slashes is protocol-relative, so the browser reads what follows as a hostname and the victim lands on bad.example.com [5]. The guard runs upstream of that fallback.

Two assignments fill `cas_return_to`, at lines 315 and 453, and 0.3.0 validates the query-string value that arrives at each of them [12]. The request path arrives at both assignments as well, and line 708 falls back to that path on its own [13]. Five paths end at a `res.redirect` call, and `isSafeReturnTo` covers two of them [20].

The check itself is careful work. It requires a leading slash and refuses a second slash or a backslash in position two, so it was written by someone who knew that `//host` is protocol-relative and that `/\host` is the variant browsers normalise into the same thing [7]. It returns false for `//bad.example.com`, false for `/\bad.example.com`, and true for `/safe` [8]. The modelling error is one level up. "Auditing the input tells you that one road into the variable is guarded; auditing the sink tells you how many roads there are," the maintainer wrote [14]. He went looking for user input, found `returnTo` because a query parameter is what user input looks like, and never classified the request path as input at all, though a stranger chooses it completely by choosing what follows the hostname in the link [15].

The published reproduction calls `bounce_redirect` on a hand-built `req` object [21]. It does not show whether a given deployment delivers the backslash to `url.parse` untouched. Counting the assignments and fallbacks that reach `res.redirect` in the code a site actually runs was the check 0.3.0 needed before shipping [20].

A regression would have been the neater story, and the maintainer expected one: 0.3.0 had rewritten the URL handling to support mounted routers, and he assumed while writing the fix that the request-path redirect was his own [16]. On the fork's first commit, `url.parse(req.url).path` goes into `cas_return_to` and the code redirects to it after validating the ticket, with `cas_return_to` reproducing as `//bad.example.com` [17]. The two holes existed side by side for seven years and were fixed eight days apart [18]. By his account the inherited line was not a lapse by kayleecodes1, whose library he forked, because `url.parse` was the URL API in Node when that code was written [19].

What to watch

  • Whether a 0.3.1 validates requestPath(req) at line 708 and not only the query-string value.
  • Whether the upstream library by kayleecodes1 still carries the 2019 assignment of url.parse(req.url).path to cas_return_to.
  • Whether an advisory is filed, so npm audit reports either redirect to sites running 0.3.0.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories