Mark extension <link> writes before rel-based classification
handleAttributeMutation returned early when resourceTypeForElement
yielded null, which happens for `<link href>` whenever `rel` is not yet
"stylesheet". An extension that called `link.setAttribute("href", ...)`
before setting `rel` therefore skipped extensionElements and
extensionUrls marking; when the rel was filled in later and the browser
fetched the stylesheet, PerformanceObserver -- whose stack carries no
extension frame -- reported it as a page tracker.
Check isExtensionCaller() before classification and tag the element and
URL whenever the element type can ever initiate a load via the given
attribute, using a new couldLoadResource helper. The page-caller path
still uses the strict resourceTypeForElement so non-stylesheet `<link>`
writes do not generate spurious reports.
Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -523,17 +523,54 @@ export class ResourceDetector implements Detector {
|
||||
): void {
|
||||
if (typeof value !== "string" || value === "") return;
|
||||
|
||||
const rt = this.resourceTypeForElement(el, attrName);
|
||||
if (rt === null) return;
|
||||
|
||||
// Extension marking runs before classification because the
|
||||
// current attribute alone is not always enough to know whether a
|
||||
// load will happen. The canonical case is `<link>`: a stylesheet
|
||||
// load is only triggered once both `href` and `rel="stylesheet"`
|
||||
// are set, in any order. If the extension sets `href` first we
|
||||
// would otherwise miss tagging, and the eventual PerformanceObserver
|
||||
// entry -- which has no extension frame on its stack -- would leak
|
||||
// through as a page tracker. The same holds for `<link rel=preload>`
|
||||
// and any future rel value that initiates a load.
|
||||
if (isExtensionCaller()) {
|
||||
this.markExtension(el, this.identifierOf(value));
|
||||
if (this.couldLoadResource(el, attrName)) {
|
||||
this.markExtension(el, this.identifierOf(value));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const rt = this.resourceTypeForElement(el, attrName);
|
||||
if (rt === null) return;
|
||||
|
||||
this.processResource(value, rt);
|
||||
}
|
||||
|
||||
// couldLoadResource reports whether `el` is an element type that can
|
||||
// ever initiate a network load via `attrName`, regardless of any
|
||||
// other attributes that may or may not be set yet. It is a superset
|
||||
// of resourceTypeForElement: it returns true for `<link href>` even
|
||||
// when `rel` has not been set, because the rel may change later and
|
||||
// turn the href into a real load.
|
||||
private couldLoadResource(el: Element, attrName: string): boolean {
|
||||
if (attrName === "src") {
|
||||
return (
|
||||
el instanceof HTMLScriptElement
|
||||
|| el instanceof HTMLIFrameElement
|
||||
|| el instanceof HTMLImageElement
|
||||
|| (typeof HTMLSourceElement !== "undefined" && el instanceof HTMLSourceElement)
|
||||
|| (typeof HTMLEmbedElement !== "undefined" && el instanceof HTMLEmbedElement)
|
||||
|| (typeof HTMLMediaElement !== "undefined" && el instanceof HTMLMediaElement)
|
||||
);
|
||||
}
|
||||
if (attrName === "href") {
|
||||
return el instanceof HTMLLinkElement;
|
||||
}
|
||||
if (attrName === "data") {
|
||||
return typeof HTMLObjectElement !== "undefined" && el instanceof HTMLObjectElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private resourceTypeForElement(el: Element, attrName: string): ResourceType | null {
|
||||
if (attrName === "src") {
|
||||
if (el instanceof HTMLScriptElement) return "script";
|
||||
|
||||
Reference in New Issue
Block a user