Monday, January 13, 2014

Evolution of Open Redirect Vulnerability.

TL;DR /// is parsed as relative-path URL by server side libraries, but Chrome and Firefox violate RFC and load instead, creating open-redirect vulnerability for library-based URL validations. This is WontFix, so don't forget to fix your code.

Think as developer. 
Say, you need to implement /login?next_url=/messages functionality. Some action must verify that the next URL is either relative or absolute but located on the same domain.

What will you do? Let’s assume you will start with the easiest option - quick regexps and first-letters checks.
1. URL starts with /
Bypass: //
2. URL starts with / but can’t start with //
Bypass: /\
3. At this point you realize your efforts were lame and unprofessional. You will use URL parsing library, following all the RFC-s and such - \ is not allowed char in URL, all libraries wouldn't accept it. Much RFC, very standard.

require ‘uri’
uri = URI.parse params[:next]
redirect params[:next] unless or uri.scheme

Absence of host and scheme clearly says it is a relative URL, doesn’t it?
Bypass for ruby, python, node.js, php, perl: ///

1 is for path, 2 is for host, 3 is for ?
>A scheme-relative URL is "//", optionally followed by userinfo and "@", followed by a host, optionally followed by ":" and a port, optionally followed by either an absolute-path-relative URL or a "?" and a query.
>A path-relative URL is zero or more path segments separated from each other by a "/", optionally followed by a "?" and a query.
A path segment is zero or more URL units, excluding "/" and "?".

Given we have base location as and where will following URLs redirect?
/ is a path-relative URL and will obviously load
// is a scheme-relative URL and will use the same scheme, https, hence load
The question is where /// (also //// etc) will redirect?

Out of question, it is a path-relative URL too. Third letter is /, so it can’t be a scheme-relative URL (which is only //, followed by host which doesn’t contain slashes).
It has 2 URL units which are empty strings, concatenated with / and supposed to load

The thing is, both Chrome and Firefox parse it as a scheme-relative URL and load Safari parses it as a path. Opera loads http:///// (?!).
where #secret can be access_token or auth code

Use a Library, Luke
Functionality like /login?to=/notifications is very common so can be found almost on any website. Now the question is how Good Programmers validate it?
As proved in the beginning of the post, best practise would be to use URL parser.

Let’s see how major platforms deal with ?next=///

Perl (parses as a path)
use URI;
print URI->new("///")->path;

Python (parses as a path)
import urllib
>>> urlparse.urlparse('//')
ParseResult(scheme='', netloc='', path='', params='', query='', fragment='')
>>> urlparse.urlparse('///')
ParseResult(scheme='', netloc='', path='/', params='', query='', fragment='')
>>> urlparse.urlparse('//////')
ParseResult(scheme='', netloc='', path='////', params='', query='', fragment='')

Ruby (parses as a path)
1.9.3-p194 :004 > URI.parse('///').path
 => "/"
1.9.3-p194 :005 > URI.parse('///').host
 => nil

Node.js (parses as a path)
> url.parse('//').host
> url.parse('///').host
> url.parse('///').path

PHP (mad behavior, quite expected)
print_r( parse_url("///"));
This doesn’t work (but should). You might be happy but wait, while all languages don’t parse /\ because it is not valid PHP gladly parses it as a path.

print_r( parse_url("/\"));
Thus PHP is vulnerable too.

Security implications
Basically, with /\ and /// we can get an open redirect for almost any website. Yeah. No matter you have “home made” parser or reliable server-side library - most likely it's vulnerable.

The only good protection is to respond with full path:
Location: http://myhost/ + params[:next]

Besides phishing, redirects can exploit many Single Sign On and OAuth solutions: 302 redirect leaks #access_token fragment, and even leads to total account takeover on websites with Facebook Connect (details soon).


  1. Or you could, y'know, normalize the path if it is a local path.

    Seems silly to just check if it is a local path without actually making sure the path is remotely sane. `require('path').normalize('///')` in node.js gives me `/`, which is probably what we want, no?

    1. Node just told you / is a path. This is a bug because this is *not* a path, and if you respond with Location: /// it will load

  2. Link to the issues in both browsers' bug trackers?

    1. Found them:

  3. PHP documentation for parse_url states that the function is not meant to validate urls.

    The core filter extension, however, seems to behave correctly:

    var_dump( filter_var ("///", FILTER_VALIDATE_URL) ); // bool(false)
    var_dump( filter_var ("/\", FILTER_VALIDATE_URL) ); // bool(false)

    1. never saw filter_var used for this purpose. Overall, PHP users don't validate anything.

  4. If what you respond with in a Location: header is anything but a full absolute URI (protocol and all), you're doing it wrong. (see RFC 2616)

    If your framework doesn't allow you to know inside the implementation of /login?next_url=/messages what your full URI is, your framework is doing it wrong.

    Yes, browsers accept all sorts of things in Location: headers. That doesn't mean it isn't sloppy (and, as you show, vulnerable) practice to use anything but an absolute URI.

    1. in perfect world everybody follows RFC, but not here)

  5. Fair points. Also internal/relative URL's can also be external-URL's. there is this specific example, take \%09/ if the filters (mostly like Facebook's linkshim) trying to detect // or \/ that should bypass it. worked for me in multiple-cases.