Michael G. Noll

Applied Research. Big Data. Distributed Systems. Open Source.

Cookie Monster for XMLHttpRequest

Some time ago, I had to find a way to strip cookies from XMLHttpRequests in Mozilla Firefox. It took me a while to figure it out, so I thought it might be a good idea to share my results.

In order to follow the rest of this article, you should have some basic knowledge of JavaScript and XPCOM. You might also want to browse the XUL Hub on the Mozilla Developer Center.

What we want to do

The goal is to implement a small JavaScript class, the “Cookie Monster”, which a) can remove cookies from XMLHttpRequests in Mozilla Firefox and b) can be used in a very simple way. I tested the cookie monster successfully with Firefox version 1.5.x and 2.0.x.

The code to use the cookie monster will be as follows:

1
2
3
4
5
6
7
8
9
10
var request = new XMLHttpRequest();
request.open(...);

// other code of yours

new CookieMonster(request); // cookie monster will make sure no cookies will survive!

// other code of yours

request.send(); // actually send the XMLHttpRequest</pre>

Sounds easy enough, eh? So let’s start to create our cookie monster.

A Word of Note

One might think that the easiest way to remove cookies from XMLHttpRequest would be to directly modify the HTTP headers of the request, for example by using setRequestHeader() as seen at the Mozilla Developer Center (see also Using XMLHttpRequest). However, the following code will not work.

1
2
3
var request = new XMLHttpRequest();
...
request.setRequestHeader("Cookie", ""); // nope, will not work

There are several reasons why this intuitive approach will get you nowhere.

  1. First, the “setRequestHeader()“ method of the XMLHttpRequest object will actually append cookies to the request. It will not replace and thus not remove them.
  2. Second (and this took me a while to figure out), the way that cookies are added to XMLHttpRequests nullifies the approach. I haven’t looked at the actual source code, but it seems that cookies are attached to requests at a later stage.

Ok, maybe this sounds a bit too fuzzy. What basically happens is that when we try to remove the cookies by callingsetRequestHeader(), the cookies have not yet been included to the request. Messing around with the HTTP headers will be pointless at this time because all those pesky cookie HTTP headers which we want to remove in the first place will simply be addedafter we called setRequestHeader().

The correct way to implement our cookie monster is therefore slightly more complicated.

The Plan

The basic idea is to use observers for getting notified when cookies are actually added to the request, and to usensIHttpChannel.setRequestHeader() to actually remove the cookies. Thus, the cookie monster will observe the assigned XMLHttpRequest and jump at its throat the moment it smells fresh cookies included in the HTTP headers!

The Execution

The CookieMonster class will provide the following methods:

  • “QueryInterface(iid)“ : method required to be compliant with the nsIObserver interface
  • “observe(subject, topic, data)“ : removes the cookies on notification; the actual method definition is also required to be compliant with the nsIObserver interface
  • “stopEating()“ : stop the cookie monster and do a clean up

Let’s start.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function CookieMonster(aXMLHttpRequest)
{
    this.channel_ = aXMLHttpRequest.channel;

    // happens after the cookie data has been loaded into the request,
    // but before the request is sent
    this.topic_ = "http-on-modify-request";

    this.observerService_ = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
    this.observerService_.addObserver(this, this.topic_, false /* hold a strong reference */);

    // we assume that waiting 15 seconds for cookies is enough in practice;
    // we want to have a defined end time for removing the observer again
    this.lunchTime_ = new Scheduler(this.stopEating, 15000 /* stop eating after 15 seconds */);
}

We assign an XMLHttpRequest to our cookie monster. The monster will make sure that no cookie will ever make it to the server to which the request is sent.

First, we store a reference to the channel property of the XMLHttpRequest object. This channel provides an interface tonsIChannelnsIHttpChannel, which in turn provides a slightly enhanced version of setRequestHeader(). We will use this method later on to actually remove (or eat, as you wish) the cookies, and thus solve problem #1.

Second and in order to solve problem #2, we have to give our cookie monster some assassination training and tell it to watch out forhttp-on-modify-request events (lines 9-10). The http-on-modify-request topic is triggered after the cookie data has been loaded into the request, but before the request is sent.

It is recommended to make sure that observers are removed when they are not needed anymore (to avoid memory leaks), especially when using strong references. This is the reason for line 14, where we make use of a small helper class, Scheduler, whose purpose is to force the cookie monster to stop eating/watch for cookies after 15 seconds have passed. It also makes sure that the cookie monster will not wait forever in case the XMLHttpRequest simply does not have any cookies to be eaten. For the sake of simplicity, we will not look at the Scheduler class for now.

