-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add prevent-element-src-loading scriptlet #180 AG-12328
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
1 parent
9af886f
commit 58260b3
Showing
9 changed files
with
319 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters