From 7c8aec250f03580bbd761810bf5bf26d277683b0 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Wed, 12 Jan 2022 10:11:49 -0500 Subject: [PATCH] Prevent highly generic cosmetic filters from affecting html/body elements Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/1692 --- src/js/contentscript.js | 5 ++++ src/js/cosmetic-filtering.js | 45 ++++++++++++++++------------ src/js/scriptlets/cosmetic-logger.js | 6 ++-- src/js/scriptlets/dom-inspector.js | 4 +-- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/js/contentscript.js b/src/js/contentscript.js index f2dcd1467..618ded875 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -676,6 +676,11 @@ vAPI.DOMFilterer = class { getAllExceptionSelectors() { return this.exceptions.join(',\n'); } + + unwrapSelector(s) { + const match = /^:is\((.+)\):not\(html,body\)\/\*hg\*\/$/.exec(s); + return match !== null ? match[1] : s; + } }; /******************************************************************************/ diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 3634a80ef..408a5ecb5 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -227,13 +227,11 @@ const FilterContainer = function() { this.highlyGeneric.simple = { canonical: 'highGenericHideSimple', dict: new Set(), - str: '', mru: new µb.MRUCache(16) }; this.highlyGeneric.complex = { canonical: 'highGenericHideComplex', dict: new Set(), - str: '', mru: new µb.MRUCache(16) }; @@ -281,11 +279,14 @@ FilterContainer.prototype.reset = function() { // highly generic selectors sets this.highlyGeneric.simple.dict.clear(); - this.highlyGeneric.simple.str = ''; this.highlyGeneric.simple.mru.reset(); this.highlyGeneric.complex.dict.clear(); - this.highlyGeneric.complex.str = ''; this.highlyGeneric.complex.mru.reset(); + + // https://developer.mozilla.org/en-US/docs/Web/CSS/:is#browser_compatibility + this.cssIs = + vAPI.webextFlavor.soup.has('firefox') && vAPI.webextFlavor.major >= 84 || + vAPI.webextFlavor.soup.has('chromium') && vAPI.webextFlavor.major >= 88; }; /******************************************************************************/ @@ -300,9 +301,7 @@ FilterContainer.prototype.freeze = function() { this.lowlyGeneric.cl.simple.size !== 0 || this.lowlyGeneric.cl.complex.size !== 0; - this.highlyGeneric.simple.str = Array.from(this.highlyGeneric.simple.dict).join(',\n'); this.highlyGeneric.simple.mru.reset(); - this.highlyGeneric.complex.str = Array.from(this.highlyGeneric.complex.dict).join(',\n'); this.highlyGeneric.complex.mru.reset(); this.frozen = true; @@ -706,9 +705,7 @@ FilterContainer.prototype.fromSelfie = function(selfie) { this.lowlyGeneric.cl.simple = new Set(selfie.lowlyGenericSCL); this.lowlyGeneric.cl.complex = new Map(selfie.lowlyGenericCCL); this.highlyGeneric.simple.dict = new Set(selfie.highSimpleGenericHideArray); - this.highlyGeneric.simple.str = selfie.highSimpleGenericHideArray.join(',\n'); this.highlyGeneric.complex.dict = new Set(selfie.highComplexGenericHideArray); - this.highlyGeneric.complex.str = selfie.highComplexGenericHideArray.join(',\n'); this.needDOMSurveyor = selfie.lowlyGenericSID.length !== 0 || selfie.lowlyGenericCID.length !== 0 || @@ -1042,14 +1039,19 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( // indirectly in the mru cache: this is to prevent duplication of the // string in memory, which I have observed occurs when the string is // stored directly as a value in a Map. + // + // https://github.com/uBlockOrigin/uBlock-issues/issues/1692 + // Wrap generic selectors to prevent matching `html` or `body` + // elements. Content script-side code must unwrap those selectors + // before reporting them in a user interface. if ( options.noGenericCosmeticFiltering !== true ) { const exceptionSetHash = out.exceptionFilters.join(); for ( const key in this.highlyGeneric ) { const entry = this.highlyGeneric[key]; - let str = entry.mru.lookup(exceptionSetHash); - if ( str === undefined ) { - str = { s: entry.str, excepted: [] }; - let genericSet = entry.dict; + let genericSet = entry.dict; + let cache = entry.mru.lookup(exceptionSetHash); + if ( cache === undefined ) { + cache = { s: '', excepted: [] }; let hit = false; for ( const exception of exceptionSet ) { if ( (hit = genericSet.has(exception)) ) { break; } @@ -1058,18 +1060,23 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( genericSet = new Set(entry.dict); for ( const exception of exceptionSet ) { if ( genericSet.delete(exception) ) { - str.excepted.push(exception); + cache.excepted.push(exception); } } - str.s = Array.from(genericSet).join(',\n'); } - entry.mru.add(exceptionSetHash, str); + let genericArr = Array.from(genericSet); + if ( this.cssIs ) { + genericArr = + genericArr.map(a => `:is(${a}):not(html,body)/*hg*/`); + } + cache.s = genericArr.join(',\n'); + entry.mru.add(exceptionSetHash, cache); } - if ( str.excepted.length !== 0 ) { - out.exceptedFilters.push(...str.excepted); + if ( cache.excepted.length !== 0 ) { + out.exceptedFilters.push(...cache.excepted); } - if ( str.s.length !== 0 ) { - injectedHideFilters.push(str.s); + if ( cache.s.length !== 0 ) { + injectedHideFilters.push(cache.s); } } } diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 53a626eed..eae387e66 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -91,6 +91,7 @@ const processDeclarativeSimple = function(node, out) { ) { return; } + const unwrapSelector = vAPI.domFilterer.unwrapSelector; for ( const selector of simpleDeclarativeSet ) { if ( (node === document || safeMatchSelector(selector, node) === false) && @@ -98,7 +99,7 @@ const processDeclarativeSimple = function(node, out) { ) { continue; } - out.push(`##${selector}`); + out.push(`##${unwrapSelector(selector)}`); simpleDeclarativeSet.delete(selector); simpleDeclarativeStr = undefined; loggedSelectors.add(selector); @@ -113,9 +114,10 @@ const processDeclarativeComplex = function(out) { complexDeclarativeStr = safeGroupSelectors(complexDeclarativeSet); } if ( document.querySelector(complexDeclarativeStr) === null ) { return; } + const unwrapSelector = vAPI.domFilterer.unwrapSelector; for ( const selector of complexDeclarativeSet ) { if ( safeQuerySelector(selector) === null ) { continue; } - out.push(`##${selector}`); + out.push(`##${unwrapSelector(selector)}`); complexDeclarativeSet.delete(selector); complexDeclarativeStr = undefined; loggedSelectors.add(selector); diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index c1edb90f4..1cdd35423 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2015-2018 Raymond Hill + Copyright (C) 2015-present Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -509,7 +509,7 @@ const cosmeticFilterMapper = (function() { // Declarative selectors. for ( const entry of (details.declarative || []) ) { for ( const selector of entry[0].split(',\n') ) { - let canonical = selector; + let canonical = vAPI.domFilterer.unwrapSelector(selector); let nodes; if ( entry[1] !== vAPI.hideStyle ) { canonical += ':style(' + entry[1] + ')';