We describe in detail the fingerprinting vectors that we study and show how these vectors could be used to fingerprint extensions in the wild.
We study five different behavior exhibited by Chrome and Firefox extensions, as follows:
executeScript
API from
the background, or through the document.createElement
API within the content scripts. The
injected code executes directly into the global JavaScript namespace of the Web page, controlled by the
page itself, unlike the content scripts which has its isolated JavaScript namespace.Due to
this lack of namespace isolation between the injected code and the Web page JavaScript, the extension
could be exposed to being fingerprinted. This is because an adversarial website may overwrite the native
definitions of the global APIs in its own namespace and can thus control their invocation by any
external code that uses them, as follows:
// injected.js
var foo = [1,2,3,4,5];
foo.forEach((element)=>{console.log(element)});
// webpage-script.js
function __hook(object, property, tag) {
let __originalFunc = object[property];
function __customFunc() {
// Extracting API related information.
let context = this;
let args = Array(...arguments);
while (arguments.callee.caller) {
// Extracting the source code of the executing code.
}
// Capturing the stack trace of the executing code.
let stacktrace = new Error().stack;
reportToServer({ api, context, args, stacktrace, callerData });
// Now, returning the result from executing native function.
return __originalFunc.apply(this, arguments);
}
object[property] = __customFunc;
}
__hook(Array.prototype, "forEach", "Array.forEach");
In the above example, when the extension-injected code is executed (i.e. the foo.forEach
invocation), it allows the Web page to capture extesion-specific operation through API overwrites and,
thus, infer the presence of certain extensions.
window
object, this
could still be used to identify the extension who set it, as follows:
// injected.js
window.foo = "bar";
//webpage-script.js
for (let v in Object.getOwnPropertyNames(window)) {
if (isVariableSeen(v) || isVariableSeen(window.v)) {
reportToServer("Target extension is installed!");
}
}
Important: Please note that since the set of default global variables on the window
object may change over time based on different browser vendors and versions, our tests may incorrectly classify an extension as fingerprintable. In such cases, we ask developers to visit our page once without the extension and extract the list of global variables available by default, through Object.getOwnPropertyNames(window)
and compare against the variables detected in our tests, available at window.top.variables
.
document.cookie
API on the client side or even within the headers on the
server-side.
// contentScript.js or injected.js
document.cookie = "extension-foo=bar";
// webpage-script.js
for (let cookie of document.cookie.split(";")) {
if (isCookieSeen(cookie)) {
reportToServer("Target extension is installed!");
}
}
localStorage
, sessionStorage
and IndexedDB
.
// contentScript.js or injected.js
window.localStorage["foo"] = "bar";
window.sessionStorage.bat = "baz";
// webpage-script.js
for (let key of Object.getOwnPropertyNames(window.localStorage)) {
if (isStorageSeen(key) || isStorageSeen(window.localStorage[key])) {
reportToServer("Target extension is installed!");
}
}
for (let key of Object.getOwnPropertyNames(window.sessionStorage)) {
if (isStorageSeen(key) || isStorageSeen(window.sessionStorage[key])) {
reportToServer("Target extension is installed!");
}
}
We recommend developers to either use the chrome.storage
APIs accessible only to the
extension scripts and not the webpage or utilize these APIs in the background context (i.e. in the
background script) which is not accessible to the Web page.
// contentScript.js or injected.js
window.postMessage("extension-specific-data", "*") ;
// webpage-script.js
window.addEventListener("message", function(event) {
if (isMessageSeen(event.data)) {
reportToServer("Target extension is installed!");
}
})