XMLHttpRequest Security

Nathaniel Inman walks through the basics of intercepting and tampering with XHR requests within the browser and a few simple techniques one might use to help mitigate these security risks.

XMLHttpRequest Security
Photo by iMattSmart / Unsplash

It's possible to intercept, adjust and otherwise tamper with a XHR request with javascript in the browser. The most common way of doing this is simply making a pointer reference to the original XMLHttpRequest.prototype.send function, overwriting that function with a new one that does the tampering and then calls the original send function once finished. Here's an example:

  const XHR = XMLHttpRequest,
        XHRopen = XHR.prototype.open;
  
  XHR.prototype.open = function() {
    onFinish();
    XHRopen.apply(this, arguments);
  };

Identifying Tampering

It's very straight forward to identify tampering of the function. Simply ensure that the XMLHttpRequest.prototype.open.toString() === 'function send() { [native code] }'; evaluates to true. The function should always be native code if it wasn't corrupted or tampered with.

Preventing Tampering

My first inclination to solve it was to perform an Object.freeze on the XMLHttpRequest.prototype to prevent adjusting or reassigning of functions like the open at all. The problem with that is if the malicious script has already manipulated the object before it's frozen, it's not helping much.

Restoring From Tampered

An easier and more robust solution than attempting to prevent tampering is simply to reacquire a pristine version of the XMLHttpRequest.prototype.send function using an iframe and use that instead. Here's an example:

const iframe = document.createElement('iframe');

// we synchronously append the iframe so the browser will create a
// pristine version of the XMLHttpRequest object, then immediately
// detach it so no listeners can attempt to manipulate it
document.body.appendChild(iframe);
XMLHttpRequest.prototype.open = iframe.contentWindow.XMLHttpRequest.prototype.open;
document.body.removeChild(iframe);

A potential problem with this approach is if the malicious script has frozen the prototype to prevent reassignment of the original XHR object. In this case the best solution could be to just acquire and use the entire XHR object instead of the prototype from the iframe's content window.

Addendum

One thought to circumvent this iframe solution is to use the MutationObserver to listen to the DOM creation. This approach won't work against the "Restoring From Tampered" solution if the actual XHR request happens immediately after the restoration as the restoration is entirely synchronous while the MutationObserver would fire the DOM creation event asynchronously.

Another thought to circumvent this iframe solution would be to overwrite the document.createElement function to listen and expect that iframe creation. This wouldn't work as the XMLHttpRequest object doesn't exist on the iframe DOM node until the DOM node is attached to parent document. The malicious script would have to do an asynchronous check which would always fire after the restoration and XHR request has finished.

Working Example