Skip to content

Commit

Permalink
AG-27139, AG-27357 Add new trusted scriptlet — trusted-prune-inbound-…
Browse files Browse the repository at this point in the history
…object. #372, #378

Squashed commit of the following:

commit 5308378
Author: Slava Leleka <v.leleka@adguard.com>
Date:   Mon Nov 13 14:56:26 2023 +0300

    Add indentation

commit 386fb36
Author: Slava Leleka <v.leleka@adguard.com>
Date:   Mon Nov 13 14:56:19 2023 +0300

    Add indentation

commit 3d348db
Author: Adam Wróblewski <adam@adguard.com>
Date:   Mon Nov 13 11:52:54 2023 +0100

    Add logMessage
    Log message if functionName is not a function

commit a826233
Merge: 8e37fe8 9abcbcb
Author: Adam Wróblewski <adam@adguard.com>
Date:   Mon Nov 13 11:51:02 2023 +0100

    Merge branch 'master' into feature/AG-27139

commit 8e37fe8
Author: Adam Wróblewski <adam@adguard.com>
Date:   Mon Nov 13 10:53:29 2023 +0100

    Add getPrunePath helper
    Change inboundObject to functionName

commit b850874
Author: Stanislav Atroschenko <s.atroschenko@adguard.com>
Date:   Mon Nov 13 10:23:16 2023 +0300

    Update description

commit 9f5e76e
Author: Adam Wróblewski <adam@adguard.com>
Date:   Thu Nov 9 11:29:01 2023 +0100

    Fix description

commit d7084d8
Author: Adam Wróblewski <adam@adguard.com>
Date:   Wed Nov 8 18:30:25 2023 +0100

    Add the correct test description
    Add another example to the description

commit 36e3b24
Author: Slava Leleka <v.leleka@adguard.com>
Date:   Wed Nov 8 20:15:03 2023 +0300

    Fix variable name

commit 5c90a46
Author: Slava Leleka <v.leleka@adguard.com>
Date:   Wed Nov 8 20:14:56 2023 +0300

    Fix variable name

commit 7e46411
Author: Slava Leleka <v.leleka@adguard.com>
Date:   Wed Nov 8 20:14:12 2023 +0300

    Update description

commit 3d7f442
Author: Slava Leleka <v.leleka@adguard.com>
Date:   Wed Nov 8 20:13:42 2023 +0300

    Add @added to description

commit 8c63534
Author: Adam Wróblewski <adam@adguard.com>
Date:   Wed Nov 8 11:27:58 2023 +0100

    Remove empty lines
    Add trustedScriptlet to description

commit 5389294
Author: Adam Wróblewski <adam@adguard.com>
Date:   Tue Nov 7 18:34:56 2023 +0100

    Add trusted-prune-inbound-object scriptlet
  • Loading branch information
