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 { isDeletion, parseCookieName, parseMaxAgeSeconds } from "../cookie-utils";
|
||||||
import type { Detector } from "./detector";
|
import type { Detector } from "./detector";
|
||||||
|
import { isExtensionCaller, isExtensionContext } from "./extension-context";
|
||||||
import { getInitiatorURL } from "./initiator";
|
import { getInitiatorURL } from "./initiator";
|
||||||
import type { ReportQueue } from "./report-queue";
|
import type { ReportQueue } from "./report-queue";
|
||||||
import type { DetectedCookieEntry } from "./types";
|
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 {
|
export class CookieDetector implements Detector {
|
||||||
private readonly queue: ReportQueue;
|
private readonly queue: ReportQueue;
|
||||||
private readonly apiOrigin: string;
|
private readonly apiOrigin: string;
|
||||||
@@ -41,6 +35,8 @@ export class CookieDetector implements Detector {
|
|||||||
start(): void {
|
start(): void {
|
||||||
this.queue.onNotFound(() => this.stop());
|
this.queue.onNotFound(() => this.stop());
|
||||||
|
|
||||||
|
if (isExtensionContext()) return;
|
||||||
|
|
||||||
const desc =
|
const desc =
|
||||||
Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ??
|
Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ??
|
||||||
Object.getOwnPropertyDescriptor(HTMLDocument.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.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import type { Detector } from "./detector";
|
import type { Detector } from "./detector";
|
||||||
|
import { isExtensionCaller, isExtensionContext } from "./extension-context";
|
||||||
import type { ReportQueue } from "./report-queue";
|
import type { ReportQueue } from "./report-queue";
|
||||||
import type { ResourceType } from "./types";
|
import type { ResourceType } from "./types";
|
||||||
|
|
||||||
const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//;
|
|
||||||
|
|
||||||
// Map browser-reported PerformanceResourceTiming.initiatorType to the
|
// Map browser-reported PerformanceResourceTiming.initiatorType to the
|
||||||
// server-side tracker_resource_type. Anything we cannot classify is
|
// server-side tracker_resource_type. Anything we cannot classify is
|
||||||
// dropped rather than reported as "other" to keep the table tidy.
|
// dropped rather than reported as "other" to keep the table tidy.
|
||||||
@@ -71,10 +70,13 @@ export class ResourceDetector implements Detector {
|
|||||||
start(): void {
|
start(): void {
|
||||||
this.queue.onNotFound(() => this.stop());
|
this.queue.onNotFound(() => this.stop());
|
||||||
|
|
||||||
this.scanExisting();
|
|
||||||
this.observeMutations();
|
this.observeMutations();
|
||||||
this.observePerformance();
|
this.observePerformance();
|
||||||
this.wrapServiceWorker();
|
this.wrapServiceWorker();
|
||||||
|
|
||||||
|
if (isExtensionContext()) return;
|
||||||
|
|
||||||
|
this.scanExisting();
|
||||||
this.scanServiceWorkers();
|
this.scanServiceWorkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +207,7 @@ export class ResourceDetector implements Detector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private processResource(src: string, resourceType: ResourceType): void {
|
private processResource(src: string, resourceType: ResourceType): void {
|
||||||
if (EXTENSION_URL_RE.test(src)) return;
|
if (isExtensionCaller()) return;
|
||||||
|
|
||||||
let parsed: URL;
|
let parsed: URL;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,17 +13,12 @@
|
|||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import type { Detector } from "./detector";
|
import type { Detector } from "./detector";
|
||||||
|
import { isExtensionCaller, isExtensionContext } from "./extension-context";
|
||||||
import { getInitiatorURL } from "./initiator";
|
import { getInitiatorURL } from "./initiator";
|
||||||
import type { ReportQueue } from "./report-queue";
|
import type { ReportQueue } from "./report-queue";
|
||||||
import type { DetectedStorageEntry, StorageSource } from "./types";
|
import type { DetectedStorageEntry, StorageSource } from "./types";
|
||||||
|
|
||||||
const OWN_KEY_PREFIX = "probo_consent:";
|
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 {
|
export class StorageDetector implements Detector {
|
||||||
private readonly queue: ReportQueue;
|
private readonly queue: ReportQueue;
|
||||||
@@ -43,6 +38,9 @@ export class StorageDetector implements Detector {
|
|||||||
this.wrapStorage();
|
this.wrapStorage();
|
||||||
this.wrapIndexedDB();
|
this.wrapIndexedDB();
|
||||||
this.wrapCacheStorage();
|
this.wrapCacheStorage();
|
||||||
|
|
||||||
|
if (isExtensionContext()) return;
|
||||||
|
|
||||||
this.scanExisting();
|
this.scanExisting();
|
||||||
this.scanCacheStorage();
|
this.scanCacheStorage();
|
||||||
}
|
}
|
||||||
@@ -90,6 +88,7 @@ export class StorageDetector implements Detector {
|
|||||||
|
|
||||||
IDBFactory.prototype.open = function (name: string, version?: number) {
|
IDBFactory.prototype.open = function (name: string, version?: number) {
|
||||||
const request = originalOpen.call(this, name, version);
|
const request = originalOpen.call(this, name, version);
|
||||||
|
if (isExtensionCaller()) return request;
|
||||||
self.onIndexedDBOpen(name);
|
self.onIndexedDBOpen(name);
|
||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
@@ -104,7 +103,9 @@ export class StorageDetector implements Detector {
|
|||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
caches.open = function (name: string): Promise<Cache> {
|
caches.open = function (name: string): Promise<Cache> {
|
||||||
|
if (!isExtensionCaller()) {
|
||||||
self.onCacheStorageOpen(name, "script");
|
self.onCacheStorageOpen(name, "script");
|
||||||
|
}
|
||||||
return originalOpen(name);
|
return originalOpen(name);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user