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.
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.