Skip to content

Commit

Permalink
add prevent-element-src-loading scriptlet #180 AG-12328
Browse files Browse the repository at this point in the history
Merge in ADGUARD-FILTERS/scriptlets from feature/AG-12328 to release/v1.6

Squashed commit of the following:

commit 60a6328
Merge: a5d38bc 9af886f
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Feb 9 19:51:52 2022 +0300

    Merge branch 'release/v1.6' into feature/AG-12328

commit a5d38bc
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Feb 9 16:37:05 2022 +0300

    organize global vars for tests

commit dff7139
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Feb 9 14:58:17 2022 +0300

    fix helper doc

commit 87b7eb8
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Feb 9 14:56:46 2022 +0300

    add another falsy argument case

commit f1cc121
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Wed Feb 9 14:52:19 2022 +0300

    add mismatch and falsy arguments for setAttribute test

commit a1f06f7
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 19:30:35 2022 +0300

    move 'src' to a constant

commit a8ab0ad
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 19:18:05 2022 +0300

    change argument order in tests

commit d9591fd
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 19:09:41 2022 +0300

    fix arguments checking for setAttribute wrapper

commit 70ba79a
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 18:49:26 2022 +0300

    fix typo & description

commit 1e1f8f4
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 18:45:35 2022 +0300

    improve matching

commit d861485
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 18:15:44 2022 +0300

    fix description & swap 'search' param to 'match'

commit a7b1c22
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 17:22:38 2022 +0300

    revert wiki about-redirects

commit 1a92d7a
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 17:13:35 2022 +0300

    rename scriptlet to prevent-element-src-loading

commit daa6c1e
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 15:55:39 2022 +0300

    add proxy and reflect check

commit 3a45178
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 15:53:20 2022 +0300

    remove excessive assertions

commit 36c71c2
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 15:51:40 2022 +0300

    fix incorrect TrustedType checking

commit 23f7ccc
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 15:00:54 2022 +0300

    fix tests

commit 4119737
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 14:06:32 2022 +0300

    add getSafedescriptor helper & use original setter for src prop

commit db56a64
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 13:03:18 2022 +0300

    fix arguments checking in setAttribute

commit efcb160
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Tue Feb 8 12:54:16 2022 +0300

    start mocking only target element

... and 3 more commits
  • Loading branch information
stanislav-atr committed Feb 10, 2022
1 parent 9af886f commit 58260b3
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 1 deletion.
14 changes: 14 additions & 0 deletions src/helpers/object-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,17 @@ export const getObjectFromEntries = (entries) => {
* @returns {boolean}
*/
export const isEmptyObject = (obj) => Object.keys(obj).length === 0;

/**
* Checks whether the obj is an empty object
* @param {Object} obj
* @param {string} prop
* @returns {Object|null}
*/
export const safeGetDescriptor = (obj, prop) => {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop);
if (descriptor && descriptor.configurable) {
return descriptor;
}
return null;
};
137 changes: 137 additions & 0 deletions src/scriptlets/prevent-element-src-loading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
hit,
toRegExp,
safeGetDescriptor,
} from '../helpers';

/* eslint-disable max-len, consistent-return */
/**
* @scriptlet prevent-element-src-loading
*
* @description
* Prevents target element source loading without triggering 'onerror' listeners and not breaking 'onload' ones.
*
* **Syntax**
* ```
* example.org#%#//scriptlet('prevent-src', tagName, match)
* ```
*
* - `tagName` - required, case-insensitive target element tagName which `src` property resource loading will be silently prevented; possible values:
* - `script`
* - `img`
* - `iframe`
* - `match` - required, string or regular expression for matching the element's URL;
*
* **Examples**
* 1. Prevent script source loading:
* ```
* example.org#%#//scriptlet('prevent-element-src-loading', 'script' ,'adsbygoogle')
* ```
*/
/* eslint-enable max-len */
export function preventElementSrcLoading(source, tagName, match) {
// do nothing if browser does not support Proxy or Reflect
if (typeof Proxy === 'undefined' || typeof Reflect === 'undefined') {
return;
}
const srcMockData = {
// "KCk9Pnt9" = "()=>{}"
script: 'data:text/javascript;base64,KCk9Pnt9',
// Empty 1x1 image
img: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
// Empty h1 tag
iframe: 'data:text/html;base64, PGRpdj48L2Rpdj4=',
};
let instance;
if (tagName === 'script') {
instance = HTMLScriptElement;
} else if (tagName === 'img') {
instance = HTMLImageElement;
} else if (tagName === 'iframe') {
instance = HTMLIFrameElement;
} else {
return;
}
// For websites that use Trusted Types
// https://w3c.github.io/webappsec-trusted-types/dist/spec/
const hasTrustedTypes = window.trustedTypes && typeof window.trustedTypes.createPolicy === 'function';
let policy;
if (hasTrustedTypes) {
policy = window.trustedTypes.createPolicy('mock', {
createScriptURL: (arg) => arg,
});
}
const SOURCE_PROPERTY_NAME = 'src';
const searchRegexp = toRegExp(match);

const setAttributeWrapper = (target, thisArg, args) => {
// Check if arguments are present
if (!args[0] || !args[1]) {
return Reflect.apply(target, thisArg, args);
}
const nodeName = thisArg.nodeName.toLowerCase();
const attrName = args[0].toLowerCase();
const attrValue = args[1];
const isMatched = attrName === SOURCE_PROPERTY_NAME
&& tagName.toLowerCase() === nodeName
&& srcMockData[nodeName]
&& searchRegexp.test(attrValue);

if (!isMatched) {
return Reflect.apply(target, thisArg, args);
}

hit(source);
// Forward the URI that corresponds with element's MIME type
return Reflect.apply(target, thisArg, [attrName, srcMockData[nodeName]]);
};

const setAttributeHandler = {
apply: setAttributeWrapper,
};
// eslint-disable-next-line max-len
instance.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, setAttributeHandler);

