Write-up: Open redirect to XSS in PayPal's profile form

Archived article from 7 October 2012. Its content reflects the state of things at the time. Reconstructed from the web archive of secalert.net - view the original.

Summary. Two buttons on PayPal's developer profile form drove navigation from a returl parameter. The "cancel" button passed the value straight into location.href with no validation, giving open redirection and javascript: execution. The "save" button ran the value through encodeURIComponent, which blocked javascript:, but a Base64-encoded data: URI still executed. Both were reported under PayPal's bug bounty program and rewarded.

The "cancel" button

I recently spent some time on PayPal's bug bounty program, taking a closer look at their JavaScript files in the hope of finding HTML5 DOM-based vulnerabilities. While experimenting and analysing the DOM manipulations, I noticed some interesting behaviour around the "cancel" and "save" buttons on various PayPal pages. I will start with the "cancel" button, which was the simpler case, and then move on to the differences and the trickier "save" button.

The "cancel" button defines where the user is redirected if they decide not to make changes on the profile form. This is driven by a parameter called returl, apparently short for "return URL" (and yes, it is also prone to open redirection).

Let's try:

test
https://developer.paypal.com/cgi-bin/devscr?cmd=_edit-profile&returl=javascript:alert(document.URL)

At first nothing happens, so let's look at the HTML source:

rendered HTML, cancel button
...

Line 185: <input class="formBtnGrey" type="button" name="submit" value="Cancel" onclick="forward('javascript:alert(document.URL)')"/>

...

As you can see, the supplied value is placed inside the input tag and passed to a function called forward(). Let's find where that function lives and look at it more closely.

view-source:https://developer.paypal.com/js/dc.js
...

Line 41: function forward(loc) { location.href = loc; }

...

To my surprise there was no input validation at all. I had at least expected some use of URLencode(). Clicking the "cancel" button then executed the payload.

The "save" button

Having found the open redirection and analysed the "cancel" button, I realised that PayPal handled the two buttons differently. The "save" button behaved quite differently when I tried to exploit it, which made for an interesting little trick.

Let's look at the HTML source again.

rendered HTML, save button
...

Source line 183: <input class="formBtnOrange" type="submit" name="submit" value="Save" name="submit" value="Save"   />

...

This time the supplied value is not placed inside the input tag as it was with the "cancel" button. So where does it go? A closer look at the source reveals the following call.

the save handler on the page
...

Source line 032:<a href="#" onclick="forwardWithReturl('devscr?cmd=_edit-profile'); return false;">

 ...

So this button is handled differently and our value is not inserted directly into the HTML. Let's step into the forwardWithReturl() function.

view-source:https://developer.paypal.com/js/dc.js
Source line 45: function forwardWithReturl(loc) {

Source line 46:    var i = location.href.indexOf("returl=");

Source line 47:    if (i< 0) {

Source line 48:        location.href = loc + '&returl=' + encodeURIComponent(location.href);

Source line 49:        }

Source line 50: }

Interesting. The returl parameter is passed to the function without proper escaping, but the value is run through encodeURIComponent. That means a common payload like javascript:alert(1) is not interpreted. However, we can still inject code using a different, legitimate encoding: a Base64-encoded data: URI is executed.

Since we cannot easily pass <script> or javascript: to the parameter, let's Base64-encode it instead.

proof of concept
https://developer.paypal.com/cgi-bin/devscr?cmd=_edit-profile&returl=data:text/html;base64,PHNjcmlwdD5hbGVydCgnc2F2ZS1idXR0b24teHNzJyk8L3NjcmlwdD4=

This executes the payload and resulted in another bug bounty submission.

Impact and remediation

Both buttons let an attacker turn a crafted profile link into open redirection or script execution in the context of the PayPal page. Percent-encoding the value is not a defence on its own, because a data: URI survives it. The correct approach is to accept only same-origin return paths and to reject any value whose scheme is not http or https, rather than passing request-derived values into location.href.