Skip to content

Commit

Permalink
Add trusted-prune-inbound-object scriptlet
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamWr committed Nov 7, 2023
1 parent 7c8e93d commit 5389294
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 15 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
17 changes: 13 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 Down
9 changes: 5 additions & 4 deletions src/scriptlets/evaldata-prune.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,21 @@ 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 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
8 changes: 6 additions & 2 deletions src/scriptlets/json-prune.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,16 @@ export function jsonPrune(source, propsToRemove, requiredInitialProps, stack = '
? requiredInitialProps.split(/ +/)
: [];

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 +127,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
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';
145 changes: 145 additions & 0 deletions src/scriptlets/trusted-prune-inbound-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
hit,
matchStackTrace,
getPropertyInChain,
getWildcardPropertyInChain,
logMessage,
isPruningNeeded,
jsonPruner,
// following helpers are needed for helpers above
toRegExp,
getNativeRegexpTest,
shouldAbortInlineOrInjectedScript,
isEmptyObject,
} from '../helpers/index';

/* eslint-disable max-len */
/**
* @scriptlet 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'[inboundObject, [, propsToRemove [, obligatoryProps [, stack]]]])
* ```
*
* - `inboundObject` — required, the name of the function to trap
* - `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')
* ```
*
*
*/
/* eslint-enable max-len */

export function trustedPruneInboundObject(source, inboundObject, propsToRemove, requiredInitialProps, stack = '') {
if (!inboundObject) {
return;
}

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

const { base, prop } = getPropertyInChain(window, inboundObject);
if (!base || !prop || typeof base[prop] !== 'function') {
return;
}

const prunePaths = propsToRemove !== undefined && propsToRemove !== ''
? propsToRemove.split(/ +/)
: [];
const requiredPaths = requiredInitialProps !== undefined && requiredInitialProps !== ''
? requiredInitialProps.split(/ +/)
: [];

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 objecthHandler = {
apply: objectWrapper,
};

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

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

trustedPruneInboundObject.injections = [
hit,
matchStackTrace,
getPropertyInChain,
getWildcardPropertyInChain,
logMessage,
isPruningNeeded,
jsonPruner,
// 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 5389294

Please sign in to comment.