Introduction
Now that we have covered cookies in their particular, isolated post (does that mean we have put them into a blog partition?), it is time to explore partitioned storage in more detail.
The problem is that in reality, there is no unique storage element that represents partitioned storage, per se. Rather, partitioning is a concept that has evolved as a means of reducing the risk surface around cross-site tracking. Remember the most important design principle underlying the Privacy Sandbox is to prevent, as much as possible, any form of cross-site tracking at the individual browser level. Partitioning is one of the fundamental design approaches that Google and other browser developers have standardized upon to achieve this goal.
Partitioning as a design concept has been used in browsers for a long time. Most forms of storage today are partitioned by top-level domain (TLD). When we talk about partitioning in terms of the Google Privacy Sandbox, what we are actually talking about is enhanced partitioning, which comes in two flavors:
- Adding a partition key where none existed before
- Adding additional keys beyond the TLD to restrict the scope of access by third-party elements on a web page or to specific subdomains
I introduced the concept of partitioned storage in the post on fenced frames. In the next few posts, we are going to delve more deeply into how this approach is used across all of Chrome storage elements to support the goals of the Privacy Sandbox. We’ll start with how the various storage elements behaved prior to the notion of enhanced partitioning. In the next post we will take a slight detour into another browser technology that supports the Sandbox: Web Assembly or WASM. Finally, we’ll explore the enhancements that the Privacy Sandbox makes to storage and their implications.
Session and Local Storage
A unique page session gets created and assigned to a particular tab when a tab is opened in Chrome. A page session lasts as long as the tab or browser is open. It also survives both page reloads and restores (for example when the browser crashes).
Each session has two kinds of storage: session storage and local storage. Session storage only maintains items until the session ends and then clears them. Local storage maintains small amounts of data that a website may need to use across sessions, such as user preferences, login state, or application state. Local storage has a limited storage quota (typically around 5MB). So it is not ideal for storing large amounts of data.
Why not use cookies instead? There are, in my mind, three main reasons:
- Limited Storage. Cookies have even more limited storage than local storage - about 4KB per cookie. This makes them unsuitable for larger data like application state information.
- Security Issues. Cookies are sent with every HTTP request to the server, which can be a security risk if they contain sensitive information.
- Performance. Local storage doesn't send data with every request to the server, improving website performance.
Going back to the definition of a session, what happens when, like me, you have 80,000 tabs open (let’s not exaggerate, it's actually only 94) and you accidentally (or purposely) open multiple tabs to the same or different pages on a single site? Chrome treats those as part of the same session. This allows any data stored during your browsing session, like cookies or session storage information to be accessible by both tabs. However, each tab can navigate independently. You can browse different pages within the same site on each tab without affecting the other.
This works because Chrome uses the origin as a key for partitioning session data. It turns out this is also the key that is used to isolate content stored in local or session storage. Thus even before the Privacy Sandbox, local and session storage were partitioned.
An origin-based partition, however, is not ideal from a privacy perspective. This is because that single key can be used not just on the top-level domain but on all subdomains. Now you might ask, why is that such a big deal? I’m only on one site. I obviously have a reason to be there if I am floating around enough to hit different subdomains (e.g. financeco.com vs. mybanking.financeco.com). And moreover, sites use Google Analytics or other analytics vendors, to learn about my movements within the site to help optimize my user experience. So why the big deal?
Well, I may not think it is a big deal (and I don’t), but others might. For example, I am on mycommercesite.com and while shopping I visit a specific seller’s subdomain, sexy.mycommercesite.com, that sells sexually explicit materials like videos or games. I really don’t want the site to track me across those two subdomains because I don’t want them popping emails into my inbox at a later time with recommendations for sexually-explicit items. Moreover, in today’s cookie banners, you can opt-out of analytics cookies, so that sites can’t use analytics to understand your behavior patterns. Thus, even within a site tracking is considered a privacy violation if not permissioned by the user.
So why not then key session and local storage to sexy.mycommercesite.com when that page loads, thus treating it as the origin? Alternatively, if I land there first and then go to mycommercesite.com, why wouldn’t I make the key the subdomain? The answer is you can’t. When the specification says you have to key to the top-level domain (TLD), that’s all you can do. You can’t treat a subdomain, also known as a TLD+1, as the TLD.
Thus Google considers origin (top-level domain) partitioning of session and local storage to provide a privacy risk.
SQL and NoSQL Databases
The Three Amigos: SQLLite, WebSQL, and IndexBD
The next major form of browser storage are SQL and NoSQL databases. SQLite and WebSQL are/were SQL databases, as their names imply. IndexedDB is a NoSQL database. Now you might say, those are two different types of storage, which I would generally agree with. Except in this case, SQLite underlies both WebSQL and IndexedDB. How that is possible requires delving a bit into the history of client-side databases.
SQLite is a relational database engine. It is not a standalone app like PostgreSQL that you access via a desktop SQL client (e.g. dbVisualizer) and read the data contained in a set of tables. It is a library that software developers embed in their web apps, and thus is part of a class of embedded databases. It was invented in 2000 by Richard Hipp of General Dynamics. SQLite is not a browser-specific technology. It is embedded in a wide array of applications. However, it is used extensively in web development. Chrome installs it with the browser and depends on it directly to store data like user browsing history.
WebSQL is a deprecated web browser API that provided a database capability within browsers. It can be queried using SQL tools. WebSQL was introduced as part of Google Chrome in 2009. However, Mozilla developers were intensely opposed to it. In fact, WebSQL was never implemented in FireFox. Only Chrome, Safari, Internet Explorer/Edge, and a few others did implement it. Mozilla’s objections had to do with the fact that WebSQL was basically a wrapper around SQLite. These objections were stated in a 2010 blog post on mozilla.org:
We think SQLite is an extremely useful technology for applications, and make it available for Firefox extensions and trusted code. We don’t think it is the right basis for an API exposed to general web content, not least of all because there isn’t a credible, widely accepted standard that subsets SQL in a useful way. Additionally, we don’t want changes to SQLite to affect the web later, and don’t think harnessing major browser releases (and a web standard) to SQLite is prudent.
Mozilla’s preferred option was IndexedDB, and it ultimately has supplanted WebSQL in all major browsers. Now it turns out that IndexDB is another child of SQLite. Well sort-of. IndexedDB on Firefox, Safari and Edge all use SQLite. But leave it to Google to use a different embedded database called LevelDB. LevelDB was invented by Google fellows Jeffrey Dean and Sanjay Ghemawat in 2011. LevelDB, unlike SQLite, is not a SQL database. It does not have a relational model and does not support SQL queries. You can therefore understand why Google chose to use it over SQLite. Why use a SQL backend for a NoSQL front-end? There are performance and other issues with using a SQL backend. Having LevelDB as the embedded database helped avoid these issues. For example, Google provided benchmarks in 2011 comparing LevelDB's performance to SQLite, and showed that it outperforms SQLite in write operations and sequential-order read operations
IndexedDB in Chrome is both an API and an abstraction layer built on top of LevelDB. As you might expect, an abstraction layer abstracts out certain functionality from another object to shield developers from unnecessary complexity. In essence, IndexedDB acts as an intermediary between web developers and the underlying LevelDB storage engine. It provides a higher-level abstraction with a more user-friendly data model and operations while still leveraging the core functionality of LevelDB for efficient data storage and retrieval.
With that introduction, let’s drill further into SQLite and IndexedDB and their status prior to the Privacy Sandbox. We’ll ignore WebSQL because as of Chrome 123 it is no longer supported, even for backwards compatibility.
SQLite
SQLite stores each of its databases as a file. The whole database (definitions, tables, indices, and the data itself) consists of a single cross-platform file on a client machine, allowing several processes or threads to access the same database concurrently. These files are not partitioned by origin key, nor do they require any kind of authentication with usernames and passwords. As I did in an earlier post, you can find a SQLite viewer extension and query the database directly. Here’s a second example where I queried the Chrome preferences file with the simple query at the top. I didn’t have to log in - just opened the file in the C:\Users\arthu\AppData\Local\Google\Chrome\User Data\Default directory. You can see all the tables in the database on the left side of the screen and no doubt will note that I have direct access to very personal information, like my credit card data.
Figure 1: Another View to a SQLite File in Chrome
However, access to these files is controlled by the file system. So unless a web application can break through other security protections Chrome employs, such as running in the Windows Sandbox or enforcing sites isolation in memory (don’t worry about what these are just know they are there), Chrome ensures security and privacy of the SQLite files.
IndexedDB
IndexedDB, as previously mentioned, is a client-side, NoSQL, high performance database that allows web applications to store and retrieve data structured into key-value pairs. It is used when large amounts of data need to be handled efficiently within the browser. This mechanism allows applications that depend on such data can retrieve it and render it on-screen in a fashion that does not degrade the user experience. IndexedDB also allows developers to cache this data locally so it can be available offline.
IndexedDB is partitioned by top-level domain. Each domain has its own database where it stores data, and each database is stored in its own subdirectory under the default directory (Figure 2). The database, without special approaches, does not have any user authorization. That is to say, you don’t need a username or password to access the data in the database. The way data is secured is that any request to access an IndexDB database must come from the top-level domain which created the database. There are ways to create a mechanic to create tighter security for IndeDB databases, but they are not immediately relevant to the Privacy Sandbox discussion.
Figure 2 - IndexedDB File for GoDaddy Shown In Directory and Contents of File Shown on GoDaddy Site
Origin Private File System
The Origin Private File System (OPFS) is a very different type of storage from anything we have discussed so far. Unlike local storage or IndexedDB, which use an optimized object/key-value storage mechanism, OFPS enables byte-by-byte access, file streaming, and low-level file manipulation. OFPS uses a sandboxed file system, so in that sense it is “partitioned”, although that is a misuse of the term. It is private to the origin of the page (website or embedded element) and not visible to the user. It is intended to allow web apps to store and manipulate files in their very own origin-specific file system on the client, and is particularly useful where high performance/high throughput files operations are required. To give a sense of the difference, there are estimates that OPFS is 3-4x faster at disk I/O in comparison to IndexedDB. Equally important, it provides more efficient use of resources as well as enhanced security and privacy.
Remember we talked about web workers in an earlier post? If not, you may want to go back and review what a web worker is in order to understand the next statement. Since OFPS works with local files on disk, it has read() and write() methods to access data from and write data to the local hard drive. The read() and write() methods are only available inside a web worker. This is because read() and write() are synchronous methods (they run as called) and if they were to run on the main thread, for example calling or writing continuously streaming data, they could significantly impact app performance. So they run in a web worker to allow them to manipulate data on and off the disk in parallel with the main browser actions.
That’s about as deep as I want to go into the technical aspects of OFPS except for one other item. OFPS can write files anywhere on the hard drive, so there is no specific subdirectory under Chrome where you will find OFPS files.
Application Cache
The browser application cache (cache) stores data from websites for use by documents and web applications even when a network connection is unavailable. This is a concept that almost every user of the web is familiar with, as at some point or another they have been asked to ‘clear their cache’. The cache also provides speed. Since a page has already been downloaded and cached, its resources come straight from the disk.
The cache is basically partitioned by origin. Each site has its own files in the C:\Users\arthu\AppData\Local\Google\Chrome\User Data\Default\Cache\Cache_Data directory (on windows). The files are stored in a binary format that is described here for those who want to drill further.
Figure 3 shows an example. Here I have used a binary converter to convert one of the files to visualizable form. As you can see, I can easily access the image from this site.
Figure 3 - Reading an Application Cache File from the Local Hard Drive
I can access this from my local hard drive as I have direct access to the file system. As a rule, the cache from one web site cannot be viewed unless the request has the origin site in the request header. However, that level of partitioning is not an impervious solution. There are numerous attacks that an evil actor can use - cross-site scripting, local storage hijacking, and cache poisoning to name a few - to gain access to other site’s cache elements and create a cross-site user profile. Existing mechanics can mitigate some of these, but they are not foolproof. We will discuss what Chrome has done to improve cache security and isolation in the post where we talk about the Cache API, which replaced a previous technology called AppCache in 2020. Given its origin date, Cache API is not part of the Privacy Sandbox specifically, but a technology on which it depends.
BLOB URL Storage
A BLOB is a Binary Large Object. BLOBS can be all sorts of files - big images, audio, video, or documents. In fact, when you download a file from a website, that is most likely downloaded by Chrome as a BLOB. BLOBs are stored in cache. However they can often be too big for the available memory, so they are sliced and the portions not being processed are temporarily stored on disk. Every web client must maintain a BLOB URL store, which is a key-value map where the key is a valid URL string and the value is the URL that points to the blob. Blob URLs look like:
blob:http://example.com/550e8400-e29b-41d4-a716-446655440000
While a BLOB is keyed to its origin. BLOB storage is not partitioned. BLOBS tend to reside in the browser’s cache temporarily and are often sliced, so do not really represent a likely attack surface for cross-site tracking. There are some subtleties here, as there are cache management capabilities that control how and for how long a BLOB is kept in the cache, but they do not change the basic rationale for why BLOB storage is not partitioned.
Done for today.
Next Post: Web Assembly and SQLite