From 97befd116b75d1f768b8e663b90a15bb25fda4e9 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 17 Jul 2022 12:42:48 -0400 Subject: [PATCH] Better detect invalid cosmetic filters (revised) Related commit: - https://github.com/gorhill/uBlock/commit/19298fecf3311f377f87567905871e978e2b2fb2 Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/2170 --- src/js/static-filtering-parser.js | 108 ++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 34 deletions(-) diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 9932758a3..7dfb95168 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -19,7 +19,7 @@ Home: https://github.com/gorhill/uBlock */ -/* globals CSSStyleSheet, document */ +/* globals document */ 'use strict'; @@ -1332,27 +1332,66 @@ Parser.prototype.SelectorCompiler = class { [ 'matches-css-after', ':matches-css-after' ], [ 'matches-css-before', ':matches-css-before' ], ]); - this.reSimpleSelector = /^[#.]?[A-Za-z_][\w-]*$/; - // https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#browser_compatibility - // Firefox does not support constructor for CSSStyleSheet - this.stylesheet = (( ) => { - if ( typeof document !== 'object' ) { return null; } - if ( document instanceof Object === false ) { return null; } + + // Use a regex for most common CSS selectors known to be valid in any + // context. + const cssIdentifier = '[A-Za-z_][\\w-]*'; + const cssClassOrId = `[.#]${cssIdentifier}`; + const cssAttribute = `\\[${cssIdentifier}[*^$]?="[^"\\]\\\\]+"\\]`; + const cssSimple = + '(?:' + + `${cssIdentifier}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' + + `${cssClassOrId}(?:${cssClassOrId})*(?:${cssAttribute})*` + '|' + + `${cssAttribute}(?:${cssAttribute})*` + + ')'; + const cssCombinator = '(?:\\s+|\\s*[>+~]\\s*)'; + this.reCommonSelector = new RegExp( + `^${cssSimple}(?:${cssCombinator}${cssSimple})*$` + ); + // Resulting regex literal: + // ^(?:[A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\])*|[.#][A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\])*|\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\](?:\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\])*)(?:(?:\s+|\s*[>+~]\s*)(?:[A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\])*|[.#][A-Za-z_][\w-]*(?:[.#][A-Za-z_][\w-]*)*(?:\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\])*|\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\](?:\[[A-Za-z_][\w-]*[*^$]?="[^"\]\\]+"\])*))*$ + + // We use an actual stylesheet to validate uncommon CSS selectors + // which can be used in a CSS declaration. + this.sheetValidatorRule = null; + (( ) => { + if ( typeof document !== 'object' ) { return; } + if ( document === null ) { return; } try { - return new CSSStyleSheet(); + const styleElement = document.createElement('style'); + document.body.append(styleElement); + const sheet = styleElement.sheet; + styleElement.remove(); + sheet.insertRule('_z{color:red}'); + const rule = sheet.cssRules[0]; + this.sheetValidatorRule = rule; } catch(ex) { } - const style = document.createElement('style'); - document.body.append(style); - const stylesheet = style.sheet; - style.remove(); - return stylesheet; })(); + + // We use another stylsheet to validate CSS properties which can be + // used in a CSS declaration. + this.styleValidatorElement = null; + (( ) => { + if ( typeof document !== 'object' ) { return; } + if ( document === null ) { return; } + try { + const styleElement = document.createElement('style'); + styleElement.appendChild(document.createTextNode(' ')); + document.body.append(styleElement); + this.styleValidatorElement = styleElement; + } catch(ex) { + } + })(); + + // We use an HTML element to validate selectors which are + // querySelector-able. this.div = (( ) => { if ( typeof document !== 'object' ) { return null; } if ( document instanceof Object === false ) { return null; } return document.createElement('div'); })(); + this.reProceduralOperator = new RegExp([ '^(?:', Array.from(parser.proceduralOperatorTokens.keys()).join('|'), @@ -1363,6 +1402,7 @@ Parser.prototype.SelectorCompiler = class { this.reDropScope = /^\s*:scope\s*(?=[+>~])/; this.reIsDanglingSelector = /[+>~\s]\s*$/; this.reIsCombinator = /^\s*[+>~]/; + this.reUnicodeSpecials = /[\uFFF0-\uFFFF]/; this.regexToRawValue = new Map(); // https://github.com/gorhill/uBlock/issues/2793 this.normalizedOperators = new Map([ @@ -1467,26 +1507,25 @@ Parser.prototype.SelectorCompiler = class { // https://github.com/uBlockOrigin/uBlock-issues/issues/1806#issuecomment-963278382 // Forbid multiple and unexpected CSS style declarations. sheetSelectable(s) { - if ( this.reSimpleSelector.test(s) ) { return true; } - if ( this.stylesheet === null ) { return true; } + if ( this.reCommonSelector.test(s) ) { return true; } + const rule = this.sheetValidatorRule; + if ( rule === null ) { return true; } + let valid = false; try { - this.stylesheet.insertRule(`${s}{color:red}`); - if ( this.stylesheet.cssRules.length !== 1 ) { return false; } - const style = this.stylesheet.cssRules[0].style; - if ( style.length !== 1 ) { return false; } - if ( style.getPropertyValue('color') !== 'red' ) { return false; } - this.stylesheet.deleteRule(0); + rule.selectorText = '_z'; + rule.selectorText = `_z + ${s}`; + valid = rule.selectorText !== '_z' && + this.reUnicodeSpecials.test(rule.selectorText) === false; } catch (ex) { - return false; } - return true; + return valid; } // https://github.com/uBlockOrigin/uBlock-issues/issues/1806 // Forbid instances of: // - opening comment `/*` querySelectable(s) { - if ( this.reSimpleSelector.test(s) ) { return true; } + if ( this.reCommonSelector.test(s) ) { return true; } if ( this.div === null ) { return true; } try { this.div.querySelector(`${s},${s}:not(#foo)`); @@ -1600,17 +1639,15 @@ Parser.prototype.SelectorCompiler = class { // - opening comment `/*` compileStyleProperties(s) { if ( /image-set\(|url\(|\/\s*\/|\\|\/\*/i.test(s) ) { return; } - if ( this.stylesheet === null ) { return s; } + if ( this.styleValidatorElement === null ) { return s; } let valid = false; try { - this.stylesheet.insertRule(`a{${s}}`); - const rules = this.stylesheet.cssRules; - valid = rules.length !== 0 && rules[0].style.cssText !== ''; + this.styleValidatorElement.childNodes[0].nodeValue = `_z{${s}} _z{color:red;}`; + const rules = this.styleValidatorElement.sheet.cssRules; + valid = rules.length >= 2 && + rules[0].style.cssText !== '' && + rules[1].style.cssText !== ''; } catch(ex) { - return; - } - if ( this.stylesheet.cssRules.length !== 0 ) { - this.stylesheet.deleteRule(0); } if ( valid ) { return s; } } @@ -1781,8 +1818,12 @@ Parser.prototype.SelectorCompiler = class { if ( i === n ) { break; } } + // When there is an explicit action, nothing should be left to parse. + if ( action !== undefined && opPrefixBeg < n ) { return; } + // No task found: then we have a CSS selector. - // At least one task found: nothing should be left to parse. + // At least one task found: either nothing or a valid plain CSS selector + // should be left to parse if ( tasks.length === 0 ) { if ( action === undefined ) { prefix = raw; @@ -1796,7 +1837,6 @@ Parser.prototype.SelectorCompiler = class { } } else if ( opPrefixBeg < n ) { - if ( action !== undefined ) { return; } const spath = this.compileSpathExpression(raw.slice(opPrefixBeg)); if ( spath === undefined ) { return; } tasks.push([ ':spath', spath ]);