Skip to content

Commit

Permalink
Add evaldata-prune scriptlet
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamWr committed May 31, 2023
1 parent 17f6f14 commit 5990342
Show file tree
Hide file tree
Showing 4 changed files with 424 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- new `evaldata-prune` scriptlet [#322](https://github.com/AdguardTeam/Scriptlets/issues/322)
- ability for `prevent-element-src-loading` scriptlet to prevent inline `onerror`
and match `link` tag [#276](https://github.com/AdguardTeam/Scriptlets/issues/276)
- new special value modifiers for `set-constant` [#316](https://github.com/AdguardTeam/Scriptlets/issues/316)
Expand Down
217 changes: 217 additions & 0 deletions src/scriptlets/evaldata-prune.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import {
hit,
matchStackTrace,
getWildcardPropertyInChain,
logMessage,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
} from '../helpers/index';

/**
* @scriptlet evaldata-prune
*
* @description
* Removes specified properties from the result of calling eval (if payloads contains `Object`) and returns the caller.
*
* ### Syntax
*
* ```text
* example.org#%#//scriptlet('evaldata-prune'[, propsToRemove [, obligatoryProps [, stack]]])
* ```
*
* - `propsToRemove` — optional, string of space-separated properties to remove
* - `obligatoryProps` — optional, string of space-separated properties
* which must be all present for the pruning to occur
* - `stack` — optional, string or regular expression that must match the current function call stack trace;
* if regular expression is invalid it will be skipped
*
* > Note please that you can use wildcard `*` for chain property name,
* > e.g. `ad.*.src` instead of `ad.0.src ad.1.src ad.2.src`.
*
* ### Examples
*
* 1. Removes property `example` from the payload of the eval call
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'example')
* ```
*
* For instance, the following call will return `{ one: 1}`
*
* ```html
* eval({ one: 1, example: true })
* ```
*
* 2. If there are no specified properties in the payload of eval call, pruning will NOT occur
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'one', 'obligatoryProp')
* ```
*
* For instance, the following call will return `{ one: 1, two: 2}`
*
* ```html
* JSON.parse('{"one":1,"two":2}')
* ```
*
* 3. A property in a list of properties can be a chain of properties
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'a.b', 'ads.url.first')
* ```
*
* 4. Removes property `content.ad` from the payload of eval call if its error stack trace contains `test.js`
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'content.ad', '', 'test.js')
* ```
*
* 5. A property in a list of properties can be a chain of properties with wildcard in it
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', 'content.*.media.src', 'content.*.media.ad')
* ```
*
* 6. Call with no arguments will log the current hostname and object payload at the console
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune')
* ```
*
* 7. Call with only second argument will log the current hostname and matched object payload at the console
*
* ```adblock
* example.org#%#//scriptlet('evaldata-prune', '', '"id":"117458"')
* ```
*
*/
export function evalDataPrune(source, propsToRemove, requiredInitialProps, stack) {
if (!!stack && !matchStackTrace(stack, new Error().stack)) {
return;
}
const prunePaths = propsToRemove !== undefined && propsToRemove !== ''
? propsToRemove.split(/ +/)
: [];
const requiredPaths = requiredInitialProps !== undefined && requiredInitialProps !== ''
? requiredInitialProps.split(/ +/)
: [];

function isPruningNeeded(root) {
if (!root) {
return false;
}

let shouldProcess;

// Only log hostname and matched JSON payload if only second argument is present
if (prunePaths.length === 0 && requiredPaths.length > 0) {
const rootString = JSON.stringify(root);
const matchRegex = toRegExp(requiredPaths.join(''));
const shouldLog = matchRegex.test(rootString);
if (shouldLog) {
logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true);
if (root && typeof root === 'object') {
logMessage(source, root, true, false);
}
shouldProcess = false;
return shouldProcess;
}
}

for (let i = 0; i < requiredPaths.length; i += 1) {
const requiredPath = requiredPaths[i];
const lastNestedPropName = requiredPath.split('.').pop();

const wildcardSymbols = ['.*.', '*.', '.*', '.[].', '[].', '.[]'];
const hasWildcard = wildcardSymbols.some((symbol) => requiredPath.includes(symbol));

// if the path has wildcard, getPropertyInChain should 'look through' chain props
const details = getWildcardPropertyInChain(root, requiredPath, hasWildcard);

// start value of 'shouldProcess' due to checking below
shouldProcess = !hasWildcard;

for (let i = 0; i < details.length; i += 1) {
if (hasWildcard) {
// if there is a wildcard,
// at least one (||) of props chain should be present in object
shouldProcess = !(details[i].base[lastNestedPropName] === undefined)
|| shouldProcess;
} else {
// otherwise each one (&&) of them should be there
shouldProcess = !(details[i].base[lastNestedPropName] === undefined)
&& shouldProcess;
}
}
}

return shouldProcess;
}

const jsonPruner = (root) => {
if (prunePaths.length === 0 && requiredPaths.length === 0) {
logMessage(source, `${window.location.hostname}\n${JSON.stringify(root, null, 2)}`, true);
if (root && typeof root === 'object') {
logMessage(source, root, true, false);
}
return root;
}

try {
if (isPruningNeeded(root) === false) {
return root;
}

// if pruning is needed, we check every input pathToRemove
// and delete it if root has it
prunePaths.forEach((path) => {
const ownerObjArr = getWildcardPropertyInChain(root, path, true);
ownerObjArr.forEach((ownerObj) => {
if (ownerObj !== undefined && ownerObj.base) {
delete ownerObj.base[ownerObj.prop];
hit(source);
}
});
});
} catch (e) {
logMessage(source, e);
}

return root;
};

const evalWrapper = (target, thisArg, args) => {
let data = Reflect.apply(target, thisArg, args);
if (typeof data === 'object') {
data = jsonPruner(data, propsToRemove, requiredInitialProps);
}
return data;
};

const evalHandler = {
apply: evalWrapper,
};
// eslint-disable-next-line no-eval
window.eval = new Proxy(window.eval, evalHandler);
}

evalDataPrune.names = [
'evaldata-prune',
// aliases are needed for matching the related scriptlet converted into our syntax
'evaldata-prune.js',
'ubo-evaldata-prune.js',
'ubo-evaldata-prune',
];

evalDataPrune.injections = [
hit,
matchStackTrace,
getWildcardPropertyInChain,
logMessage,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
];
1 change: 1 addition & 0 deletions src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ export * from './trusted-replace-fetch-response';
export * from './trusted-set-local-storage-item';
export * from './trusted-set-constant';
export * from './inject-css-in-shadow-dom';
export * from './evaldata-prune';
Loading

0 comments on commit 5990342

Please sign in to comment.