const origDescriptor = safeGetDescriptor(instance.prototype, SOURCE_PROPERTY_NAME);
if (!origDescriptor) {
return;
}
Object.defineProperty(instance.prototype, SOURCE_PROPERTY_NAME, {
enumerable: true,
configurable: true,
get() {
return origDescriptor.get.call(this);
},
set(urlValue) {
const nodeName = this.nodeName.toLowerCase();
const isMatched = tagName.toLowerCase() === nodeName
&& srcMockData[nodeName]
&& searchRegexp.test(urlValue);

if (!isMatched) {
origDescriptor.set.call(this, urlValue);
return;
}

// eslint-disable-next-line no-undef
if (policy && urlValue instanceof TrustedScriptURL) {
const trustedSrc = policy.createScriptURL(urlValue);
origDescriptor.set.call(this, trustedSrc);
hit(source);
return;
}
origDescriptor.set.call(this, srcMockData[nodeName]);
hit(source);
},
});
}

preventElementSrcLoading.names = [
'prevent-element-src-loading',
];

preventElementSrcLoading.injections = [
hit,
toRegExp,
safeGetDescriptor,
];
1 change: 1 addition & 0 deletions src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ export * from './log-on-stack-trace';
export * from './prevent-xhr';
export * from './close-window';
export * from './prevent-refresh';
export * from './prevent-element-src-loading';
1 change: 1 addition & 0 deletions tests/scriptlets/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ import './abort-on-stack-trace.test';
import './log-on-stack-trace.test';
import './close-window.test';
import './prevent-refresh.test';
import './prevent-element-src-loading.test';
166 changes: 166 additions & 0 deletions tests/scriptlets/prevent-element-src-loading.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/* eslint-disable no-underscore-dangle, no-restricted-globals, vars-on-top, no-var */
import { runScriptlet, clearGlobalProps } from '../helpers';

const { test, module } = QUnit;
const name = 'prevent-element-src-loading';

const beforeEach = () => {
window.__debug = () => {
window.hit = 'FIRED';
};
};

const afterEach = () => {
if (window.elem) {
window.elem.remove();
}
clearGlobalProps('hit', '__debug', 'elem');
};

const createTagWithSetAttr = (assert, nodeName, url) => {
const done = assert.async();

const node = document.createElement(nodeName);
node.onload = () => {
assert.ok(true, '.onload triggered');
done();
};
node.onerror = () => {
assert.ok(false, '.onerror triggered');
};
node.setAttribute('src', url);
document.body.append(node);
return node;
};

const createTagWithSrcProp = (assert, nodeName, url) => {
const done = assert.async();

const node = document.createElement(nodeName);
node.onload = () => {
assert.ok(true, '.onload triggered');
done();
};
node.onerror = () => {
assert.ok(false, '.onerror triggered');
};

node.src = url;
document.body.append(node);
return node;
};

const srcMockData = {
script: 'data:text/javascript;base64,KCk9Pnt9',
img: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
iframe: 'data:text/html;base64, PGRpdj48L2Rpdj4=',
};
const TEST_FILES_DIR = './test-files/';
const TEST_SCRIPT01_FILENAME = 'test-script01.js';
const TEST_SCRIPT02_FILENAME = 'test-script02.js';
const TEST_IMAGE_FILENAME = 'test-image.jpeg';
const TEST_IFRAME_FILENAME = 'empty.html';
const SCRIPT_TARGET_NODE = 'script';
const IMG_TARGET_NODE = 'img';
const IFRAME_TARGET_NODE = 'iframe';

module(name, { beforeEach, afterEach });

