From 19cdd50a1cd0ef984f14673cb6e0022cbe2d1b3a Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 23 May 2023 10:59:27 -0400 Subject: [PATCH] Add `trusted-set-constant` scriptlet Alias: `trusted-set` Behaves exactly like set-constant, except that any arbitrary JSON- compatible value can be set. By default the value is treated as a string, which can be anything. If the value starts with `{` and ends with `}`, the value will be JSON-parsed, and the `value` property of the resulting object will be used. As with any scriptlet requiring trust, filters using `trusted-set-constant` can only come from trusted filter lists, otherwise they are discarded. Related discussion: - https://github.com/uBlockOrigin/uAssets/discussions/18185#discussioncomment-5977456 --- assets/resources/scriptlets.js | 405 ++++++++++++++++++--------------- 1 file changed, 222 insertions(+), 183 deletions(-) diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 77e1fcccb..d781bdaf4 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -157,6 +157,208 @@ function runAt(fn, when) { safe.addEventListener.apply(document, args); } +/******************************************************************************/ + +builtinScriptlets.push({ + name: 'set-constant-core.fn', + fn: setConstantCore, + dependencies: [ + 'safe-self.fn', + ], +}); + +function setConstantCore( + trusted = false, + arg1 = '', + arg2 = '', + arg3 = '' +) { + const details = typeof arg1 !== 'object' + ? { prop: arg1, value: arg2 } + : arg1; + if ( arg3 !== '' ) { + if ( /^\d$/.test(arg3) ) { + details.options = [ arg3 ]; + } else { + details.options = Array.from(arguments).slice(3); + } + } + const { prop: chain = '', value: cValue = '' } = details; + if ( typeof chain !== 'string' ) { return; } + if ( chain === '' ) { return; } + const options = details.options || []; + function setConstant(chain, cValue) { + const trappedProp = (( ) => { + const pos = chain.lastIndexOf('.'); + if ( pos === -1 ) { return chain; } + return chain.slice(pos+1); + })(); + if ( trappedProp === '' ) { return; } + const thisScript = document.currentScript; + const objectDefineProperty = Object.defineProperty.bind(Object); + const cloakFunc = fn => { + objectDefineProperty(fn, 'name', { value: trappedProp }); + const proxy = new Proxy(fn, { + defineProperty(target, prop) { + if ( prop !== 'toString' ) { + return Reflect.deleteProperty(...arguments); + } + return true; + }, + deleteProperty(target, prop) { + if ( prop !== 'toString' ) { + return Reflect.deleteProperty(...arguments); + } + return true; + }, + get(target, prop) { + if ( prop === 'toString' ) { + return function() { + return `function ${trappedProp}() { [native code] }`; + }.bind(null); + } + return Reflect.get(...arguments); + }, + }); + return proxy; + }; + if ( cValue === 'undefined' ) { + cValue = undefined; + } else if ( cValue === 'false' ) { + cValue = false; + } else if ( cValue === 'true' ) { + cValue = true; + } else if ( cValue === 'null' ) { + cValue = null; + } else if ( cValue === "''" ) { + cValue = ''; + } else if ( cValue === '[]' ) { + cValue = []; + } else if ( cValue === '{}' ) { + cValue = {}; + } else if ( cValue === 'noopFunc' ) { + cValue = cloakFunc(function(){}); + } else if ( cValue === 'trueFunc' ) { + cValue = cloakFunc(function(){ return true; }); + } else if ( cValue === 'falseFunc' ) { + cValue = cloakFunc(function(){ return false; }); + } else if ( /^-?\d+$/.test(cValue) ) { + cValue = parseInt(cValue); + if ( isNaN(cValue) ) { return; } + if ( Math.abs(cValue) > 0x7FFF ) { return; } + } else if ( trusted ) { + if ( cValue.startsWith('{') && cValue.endsWith('}') ) { + try { cValue = JSON.parse(cValue).value; } catch(ex) { return; } + } + } else { + return; + } + if ( options.includes('asFunction') ) { + cValue = ( ) => cValue; + } else if ( options.includes('asCallback') ) { + cValue = ( ) => (( ) => cValue); + } else if ( options.includes('asResolved') ) { + cValue = Promise.resolve(cValue); + } else if ( options.includes('asRejected') ) { + cValue = Promise.reject(cValue); + } + let aborted = false; + const mustAbort = function(v) { + if ( trusted ) { return false; } + if ( aborted ) { return true; } + aborted = + (v !== undefined && v !== null) && + (cValue !== undefined && cValue !== null) && + (typeof v !== typeof cValue); + return aborted; + }; + // https://github.com/uBlockOrigin/uBlock-issues/issues/156 + // Support multiple trappers for the same property. + const trapProp = function(owner, prop, configurable, handler) { + if ( handler.init(configurable ? owner[prop] : cValue) === false ) { return; } + const odesc = Object.getOwnPropertyDescriptor(owner, prop); + let prevGetter, prevSetter; + if ( odesc instanceof Object ) { + owner[prop] = cValue; + if ( odesc.get instanceof Function ) { + prevGetter = odesc.get; + } + if ( odesc.set instanceof Function ) { + prevSetter = odesc.set; + } + } + try { + objectDefineProperty(owner, prop, { + configurable, + get() { + if ( prevGetter !== undefined ) { + prevGetter(); + } + return handler.getter(); // cValue + }, + set(a) { + if ( prevSetter !== undefined ) { + prevSetter(a); + } + handler.setter(a); + } + }); + } catch(ex) { + } + }; + const trapChain = function(owner, chain) { + const pos = chain.indexOf('.'); + if ( pos === -1 ) { + trapProp(owner, chain, false, { + v: undefined, + init: function(v) { + if ( mustAbort(v) ) { return false; } + this.v = v; + return true; + }, + getter: function() { + return document.currentScript === thisScript + ? this.v + : cValue; + }, + setter: function(a) { + if ( mustAbort(a) === false ) { return; } + cValue = a; + } + }); + return; + } + const prop = chain.slice(0, pos); + const v = owner[prop]; + chain = chain.slice(pos + 1); + if ( v instanceof Object || typeof v === 'object' && v !== null ) { + trapChain(v, chain); + return; + } + trapProp(owner, prop, true, { + v: undefined, + init: function(v) { + this.v = v; + return true; + }, + getter: function() { + return this.v; + }, + setter: function(a) { + this.v = a; + if ( a instanceof Object ) { + trapChain(a, chain); + } + } + }); + }; + trapChain(window, chain); + } + runAt(( ) => { + setConstant(chain, cValue); + }, options); +} + /******************************************************************************* Injectable scriptlets @@ -1029,193 +1231,13 @@ builtinScriptlets.push({ aliases: [ 'set.js' ], fn: setConstant, dependencies: [ - 'run-at.fn', + 'set-constant-core.fn' ], }); function setConstant( - arg1 = '', - arg2 = '', - arg3 = '' + ...args ) { - const details = typeof arg1 !== 'object' - ? { prop: arg1, value: arg2 } - : arg1; - if ( arg3 !== '' ) { - if ( /^\d$/.test(arg3) ) { - details.options = [ arg3 ]; - } else { - details.options = Array.from(arguments).slice(2); - } - } - const { prop: chain = '', value: cValue = '' } = details; - if ( typeof chain !== 'string' ) { return; } - if ( chain === '' ) { return; } - const options = details.options || []; - function setConstant(chain, cValue) { - const trappedProp = (( ) => { - const pos = chain.lastIndexOf('.'); - if ( pos === -1 ) { return chain; } - return chain.slice(pos+1); - })(); - if ( trappedProp === '' ) { return; } - const thisScript = document.currentScript; - const objectDefineProperty = Object.defineProperty.bind(Object); - const cloakFunc = fn => { - objectDefineProperty(fn, 'name', { value: trappedProp }); - const proxy = new Proxy(fn, { - defineProperty(target, prop) { - if ( prop !== 'toString' ) { - return Reflect.deleteProperty(...arguments); - } - return true; - }, - deleteProperty(target, prop) { - if ( prop !== 'toString' ) { - return Reflect.deleteProperty(...arguments); - } - return true; - }, - get(target, prop) { - if ( prop === 'toString' ) { - return function() { - return `function ${trappedProp}() { [native code] }`; - }.bind(null); - } - return Reflect.get(...arguments); - }, - }); - return proxy; - }; - if ( cValue === 'undefined' ) { - cValue = undefined; - } else if ( cValue === 'false' ) { - cValue = false; - } else if ( cValue === 'true' ) { - cValue = true; - } else if ( cValue === 'null' ) { - cValue = null; - } else if ( cValue === "''" ) { - cValue = ''; - } else if ( cValue === '[]' ) { - cValue = []; - } else if ( cValue === '{}' ) { - cValue = {}; - } else if ( cValue === 'noopFunc' ) { - cValue = cloakFunc(function(){}); - } else if ( cValue === 'trueFunc' ) { - cValue = cloakFunc(function(){ return true; }); - } else if ( cValue === 'falseFunc' ) { - cValue = cloakFunc(function(){ return false; }); - } else if ( /^-?\d+$/.test(cValue) ) { - cValue = parseInt(cValue); - if ( isNaN(cValue) ) { return; } - if ( Math.abs(cValue) > 0x7FFF ) { return; } - } else { - return; - } - if ( options.includes('asFunction') ) { - cValue = ( ) => cValue; - } else if ( options.includes('asCallback') ) { - cValue = ( ) => (( ) => cValue); - } else if ( options.includes('asResolved') ) { - cValue = Promise.resolve(cValue); - } else if ( options.includes('asRejected') ) { - cValue = Promise.reject(cValue); - } - let aborted = false; - const mustAbort = function(v) { - if ( aborted ) { return true; } - aborted = - (v !== undefined && v !== null) && - (cValue !== undefined && cValue !== null) && - (typeof v !== typeof cValue); - return aborted; - }; - // https://github.com/uBlockOrigin/uBlock-issues/issues/156 - // Support multiple trappers for the same property. - const trapProp = function(owner, prop, configurable, handler) { - if ( handler.init(configurable ? owner[prop] : cValue) === false ) { return; } - const odesc = Object.getOwnPropertyDescriptor(owner, prop); - let prevGetter, prevSetter; - if ( odesc instanceof Object ) { - owner[prop] = cValue; - if ( odesc.get instanceof Function ) { - prevGetter = odesc.get; - } - if ( odesc.set instanceof Function ) { - prevSetter = odesc.set; - } - } - try { - objectDefineProperty(owner, prop, { - configurable, - get() { - if ( prevGetter !== undefined ) { - prevGetter(); - } - return handler.getter(); // cValue - }, - set(a) { - if ( prevSetter !== undefined ) { - prevSetter(a); - } - handler.setter(a); - } - }); - } catch(ex) { - } - }; - const trapChain = function(owner, chain) { - const pos = chain.indexOf('.'); - if ( pos === -1 ) { - trapProp(owner, chain, false, { - v: undefined, - init: function(v) { - if ( mustAbort(v) ) { return false; } - this.v = v; - return true; - }, - getter: function() { - return document.currentScript === thisScript - ? this.v - : cValue; - }, - setter: function(a) { - if ( mustAbort(a) === false ) { return; } - cValue = a; - } - }); - return; - } - const prop = chain.slice(0, pos); - const v = owner[prop]; - chain = chain.slice(pos + 1); - if ( v instanceof Object || typeof v === 'object' && v !== null ) { - trapChain(v, chain); - return; - } - trapProp(owner, prop, true, { - v: undefined, - init: function(v) { - this.v = v; - return true; - }, - getter: function() { - return this.v; - }, - setter: function(a) { - this.v = a; - if ( a instanceof Object ) { - trapChain(a, chain); - } - } - }); - }; - trapChain(window, chain); - } - runAt(( ) => { - setConstant(chain, cValue); - }, options); + setConstantCore(false, ...args); } /******************************************************************************/ @@ -2338,3 +2360,20 @@ function sed( } /******************************************************************************/ + +builtinScriptlets.push({ + name: 'trusted-set-constant.js', + requiresTrust: true, + aliases: [ 'trusted-set' ], + fn: trustedSetConstant, + dependencies: [ + 'set-constant-core.fn' + ], +}); +function trustedSetConstant( + ...args +) { + setConstantCore(true, ...args); +} + +/******************************************************************************/