Filter extension activity across all cookie banner detectors

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-22 14:48:29 +02:00
parent e2b8ee15e7
commit f1d320361e
4 changed files with 75 additions and 18 deletions

View File

@@ -14,17 +14,11 @@
import { isDeletion, parseCookieName, parseMaxAgeSeconds } from "../cookie-utils";
import type { Detector } from "./detector";
import { isExtensionCaller, isExtensionContext } from "./extension-context";
import { getInitiatorURL } from "./initiator";
import type { ReportQueue } from "./report-queue";
import type { DetectedCookieEntry } from "./types";
const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//;
function isExtensionCaller(): boolean {
const stack = new Error().stack ?? "";
return EXTENSION_URL_RE.test(stack);
}
export class CookieDetector implements Detector {
private readonly queue: ReportQueue;
private readonly apiOrigin: string;
@@ -41,6 +35,8 @@ export class CookieDetector implements Detector {
start(): void {
this.queue.onNotFound(() => this.stop());
if (isExtensionContext()) return;
const desc =
Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ??
Object.getOwnPropertyDescriptor(HTMLDocument.prototype, "cookie");

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
export const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//;
const EXTENSION_PROTOCOLS = new Set([
"chrome-extension:",
"moz-extension:",
"safari-web-extension:",
]);
// isExtensionCaller inspects the synchronous JS call stack for a frame
// whose URL is a browser extension origin. It is a best-effort check:
// some hooks (PerformanceObserver, browser internals firing events from
// their own threads) will not carry the original injector frame, so a
// `false` result does not guarantee the caller is first-party.
export function isExtensionCaller(): boolean {
const stack = new Error().stack ?? "";
return EXTENSION_URL_RE.test(stack);
}
// extensionContext is evaluated once at module load. We capture
// `document.currentScript?.src` here because that reference only
// resolves while the loading script is still executing -- by the time
// a detector's `start()` is called, currentScript will typically be
// null. Computing this lazily would defeat the check.
const extensionContext: boolean = (() => {
if (typeof location !== "undefined" && EXTENSION_PROTOCOLS.has(location.protocol)) {
return true;
}
if (typeof document !== "undefined") {
const src = document.currentScript instanceof HTMLScriptElement
? document.currentScript.src
: null;
if (src && EXTENSION_URL_RE.test(src)) return true;
}
return false;
})();
// isExtensionContext reports whether the SDK itself is being executed
// from inside a browser extension (either an extension page, or a
// script loaded via an extension URL). Detectors use this to skip
// pre-existing-state scans that have no caller stack to inspect and
// would otherwise attribute the extension's own data to the host page.
export function isExtensionContext(): boolean {
return extensionContext;
}

View File

@@ -13,11 +13,10 @@
// PERFORMANCE OF THIS SOFTWARE.
import type { Detector } from "./detector";
import { isExtensionCaller, isExtensionContext } from "./extension-context";
import type { ReportQueue } from "./report-queue";
import type { ResourceType } from "./types";
const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//;
// Map browser-reported PerformanceResourceTiming.initiatorType to the
// server-side tracker_resource_type. Anything we cannot classify is
// dropped rather than reported as "other" to keep the table tidy.
@@ -71,10 +70,13 @@ export class ResourceDetector implements Detector {
start(): void {
this.queue.onNotFound(() => this.stop());
this.scanExisting();
this.observeMutations();
this.observePerformance();
this.wrapServiceWorker();
if (isExtensionContext()) return;
this.scanExisting();
this.scanServiceWorkers();
}
@@ -205,7 +207,7 @@ export class ResourceDetector implements Detector {
}
private processResource(src: string, resourceType: ResourceType): void {
if (EXTENSION_URL_RE.test(src)) return;
if (isExtensionCaller()) return;
let parsed: URL;
try {

View File

@@ -13,17 +13,12 @@
// PERFORMANCE OF THIS SOFTWARE.
import type { Detector } from "./detector";
import { isExtensionCaller, isExtensionContext } from "./extension-context";
import { getInitiatorURL } from "./initiator";
import type { ReportQueue } from "./report-queue";
import type { DetectedStorageEntry, StorageSource } from "./types";
const OWN_KEY_PREFIX = "probo_consent:";
const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//;
function isExtensionCaller(): boolean {
const stack = new Error().stack ?? "";
return EXTENSION_URL_RE.test(stack);
}
export class StorageDetector implements Detector {
private readonly queue: ReportQueue;
@@ -43,6 +38,9 @@ export class StorageDetector implements Detector {
this.wrapStorage();
this.wrapIndexedDB();
this.wrapCacheStorage();
if (isExtensionContext()) return;
this.scanExisting();
this.scanCacheStorage();
}
@@ -90,6 +88,7 @@ export class StorageDetector implements Detector {
IDBFactory.prototype.open = function (name: string, version?: number) {
const request = originalOpen.call(this, name, version);
if (isExtensionCaller()) return request;
self.onIndexedDBOpen(name);
return request;
};
@@ -104,7 +103,9 @@ export class StorageDetector implements Detector {
const self = this;
caches.open = function (name: string): Promise<Cache> {
self.onCacheStorageOpen(name, "script");
if (!isExtensionCaller()) {
self.onCacheStorageOpen(name, "script");
}
return originalOpen(name);
};
}