test('setAttribute, matching script element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_SCRIPT01_FILENAME}`;
const scriptletArgs = [SCRIPT_TARGET_NODE, TEST_SCRIPT01_FILENAME];
runScriptlet(name, scriptletArgs);

var elem = createTagWithSetAttr(assert, SCRIPT_TARGET_NODE, SOURCE_PATH);
assert.strictEqual(elem.src, srcMockData[SCRIPT_TARGET_NODE], 'src was mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
});

test('setAttribute, matching image element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_IMAGE_FILENAME}`;
const scriptletArgs = [IMG_TARGET_NODE, TEST_IMAGE_FILENAME];
runScriptlet(name, scriptletArgs);

window.elem = createTagWithSetAttr(assert, IMG_TARGET_NODE, SOURCE_PATH);
assert.strictEqual(window.elem.src, srcMockData[IMG_TARGET_NODE], 'src was mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
});

test('setAttribute, matching iframe element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_IFRAME_FILENAME}`;
const scriptletArgs = [IFRAME_TARGET_NODE, 'empty.html'];
runScriptlet(name, scriptletArgs);

window.elem = createTagWithSetAttr(assert, IFRAME_TARGET_NODE, SOURCE_PATH);
assert.strictEqual(window.elem.src, srcMockData[IFRAME_TARGET_NODE], 'src was mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
});

test('src prop, matching script element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_SCRIPT01_FILENAME}`;
const scriptletArgs = [SCRIPT_TARGET_NODE, TEST_SCRIPT01_FILENAME];
runScriptlet(name, scriptletArgs);

var elem = createTagWithSrcProp(assert, SCRIPT_TARGET_NODE, SOURCE_PATH);
assert.strictEqual(elem.src, srcMockData[SCRIPT_TARGET_NODE], 'src was mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
});

test('src prop, matching image element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_IMAGE_FILENAME}`;
const scriptletArgs = [IMG_TARGET_NODE, TEST_IMAGE_FILENAME];
runScriptlet(name, scriptletArgs);

window.elem = createTagWithSrcProp(assert, IMG_TARGET_NODE, SOURCE_PATH);
assert.strictEqual(window.elem.src, srcMockData[IMG_TARGET_NODE], 'src was mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
});

test('src prop, matching iframe element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_IFRAME_FILENAME}`;
const scriptletArgs = [IFRAME_TARGET_NODE, 'empty.html'];
runScriptlet(name, scriptletArgs);

window.elem = createTagWithSrcProp(assert, IFRAME_TARGET_NODE, SOURCE_PATH);
assert.strictEqual(window.elem.src, srcMockData[IFRAME_TARGET_NODE], 'src was mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit fired');
});

test('setAttribute, mismatching element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_SCRIPT02_FILENAME}`;
const scriptletArgs = [SCRIPT_TARGET_NODE, 'not-test-script.js'];
runScriptlet(name, scriptletArgs);

window.elem = createTagWithSetAttr(assert, SCRIPT_TARGET_NODE, SOURCE_PATH);
assert.ok(window.elem.src.indexOf(TEST_SCRIPT02_FILENAME) !== -1, 'src was NOT mocked');
assert.strictEqual(window.hit, undefined, 'hit should NOT fire');
});

test('src prop, mismatching element', (assert) => {
const SOURCE_PATH = `${TEST_FILES_DIR}${TEST_SCRIPT02_FILENAME}`;
const scriptletArgs = [SCRIPT_TARGET_NODE, 'not-test-script.js'];
runScriptlet(name, scriptletArgs);

window.elem = createTagWithSrcProp(assert, SCRIPT_TARGET_NODE, SOURCE_PATH);
assert.ok(window.elem.src.indexOf(TEST_SCRIPT02_FILENAME) !== -1, 'src was NOT mocked');
assert.strictEqual(window.hit, undefined, 'hit should NOT fire');
});

test('setAttribute, falsy arguments', (assert) => {
const scriptletArgs = [SCRIPT_TARGET_NODE, 'test-string'];
runScriptlet(name, scriptletArgs);

const node = document.createElement(SCRIPT_TARGET_NODE);
node.setAttribute(null, undefined);
document.body.append(node);
assert.strictEqual(node.getAttribute('null'), 'undefined', 'falsy attr value passed');
node.remove();

const node2 = document.createElement(SCRIPT_TARGET_NODE);
node2.setAttribute(null, 0);
document.body.append(node2);
assert.strictEqual(node2.getAttribute('null'), '0', 'falsy attr value passed');
node2.remove();

assert.strictEqual(window.hit, undefined, 'hit should NOT fire');
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Empty file.
1 change: 0 additions & 1 deletion wiki/about-redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,4 +440,3 @@ https://github.com/gorhill/uBlock/blob/1.31.0/src/web_accessible_resources/click
```
[Redirect source](../src/redirects/blocking-redirects/click2load.html)
* * *

0 comments on commit 58260b3

Please sign in to comment.