1
2
3
4
5
6
7
8
9
10
/*
    Be a standard conform cookie monster.
*/
CookieMonster.prototype.QueryInterface = function(iid)
{
    if (   iid.equals(Components.interfaces.nsISupports)
        || iid.equals(Components.interfaces.nsIObserver))
        return this;
    throw Components.results.NS_ERROR_NO_INTERFACE;
}

We need to implement a QueryInterface() method so that the observer service from the previous code snippet knows that our cookie monster is able to observe topics, in this case http-on-modify-request. If you have ever worked with observers before, this is nothing new and a pretty standard way to implement this required method. No magic here (and no cookies, unfortunately).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
    When we are notified that a cookie comes our way through our channel
    (attached to the XMLHttpRequest), we will eat all of them, i.e. remove them.
*/
CookieMonster.prototype.observe = function(subject, topic, data)
{
    if (topic != this.topic_ || subject != this.channel_)
        return; // not our cookies, bleh (as if the original cookie monster did care...)

    // lunch time!
    this.channel_.QueryInterface(Components.interfaces.nsIHttpChannel);
    this.channel_.setRequestHeader("Cookie", "", false); // aaah, cookies! scrunch, scrunch...

    // Cookies will only be included once to the HTTP channel, so whenever
    // we have been notified via topic "http-on-modify-request" and ate all
    // cookies, our work is done and we will stop eating.
    this.lunchTime_.stop();
    this.stopEating();
}

The observe method is the critical part. Here’s where we let the cookie monster eat up all cookies!

We check first if the notification sent from the observer service is matching the topic we’re interested in (http-on-modify-request) and make sure that the notification corresponds to the channel of the assigned XMLHttpRequest. If so, we let the cookie monster lose: we use the slightly enhanced setRequestHeader() method of the channel to remove all existing cookies. The important difference to XMLHttpRequest.setRequestHeader() is the availability of a third parameter called merge, which we set to false. To quote the XPCOM Reference: If [the HTTP header] value is empty and merge is false, the header will be cleared.

After we have removed all cookies, there is no need to watch out for new cookies, so we will stop scheduler (we are already done) and stop eating, as seen in lines 17-18.

1
2
3
4
5
6
7
8
9
10
11
12
/*
    Stop eating cookies.
*/
CookieMonster.prototype.stopEating = function()
{
    // we finished our lunch, so we clean up (again, as if the original cookie monster…)
    this.observerService_.removeObserver(this, this.topic_); // avoid memory leaks

    delete(this.channel_);
    delete(this.lunchTime_);
    delete(this.observerService_);
}

The last method, stopEating(), is used to end the life of our cookie monster because it has served its purpose. The cookie monster stops watching for cookies (line 7), and handles all instance variables over to garbage collection. I admit that we have coded a rather domestized version of the original cookie monster, but really, cleaning up is important nowadays (at least our cookie monster is still allowed to eat cookies).

That’s it! We have implemented a cookie monster which observes an XMLHttpRequest and removes all cookies from it. I hope it was as easy as promised.

Download

You can download the code straight from my GitHub repository.

The code has been tested with Firefox version 1.5.x and 2.0.x.

It might work with Firefox version 3.x. However, there are some changes in 3.x that will require you to update the Cookie Monster code. For example, all of the relevant attributes of the nsIJSXMLHttpRequest interface, which in Firefox 2.0.x allowed you to monitor an HTTP request for progress updates, errors, etc., were moved to the nsIDOMProgressEvent interface in Firefox 3.1. Have a look at the Monitoring Progress section in Using XMLHttpRequest for instructions on how to update your code for Firefox 3.x.

License

The code is licensed to you under the GNU General Public License, version 2.

An alternative approach

Update 2011-09-25: Reader Ben Bucksch pointed out a different – and easier – method to prevent Firefox from sending cookies:

Given that it’s the cookie lib that’s overwriting our header, I just deactivate the lib. That’s fairly simple:


yourXMLHttpReq.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;

Ben Bucksch

See also the documentation for LOAD_ANONYMOUS:

1
2
3
4
5
6
/**
 * When set, this flag indicates that no user-specific data should be added
 * to the request when opened. This means that things like authorization
 * tokens or cookie headers should not be added.
 */
const unsigned long LOAD_ANONYMOUS = 1 << 14;

Thanks, Ben!

Comments