Better detect invalid cosmetic filters (revised)

Related commit:
- 19298fecf3

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/2170
This commit is contained in:
Raymond Hill 2022-07-17 12:42:48 -04:00
parent 1dd37c8181
commit 97befd116b
No known key found for this signature in database
GPG key ID: 25E1490B761470C2

View file

@ -19,7 +19,7 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* globals CSSStyleSheet, document */ /* globals document */
'use strict'; 'use strict';
@ -1332,27 +1332,66 @@ Parser.prototype.SelectorCompiler = class {
[ 'matches-css-after', ':matches-css-after' ], [ 'matches-css-after', ':matches-css-after' ],
[ 'matches-css-before', ':matches-css-before' ], [ 'matches-css-before', ':matches-css-before' ],
]); ]);
this.reSimpleSelector = /^[#.]?[A-Za-z_][\w-]*$/;
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#browser_compatibility // Use a regex for most common CSS selectors known to be valid in any
// Firefox does not support constructor for CSSStyleSheet // context.
this.stylesheet = (( ) => { const cssIdentifier = '[A-Za-z_][\\w-]*';
if ( typeof document !== 'object' ) { return null; } const cssClassOrId = `[.#]${cssIdentifier}`;
if ( document instanceof Object === false ) { return null; } 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 { 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) { } 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 = (( ) => { this.div = (( ) => {
if ( typeof document !== 'object' ) { return null; } if ( typeof document !== 'object' ) { return null; }
if ( document instanceof Object === false ) { return null; } if ( document instanceof Object === false ) { return null; }
return document.createElement('div'); return document.createElement('div');
})(); })();
this.reProceduralOperator = new RegExp([ this.reProceduralOperator = new RegExp([
'^(?:', '^(?:',
Array.from(parser.proceduralOperatorTokens.keys()).join('|'), Array.from(parser.proceduralOperatorTokens.keys()).join('|'),
@ -1363,6 +1402,7 @@ Parser.prototype.SelectorCompiler = class {
this.reDropScope = /^\s*:scope\s*(?=[+>~])/; this.reDropScope = /^\s*:scope\s*(?=[+>~])/;
this.reIsDanglingSelector = /[+>~\s]\s*$/; this.reIsDanglingSelector = /[+>~\s]\s*$/;
this.reIsCombinator = /^\s*[+>~]/; this.reIsCombinator = /^\s*[+>~]/;
this.reUnicodeSpecials = /[\uFFF0-\uFFFF]/;
this.regexToRawValue = new Map(); this.regexToRawValue = new Map();
// https://github.com/gorhill/uBlock/issues/2793 // https://github.com/gorhill/uBlock/issues/2793
this.normalizedOperators = new Map([ this.normalizedOperators = new Map([
@ -1467,26 +1507,25 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/1806#issuecomment-963278382 // https://github.com/uBlockOrigin/uBlock-issues/issues/1806#issuecomment-963278382
// Forbid multiple and unexpected CSS style declarations. // Forbid multiple and unexpected CSS style declarations.
sheetSelectable(s) { sheetSelectable(s) {
if ( this.reSimpleSelector.test(s) ) { return true; } if ( this.reCommonSelector.test(s) ) { return true; }
if ( this.stylesheet === null ) { return true; } const rule = this.sheetValidatorRule;
if ( rule === null ) { return true; }
let valid = false;
try { try {
this.stylesheet.insertRule(`${s}{color:red}`); rule.selectorText = '_z';
if ( this.stylesheet.cssRules.length !== 1 ) { return false; } rule.selectorText = `_z + ${s}`;
const style = this.stylesheet.cssRules[0].style; valid = rule.selectorText !== '_z' &&
if ( style.length !== 1 ) { return false; } this.reUnicodeSpecials.test(rule.selectorText) === false;
if ( style.getPropertyValue('color') !== 'red' ) { return false; }
this.stylesheet.deleteRule(0);
} catch (ex) { } catch (ex) {
return false;
} }
return true; return valid;
} }
// https://github.com/uBlockOrigin/uBlock-issues/issues/1806 // https://github.com/uBlockOrigin/uBlock-issues/issues/1806
// Forbid instances of: // Forbid instances of:
// - opening comment `/*` // - opening comment `/*`
querySelectable(s) { querySelectable(s) {
if ( this.reSimpleSelector.test(s) ) { return true; } if ( this.reCommonSelector.test(s) ) { return true; }
if ( this.div === null ) { return true; } if ( this.div === null ) { return true; }
try { try {
this.div.querySelector(`${s},${s}:not(#foo)`); this.div.querySelector(`${s},${s}:not(#foo)`);
@ -1600,17 +1639,15 @@ Parser.prototype.SelectorCompiler = class {
// - opening comment `/*` // - opening comment `/*`
compileStyleProperties(s) { compileStyleProperties(s) {
if ( /image-set\(|url\(|\/\s*\/|\\|\/\*/i.test(s) ) { return; } if ( /image-set\(|url\(|\/\s*\/|\\|\/\*/i.test(s) ) { return; }
if ( this.stylesheet === null ) { return s; } if ( this.styleValidatorElement === null ) { return s; }
let valid = false; let valid = false;
try { try {
this.stylesheet.insertRule(`a{${s}}`); this.styleValidatorElement.childNodes[0].nodeValue = `_z{${s}} _z{color:red;}`;
const rules = this.stylesheet.cssRules; const rules = this.styleValidatorElement.sheet.cssRules;
valid = rules.length !== 0 && rules[0].style.cssText !== ''; valid = rules.length >= 2 &&
rules[0].style.cssText !== '' &&
rules[1].style.cssText !== '';
} catch(ex) { } catch(ex) {
return;
}
if ( this.stylesheet.cssRules.length !== 0 ) {
this.stylesheet.deleteRule(0);
} }
if ( valid ) { return s; } if ( valid ) { return s; }
} }
@ -1781,8 +1818,12 @@ Parser.prototype.SelectorCompiler = class {
if ( i === n ) { break; } 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. // 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 ( tasks.length === 0 ) {
if ( action === undefined ) { if ( action === undefined ) {
prefix = raw; prefix = raw;
@ -1796,7 +1837,6 @@ Parser.prototype.SelectorCompiler = class {
} }
} else if ( opPrefixBeg < n ) { } else if ( opPrefixBeg < n ) {
if ( action !== undefined ) { return; }
const spath = this.compileSpathExpression(raw.slice(opPrefixBeg)); const spath = this.compileSpathExpression(raw.slice(opPrefixBeg));
if ( spath === undefined ) { return; } if ( spath === undefined ) { return; }
tasks.push([ ':spath', spath ]); tasks.push([ ':spath', spath ]);