Demo: Exploiting leaked timestamps from Google Chrome extensions

Image for Chromium extensions timestamp leaks

Chromium extensions are unintentionally exposing an exploit that can be used for advanced browser fingerprinting. In this article, we’ll demonstrate how we managed to create a unique visitor identifier using the last modified timestamps of extension files.

To illustrate the issue, we prepared a demo that works in all Chromium-based browsers such as Google Chrome, Opera, or Microsoft Edge.

DISCLAIMER: Fingerprint does not use this technique in our products, and we do not provide cross-site tracking services. We focus on detecting and preventing fraud and supporting modern privacy trends for removing third-party tracking entirely. We support open discussions about such techniques to help internet browser providers fix them quickly. 

Browser extension files and their metadata

When a user wants to install an ad blocker or a password manager onto their Google Chrome browser, they download an extension from the Google Web Store. This extension is typically an archive of files saved locally on their computer. 

Each file of your computer’s file system contains useful metadata, such as its size and precise time of last modification. In the case of extensions, the last modification timestamp reflects the specific time when the extension was installed.

In order to keep extensions up to date, the browser automatically requests updates from the Google Web Store in the background. We found that the extension will attempt to update once every 5 hours from the start of the browser:

// Default frequency for auto updates, if turned on (5 hours).
inline constexpr int kDefaultUpdateFrequencySeconds = 60 \* 60 \* 5;

If there is a newer version of an installed extension available, it will be automatically updated. The browser will replace all local extension files with ones extracted from the latest archive. This means that the “last modification” timestamp of the file metadata will be updated. This is relevant to all extension source files without exceptions. In practice, some updates can be delayed — for example, if the extension is actively running in the current browser tab. 

The update or installation time is also affected by external factors such as the user's time zone or internet speed. This creates a unique data pattern for last modification timestamps since the precise minute and second of extension installation or update is randomly distributed in time. 

Based on our experience with similar browser unique attributes, the probability of two distinct browsers having the exact same timestamp is extremely low, even for a large volume of traffic. This means that the extension timestamp itself can be used as a strong browser identifier by websites to track the browser across websites.

Please keep in mind that extensions are not enabled by default on private modes and are not enabled on all installed profiles. Each browser profile has its own set of installed extensions.

Reading web-accessible resources timestamps

Chromium extensions have the option to expose their resources to websites or other extensions. For example, a password manager extension can expose JavaScript code to fetch the password from the extension and insert it into the password input on the web page.

To expose such files, the extension developer indicates under the web_accessible_resources field in the extension’s manifest the files they would like to expose to external access. Some developers may choose to expose certain resources to all websites and extensions by adding the "<all_urls>" match pattern. For example, the default installed and enabled Google Docs Offline extension manifest.json file contains the following:

  "web_accessible_resources": [ {
      "matches": [ "<all_urls>" ],
      "resources": [ "page_embed_script.js" ]
   } ]

As the manifest.json file indicates, all resources under “resources” are accessible from all websites and extensions trying to access them.

This allows any website opened in Google Chrome with the installed extension to load this file or make an arbitrary request to chrome-extension://ghbmnnjooekpmoecnnnilnnbdlolhkhi/page_embed_script.js URL.

For the exploit, we are going to use the JavaScript Fetch API, which allows a website to create an HTTP request and read response headers. In our case, the response will contain the Last-Modified header, which is going to expose the extension file timestamp. 

extension = "ghbmnnjooekpmoecnnnilnnbdlolhkhi";
resource = "page_embed_script.js";
url = `chrome-extension://${extension}/${resource}`;
  
fetch(url, {method: 'GET'}).then(resp=>{
  console.log(resp.headers.get('last-modified'));
})

Screenshot showing exposed extension file timestamp

This worked! We got the date “Mon, 09 Sep 2024 12:16:31 GMT”.

Where does this date come from? You can view some debug information regarding the extensions if you visit “chrome://extensions-internals/”. This will also tell you where the extension files are located:

Screenshot showing extension files location


In my case, I am using a Mac, so if we visit “/Users/aw/Library/Application Support/Google/Chrome/Profile 2/Extensions/ghbmnnjooekpmoecnnnilnnbdlolhkhi/1.80.1_0”, we can see that the date modified that shows up indeed matches what we received from the script.

(Since I am in GMT+2 and the date in last-modified is GMT there is a two-hour difference):

Screenshot showing date last modified

We can now use this last-modified value as a highly unique identifier to track the browser across the internet.

Investigating the root cause

During our research, we found this behavior is not intended and Chromium developers tried to prevent this ~11 years ago

// Hash the time and make an etag to avoid exposing the exact
// user installation time of the extension.
std::string hash = base::StringPrintf("%"PRId64"", last_modified_time.ToInternalValue());

However, this behavior was broken around 2 years ago when they added Last-Modified header for all files loaded from a disk in a commit in 2022 as a fix for a different issue:

// We add a Last-Modified header to file responses so that our
// implementation of document.lastModified can access it (crbug.com/875299).
head->headers->AddHeader(net::HttpResponseHeaders::kLastModified, base::TimeFormatHTTP(info.last_modified));
client_->OnReceiveResponse(std::move(head), std::move(consumer_handle), std::nullopt);

This issue remains open for exploitation to this day and allows any website or malicious actor to track a browser across websites with high uniqueness.  

Why is this important?

This technique can be used as a reliable third-party tracker to track someone’s activity across multiple websites.

Traditionally, browser fingerprinting is performed using a combination of multiple data points to create a unique browser identifier. In practice, however, a malicious actor needs just one browser weakness that exposes critical information resulting in a by far stronger unique identifier compared to well-known fingerprinting techniques. This can also nullify the time and effort browser vendors spend to make browser APIs fingerprint-resistant, such as adding noise to canvas or limiting JavaScript API access.

Conclusion

Since the Google Docs Offline extension is installed and enabled by default, it is a great method to fingerprint Chromium-based browser users. While this is a powerful method for tracking, there are some limitations: It will only work on desktop browsers, the default extension is only installed on Google Chrome, extensions are not enabled by default on incognito, and different extensions are available in different profiles. 

In our demo, we also try to extract timestamps from popular third-party extensions such as password managers or ad blockers, which increases the uniqueness of a combined identifier even more.

Share this post