Filter extension activity across all cookie banner detectors
Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -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");
|
||||
|
||||
58
packages/cookie-banner/src/detectors/extension-context.ts
Normal file
58
packages/cookie-banner/src/detectors/extension-context.ts
Normal 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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user