Improve compatibility of uritransform= with DNR syntax

The `uritransform=` option will now be converted to a proper DNR
rule when the following condition are fulfilled:

- The value of the `uritransform` option matches `//[replacement]/`,
i.e. the pattern to match is empty, and only the replacement part is
provided.

- The filter pattern is a regex.

Is such case, the DNR rule will be a `redirect` making use of the
`regexSubstitution` property.

In case the above conditions are not fulfilled, the filter will
be discarded as incompatible with DNR syntax (as was the case
before).

This is potentially a breaking change, in cases where a filter
assumed that the part to match was the start of the path part of
a URL.

A reminder that `uritransform` is an option which requires a
trusted source, otherwise it is rejected.
This commit is contained in:
Raymond Hill 2025-08-15 15:55:33 -04:00
parent 25d9964b1e
commit aaf35d9d71
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
5 changed files with 50 additions and 26 deletions

View file

@ -183,7 +183,7 @@ export async function benchmarkStaticNetFiltering(options = {}) {
if ( r === 1 ) { blockCount += 1; }
else if ( r === 2 ) { allowCount += 1; }
if ( r !== 1 ) {
if ( sfne.transformRequest(fctxt) ) {
if ( sfne.transformURL(fctxt) ) {
redirectCount += 1;
}
if ( sfne.hasQuery(fctxt) ) {

View file

@ -1886,7 +1886,9 @@ const onMessage = function(request, sender, callback) {
return {
name: assetKey,
text: details.content,
trustedSource: assetKey.startsWith('ublock-'),
trustedSource: assetKey.startsWith('ublock-') ||
assetKey === µb.userFiltersPath &&
µb.userSettings.userFiltersTrusted,
};
})
);

View file

@ -956,7 +956,7 @@ const PageStore = class {
redirectNonBlockedRequest(fctxt) {
const directives = [];
staticNetFilteringEngine.transformRequest(fctxt, directives);
staticNetFilteringEngine.transformURL(fctxt, directives);
if ( staticNetFilteringEngine.hasQuery(fctxt) ) {
staticNetFilteringEngine.filterQuery(fctxt, directives);
}

View file

@ -841,6 +841,11 @@ export class AstFilterParser {
this.netOptionValueParser = new ArglistParser(',');
this.scriptletArgListParser = new ArglistParser(',');
this.domainRegexValueParser = new ArglistParser('/');
this.reNetOptionTokens = new RegExp(
`^(${Array.from(netOptionTokenDescriptors.keys())
.map(s => escapeForRegex(s))
.join('|')})\\b`
);
}
finish() {
@ -1512,12 +1517,7 @@ export class AstFilterParser {
for (;;) {
const before = s.charAt(j-1);
if ( before === '$' ) { return -1; }
const after = s.charAt(j+1);
if ( ')/|'.includes(after) === false ) {
if ( before === '' || '"\'\\`'.includes(before) === false ) {
return j;
}
}
if ( this.reNetOptionTokens.test(s.slice(j+1)) ) { return j; }
if ( j === start ) { break; }
j = s.lastIndexOf('$', j-1);
if ( j === -1 ) { break; }
@ -3080,9 +3080,10 @@ export function parseReplaceByRegexValue(s) {
if ( parser.transform ) {
pattern = parser.normalizeArg(pattern);
}
if ( pattern === '' ) { return; }
pattern = parser.normalizeArg(pattern, '$');
pattern = parser.normalizeArg(pattern, ',');
if ( pattern !== '' ) {
pattern = parser.normalizeArg(pattern, '$');
pattern = parser.normalizeArg(pattern, ',');
}
parser.nextArg(s, parser.separatorEnd);
let replacement = s.slice(parser.argBeg, parser.argEnd);
if ( parser.separatorEnd === parser.separatorBeg ) { return; }
@ -3092,6 +3093,9 @@ export function parseReplaceByRegexValue(s) {
replacement = parser.normalizeArg(replacement, '$');
replacement = parser.normalizeArg(replacement, ',');
const flags = s.slice(parser.separatorEnd);
if ( pattern === '' ) {
return { flags, replacement }
}
try {
return { re: new RegExp(pattern, flags), replacement };
} catch {
@ -3101,7 +3105,10 @@ export function parseReplaceByRegexValue(s) {
export function parseReplaceValue(s) {
if ( s.startsWith('/') ) {
const r = parseReplaceByRegexValue(s);
if ( r ) { r.type = 'text'; }
if ( r ) {
if ( r.re === undefined ) { return; }
r.type = 'text';
}
return r;
}
const pos = s.indexOf(':');

View file

@ -1231,7 +1231,6 @@ class FilterRegex {
);
}
if ( refs.$re.test($requestURLRaw) === false ) { return false; }
$patternMatchLeft = $requestURLRaw.search(refs.$re);
return true;
}
@ -4783,7 +4782,19 @@ StaticNetFilteringEngine.prototype.dnrFromCompiled = function(op, context, ...ar
break;
}
case 'uritransform': {
dnrAddRuleError(rule, `Incompatible with DNR: uritransform=${rule.__modifierValue}`);
const parsed = sfp.parseReplaceByRegexValue(rule.__modifierValue);
if ( parsed.re !== undefined ) {
dnrAddRuleError(rule, `Incompatible with DNR: uritransform=${rule.__modifierValue}`);
break;
}
if ( rule.condition.regexFilter === undefined ) {
dnrAddRuleError(rule, `Incompatible with DNR (need regexFilter): uritransform=${rule.__modifierValue}`);
break;
}
rule.action.type = 'redirect';
rule.action.redirect = {
regexSubstitution: parsed.replacement.replace(/\$(\d+)/g, '\\$1')
};
break;
}
case 'urlskip': {
@ -5500,7 +5511,7 @@ function compareRedirectRequests(redirectEngine, a, b) {
/******************************************************************************/
StaticNetFilteringEngine.prototype.transformRequest = function(fctxt, out = []) {
StaticNetFilteringEngine.prototype.transformURL = function(fctxt, out = []) {
const directives = this.matchAndFetchModifiers(fctxt, 'uritransform');
if ( directives === undefined ) { return; }
const redirectURL = new URL(fctxt.url);
@ -5514,17 +5525,21 @@ StaticNetFilteringEngine.prototype.transformRequest = function(fctxt, out = [])
}
const cache = directive.cache;
if ( cache === undefined ) { continue; }
const before = `${redirectURL.pathname}${redirectURL.search}${redirectURL.hash}`;
if ( cache.re.test(before) !== true ) { continue; }
const after = before.replace(cache.re, cache.replacement);
let { re } = cache;
const before = redirectURL.href;
if ( re === undefined ) {
const logdata = directive.logData();
if ( logdata === undefined ) { continue; }
try { re = new RegExp(logdata.regex, cache.flags); }
catch { continue; }
}
if ( re.test(before) !== true ) { continue; }
const after = before.replace(re, cache.replacement);
try { void new URL(after); } catch { continue; }
if ( after === before ) { continue; }
const hashPos = after.indexOf('#');
redirectURL.hash = hashPos !== -1 ? after.slice(hashPos) : '';
const afterMinusHash = hashPos !== -1 ? after.slice(0, hashPos) : after;
const searchPos = afterMinusHash.indexOf('?');
redirectURL.search = searchPos !== -1 ? afterMinusHash.slice(searchPos) : '';
redirectURL.pathname = searchPos !== -1 ? after.slice(0, searchPos) : after;
redirectURL.href = after;
out.push(directive);
break;
}
if ( out.length === 0 ) { return; }
if ( redirectURL.href !== fctxt.url ) {
@ -5724,7 +5739,7 @@ StaticNetFilteringEngine.prototype.test = function(details) {
out.push('not blocked');
}
if ( r !== 1 ) {
const entries = this.transformRequest(fctxt);
const entries = this.transformURL(fctxt);
if ( entries ) {
for ( const entry of entries ) {
out.push(`modified: ${entry.logData().raw}`);