AdamWr committed Nov 13, 2023
1 parent 9abcbcb commit ad565da
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 27 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- new `trusted-prune-inbound-object` scriptlet [#372](https://github.com/AdguardTeam/Scriptlets/issues/372)
- new values to `set-cookie` scriptlet: `on`, `off`, `accepted`, `notaccepted`, `rejected`, `allowed`,
`disallow`, `enable`, `enabled`, `disable`, `disabled` [#375](https://github.com/AdguardTeam/Scriptlets/issues/375)
- new values to `set-local-storage-item` and `set-session-storage-item` scriptlets: `on`, `off`
[#366](https://github.com/AdguardTeam/Scriptlets/issues/366)

### Fixed

- issue with `stack` in `evaldata-prune` scriptlet [#378](https://github.com/AdguardTeam/Scriptlets/issues/378)
- issue with setting values to wrong properties in `set-constant` scriptlet
[#373](https://github.com/AdguardTeam/Scriptlets/issues/373)

Expand Down
34 changes: 30 additions & 4 deletions src/helpers/prune-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { matchStackTrace } from './match-stack';
* @param root object which should be pruned or logged
* @param prunePaths array with string of space-separated property chains to remove
* @param requiredPaths array with string of space-separated propertiy chains
* @param stack string which should be matched by stack trace
* @param nativeObjects reference to native objects, required for a trusted-prune-inbound-object to fix infinite loop
* which must be all present for the pruning to occur
* @returns true if prunning is required
*/
Expand All @@ -20,22 +22,25 @@ export function isPruningNeeded(
prunePaths: string[],
requiredPaths: string[],
stack: string,
nativeObjects: any,
): boolean | undefined {
if (!root) {
return false;
}

const { nativeStringify } = nativeObjects;

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 rootString = nativeStringify(root);
const matchRegex = toRegExp(requiredPaths.join(''));
const shouldLog = matchRegex.test(rootString);
if (shouldLog) {
logMessage(
source,
`${window.location.hostname}\n${JSON.stringify(root, null, 2)}\nStack trace:\n${new Error().stack}`,
`${window.location.hostname}\n${nativeStringify(root, null, 2)}\nStack trace:\n${new Error().stack}`,
true,
);
if (root && typeof root === 'object') {
Expand Down Expand Up @@ -95,6 +100,8 @@ export function isPruningNeeded(
* @param root object which should be pruned or logged
* @param prunePaths array with string of space-separated properties to remove
* @param requiredPaths array with string of space-separated properties
* @param stack string which should be matched by stack trace
* @param nativeObjects reference to native objects, required for a trusted-prune-inbound-object to fix infinite loop
* which must be all present for the pruning to occur
* @returns pruned root
*/
Expand All @@ -104,11 +111,13 @@ export const jsonPruner = (
prunePaths: string[],
requiredPaths: string[],
stack: string,
nativeObjects: any,
): ArbitraryObject => {
const { nativeStringify } = nativeObjects;
if (prunePaths.length === 0 && requiredPaths.length === 0) {
logMessage(
source,
`${window.location.hostname}\n${JSON.stringify(root, null, 2)}\nStack trace:\n${new Error().stack}`,
`${window.location.hostname}\n${nativeStringify(root, null, 2)}\nStack trace:\n${new Error().stack}`,
true,
);
if (root && typeof root === 'object') {
Expand All @@ -118,7 +127,7 @@ export const jsonPruner = (
}

try {
if (isPruningNeeded(source, root, prunePaths, requiredPaths, stack) === false) {
if (isPruningNeeded(source, root, prunePaths, requiredPaths, stack, nativeObjects) === false) {
return root;
}

Expand All @@ -139,3 +148,20 @@ export const jsonPruner = (

return root;
};

/**
* Checks if props is a string and returns array of properties
* or empty array if props is not a string
*
* @param props string of space-separated properties or undefined
* @returns array of properties or empty array if props is not a string
*/
export const getPrunePath = (props: unknown) => {
const validPropsString = typeof props === 'string'
&& props !== undefined
&& props !== '';

return validPropsString
? props.split(/ +/)
: [];
};
19 changes: 9 additions & 10 deletions src/scriptlets/evaldata-prune.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
toRegExp,
isPruningNeeded,
jsonPruner,
getPrunePath,
// following helpers are needed for helpers above
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
Expand Down Expand Up @@ -96,20 +97,17 @@ import {
*/
/* eslint-enable max-len */
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(/ +/)
: [];
const prunePaths = getPrunePath(propsToRemove);
const requiredPaths = getPrunePath(requiredInitialProps);

const nativeObjects = {
nativeStringify: window.JSON.stringify,
};

const evalWrapper = (target, thisArg, args) => {
let data = Reflect.apply(target, thisArg, args);
if (typeof data === 'object') {
data = jsonPruner(source, data, prunePaths, requiredPaths);
data = jsonPruner(source, data, prunePaths, requiredPaths, stack, nativeObjects);
}
return data;
};
Expand Down Expand Up @@ -137,6 +135,7 @@ evalDataPrune.injections = [
toRegExp,
isPruningNeeded,
jsonPruner,
getPrunePath,
// following helpers are needed for helpers above
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
Expand Down
18 changes: 10 additions & 8 deletions src/scriptlets/json-prune.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
logMessage,
isPruningNeeded,
jsonPruner,
getPrunePath,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
Expand Down Expand Up @@ -99,19 +100,19 @@ import {
*/
/* eslint-enable max-len */
export function jsonPrune(source, propsToRemove, requiredInitialProps, stack = '') {
const prunePaths = propsToRemove !== undefined && propsToRemove !== ''
? propsToRemove.split(/ +/)
: [];
const requiredPaths = requiredInitialProps !== undefined && requiredInitialProps !== ''
? requiredInitialProps.split(/ +/)
: [];
const prunePaths = getPrunePath(propsToRemove);
const requiredPaths = getPrunePath(requiredInitialProps);

const nativeObjects = {
nativeStringify: window.JSON.stringify,
};

const nativeJSONParse = JSON.parse;
const jsonParseWrapper = (...args) => {
// dealing with stringified json in args, which should be parsed.
// so we call nativeJSONParse as JSON.parse which is bound to JSON object
const root = nativeJSONParse.apply(JSON, args);
return jsonPruner(source, root, prunePaths, requiredPaths, stack);
return jsonPruner(source, root, prunePaths, requiredPaths, stack, nativeObjects);
};

// JSON.parse mocking
Expand All @@ -123,7 +124,7 @@ export function jsonPrune(source, propsToRemove, requiredInitialProps, stack = '
const responseJsonWrapper = function () {
const promise = nativeResponseJson.apply(this);
return promise.then((obj) => {
return jsonPruner(source, obj, prunePaths, requiredPaths, stack);
return jsonPruner(source, obj, prunePaths, requiredPaths, stack, nativeObjects);
});
};

Expand Down Expand Up @@ -152,6 +153,7 @@ jsonPrune.injections = [
logMessage,
isPruningNeeded,
jsonPruner,
getPrunePath,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
Expand Down
1 change: 1 addition & 0 deletions src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ export * from './inject-css-in-shadow-dom';
export * from './remove-node-text';
export * from './trusted-replace-node-text';
export * from './evaldata-prune';
export * from './trusted-prune-inbound-object';
151 changes: 151 additions & 0 deletions src/scriptlets/trusted-prune-inbound-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
hit,
matchStackTrace,
getPropertyInChain,
getWildcardPropertyInChain,
logMessage,
isPruningNeeded,
jsonPruner,
getPrunePath,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
isEmptyObject,
} from '../helpers/index';

/* eslint-disable max-len */
/**
* @trustedScriptlet trusted-prune-inbound-object
*
* @description
* Removes listed properties from the result of calling specific function (if payload contains `Object`)
* and returns to the caller.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/commit/1c9da227d7
*
* ### Syntax
*
* ```text
* example.org#%#//scriptlet('trusted-prune-inbound-object', functionName[, propsToRemove [, obligatoryProps [, stack]]])
* ```
*
* - `functionName` — required, the name of the function to trap, it must have an object as an argument
* - `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 Object.getOwnPropertyNames call
*
* ```adblock
* example.org#%#//scriptlet('trusted-prune-inbound-object', 'Object.getOwnPropertyNames', 'example')
* ```
*
* For instance, the following call will return `['one']`
*
* ```html
* Object.getOwnPropertyNames({ one: 1, example: true })
* ```
*
* 2. Removes property `ads` from the payload of the Object.keys call
*
* ```adblock
* example.org#%#//scriptlet('trusted-prune-inbound-object', 'Object.keys', 'ads')
* ```
*
* For instance, the following call will return `['one', 'two']`
*
* ```html
* Object.keys({ one: 1, two: 2, ads: true })
* ```
*
* 3. Removes property `foo.bar` from the payload of the JSON.stringify call
*
* ```adblock
* example.org#%#//scriptlet('trusted-prune-inbound-object', 'JSON.stringify', 'foo.bar')
* ```
*
* For instance, the following call will return `'{"foo":{"a":2},"b":3}'`
*
* ```html
* JSON.stringify({ foo: { bar: 1, a: 2 }, b: 3 })
* ```
*
* 4. Removes property `foo.bar` from the payload of the JSON.stringify call if its error stack trace contains `test.js`
*
* ```adblock
* example.org#%#//scriptlet('trusted-prune-inbound-object', 'JSON.stringify', 'foo.bar', '', 'test.js')
* ```
*
* 5. Call with only first and third argument will log the current hostname and matched payload at the console
*
* ```adblock
* example.org#%#//scriptlet('trusted-prune-inbound-object', 'JSON.stringify', '', 'bar', '')
* ```
*
* @added unknown.
*/
/* eslint-enable max-len */
export function trustedPruneInboundObject(source, functionName, propsToRemove, requiredInitialProps, stack = '') {
if (!functionName) {
return;
}

const nativeObjects = {
nativeStringify: window.JSON.stringify,
};

const { base, prop } = getPropertyInChain(window, functionName);
if (!base || !prop || typeof base[prop] !== 'function') {
const message = `${functionName} is not a function`;
logMessage(source, message);
return;
}

const prunePaths = getPrunePath(propsToRemove);
const requiredPaths = getPrunePath(requiredInitialProps);

const objectWrapper = (target, thisArg, args) => {
let data = args[0];
if (typeof data === 'object') {
data = jsonPruner(source, data, prunePaths, requiredPaths, stack, nativeObjects);
args[0] = data;
}
return Reflect.apply(target, thisArg, args);
};

const objectHandler = {
apply: objectWrapper,
};

base[prop] = new Proxy(base[prop], objectHandler);
}

trustedPruneInboundObject.names = [
'trusted-prune-inbound-object',
// trusted scriptlets support no aliases
];

trustedPruneInboundObject.injections = [
hit,
matchStackTrace,
getPropertyInChain,
getWildcardPropertyInChain,
logMessage,
isPruningNeeded,
jsonPruner,
getPrunePath,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
isEmptyObject,
];
18 changes: 13 additions & 5 deletions tests/scriptlets/evaldata-prune.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,26 @@ test('does NOT remove propsToRemove if invoked without parameter propsToRemove a
});

test('removes propsToRemove + stack match', (assert) => {
const stackMatch = 'evaldata-prune';
const firstStackMatch = 'testForFirstStackMatch';
const secondStackMatch = 'testForSecondStackMatch';

runScriptlet(name, ['c', '', firstStackMatch]);

const testForFirstStackMatch = () => eval({ a: 1, b: 2, c: 3 });
const firstResult = testForFirstStackMatch();

runScriptlet(name, ['c', '', stackMatch]);
assert.deepEqual(
eval({ a: 1, b: 2, c: 3 }),
firstResult,
{ a: 1, b: 2 },
'stack match: should remove single propsToRemove',
);

runScriptlet(name, ['nested.c nested.b', '', stackMatch]);
runScriptlet(name, ['nested.c nested.b', '', secondStackMatch]);
const testForSecondStackMatch = () => eval({ nested: { a: 1, b: 2, c: 3 } });
const secondResult = testForSecondStackMatch();

assert.deepEqual(
eval({ nested: { a: 1, b: 2, c: 3 } }),
secondResult,
{ nested: { a: 1 } },
'stack match: should remove multiple nested propsToRemove',
);
Expand Down
1 change: 1 addition & 0 deletions tests/scriptlets/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ import './trusted-set-constant.test';
import './inject-css-in-shadow-dom.test';
import './remove-node-text.test';
import './trusted-replace-node-text.test';
import './trusted-prune-inbound-object.test';
Loading

0 comments on commit ad565da

Please sign in to comment.