Last week I built a Chrome extension that was supposed to solve privacy on the Web. It didn’t - but that’s a different post.
I did, however, find my self in need of intercepting all cookie read/writes in a Chrome extension. This proved to be interesting and non-trivial so here’s a brain dump of the process - hopefully this helps someone somewhere. (and by someone, I’m looking at you, future self!)
Yup. Some potential use cases:
- Managing multiple Cookie stores
- Using alternative Cookie stores
- Namespacing Cookies
- Exposing Cookies on a conditional basis like time, url, etc.
- Cookie Art
Generally speaking, an extension will intercept cookies either to save your privacy or to completely violate it. With great power comes great responsibility.
Isn’t there already a Cookie API for Chrome extensions?
True. And it’s a fine API, yet, it does not allow real-time interception. Meaning, you can read and write cookies and even get notified when cookies change, but not manipulate them WHILE they are being read / written.
For the sake of this post I will build OOGI (shorthand for Oogifletset - the Hebrew translation of “The Cookie Monster”) a simple extension that statically namespaces all cookies with the prefix
oogi$. When cookies are set they will be prefixed before being written into the cookie store, and when being queried the prefix will be removed - making it completely transparent to remote servers.
There are two ways to get and set cookies. I’ll tackle each one of them separately:
- HTTP Headers
The HTTP Request “Cookie” header will contain all cookies currently set and the HTTP Response’s “Set-Cookie” header writes/modifies cookies.
A Request’s “Cookie” header is a concatenation of all cookie names and values for the current domain and path:
A Response can contain multiple “Set-Cookie” headers, one per cookie. These also contain a Path and optionally a timeout:
Luckily, Chrome extensions have a Webrequest API for manipulating HTTP requests and responses in a blocking manner.
Declaring stuff in
The WebRequest API allows us to intercept different stages in the lifecycle of an HTTP Request/Response loop.
background.js, we plug into
onBeforeSendHeaders to modify sent cookies and to
onHeadersReceived to modify the setting of cookies:
The actual cookie string manipulation happens in a separate file. See full code for specifics.
This is the fun part.
document.cookie in the Chrome dev tools console:
Looks like a String property. right? Let’s try to write a different value.
So writing to this property actually appends the written value?
Nope. It seems that the String is only a representation of an actual collection behind the scenes.
So what’s going on here? Accessor properties can be set with custom getter and setter functions. That means that by invoking the property we’re actually calling a getter function and by assigning value, a setter function. These are defined using the Object.defineProperty() method.
Let’s try to use Object.defineProperty() function to override document.cookie:
Great success! This might suffice if our extension completely ignores Chrome’s internal cookie store. But, if access to the cookie store is still required, which is case for Oogi, we need to save a reference to the original getter and setter functions of document.cookie.
Theoretically, the Object.getOwnPropertyDescriptor() function should help with that.
It seems, however, that Chrome doesn’t want you to access that specific descriptor.
However, all of this this experimentation was done in the Chrome dev tools console. How do we port this to our extension?
Declaring our content script in
content.js script that injects an external js file:
And finally, overriding
I left out some of the glue code to keep this post relatively short. You can checkout the complete example here. Feel free to submit issues and pull requests if I missed anything.