From 3e9f850da4478645ba32f8b11c1f992c34fb2895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Wed, 10 May 2023 19:54:50 +0200 Subject: [PATCH] Improve prevent-element-src-loading Add ability to prevent link tag Add ability to prevent inline onerror --- CHANGELOG.md | 5 + src/scriptlets/prevent-element-src-loading.js | 28 +++- .../prevent-element-src-loading.test.js | 124 +++++++++++++++++- 3 files changed, 152 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d073153..bd29f72ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- ability for `prevent-element-src-loading` scriptlet to prevent inline `onerror` [#276](https://github.com/AdguardTeam/Scriptlets/issues/276) +- ability for `prevent-element-src-loading` scriptlet to prevent `link` tag [#276](https://github.com/AdguardTeam/Scriptlets/issues/276) + ### Fixed - issue with reloading website if `$now$`/`$currentDate$` value is used in `trusted-set-cookie-reload` scriptlet [#291](https://github.com/AdguardTeam/Scriptlets/issues/291) diff --git a/src/scriptlets/prevent-element-src-loading.js b/src/scriptlets/prevent-element-src-loading.js index df99bdb4c..8a6f2c7e3 100644 --- a/src/scriptlets/prevent-element-src-loading.js +++ b/src/scriptlets/prevent-element-src-loading.js @@ -20,6 +20,7 @@ import { * - `script` * - `img` * - `iframe` + * - `link` * - `match` — required, string or regular expression for matching the element's URL; * * **Examples** @@ -42,6 +43,8 @@ export function preventElementSrcLoading(source, tagName, match) { img: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', // Empty h1 tag iframe: 'data:text/html;base64, PGRpdj48L2Rpdj4=', + // Empty data + link: 'data:text/plain;base64,', }; let instance; @@ -51,6 +54,8 @@ export function preventElementSrcLoading(source, tagName, match) { instance = HTMLImageElement; } else if (tagName === 'iframe') { instance = HTMLIFrameElement; + } else if (tagName === 'link') { + instance = HTMLLinkElement; } else { return; } @@ -71,7 +76,7 @@ export function preventElementSrcLoading(source, tagName, match) { }); } - const SOURCE_PROPERTY_NAME = 'src'; + const SOURCE_PROPERTY_NAME = tagName === 'link' ? 'href' : 'src'; const ONERROR_PROPERTY_NAME = 'onerror'; const searchRegexp = toRegExp(match); @@ -191,6 +196,27 @@ export function preventElementSrcLoading(source, tagName, match) { }; // eslint-disable-next-line max-len EventTarget.prototype.addEventListener = new Proxy(EventTarget.prototype.addEventListener, addEventListenerHandler); + + const preventInlineOnerror = (tagName, src) => { + window.addEventListener('error', (event) => { + if ( + !event.target + || !event.target.nodeName + || event.target.nodeName.toLowerCase() !== tagName + || !event.target.src + || !src.test(event.target.src) + ) { + return; + } + hit(source); + if (typeof event.target.onload === 'function') { + event.target.onerror = event.target.onload; + return; + } + event.target.onerror = noopFunc; + }, true); + }; + preventInlineOnerror(tagName, searchRegexp); } preventElementSrcLoading.names = [ diff --git a/tests/scriptlets/prevent-element-src-loading.test.js b/tests/scriptlets/prevent-element-src-loading.test.js index 78eab0026..3a4cb3ac3 100644 --- a/tests/scriptlets/prevent-element-src-loading.test.js +++ b/tests/scriptlets/prevent-element-src-loading.test.js @@ -14,11 +14,13 @@ const afterEach = () => { if (window.elem) { window.elem.remove(); } - clearGlobalProps('hit', '__debug', 'elem'); + clearGlobalProps('hit', '__debug', 'elem', 'scriptLoaded', 'scriptBlocked'); }; const SET_SRC_ATTRIBUTE = 'setSrcAttribute'; const SET_SRC_PROP = 'srcProp'; +const SET_LINK_HREF_ATTRIBUTE = 'linkHrefAttribute'; +const SET_LINK_HREF_PROP = 'linkHrefProp'; const ONERROR_PROP = 'onerrorProp'; const ERROR_LISTENER = 'addErrorListener'; @@ -37,6 +39,18 @@ const createTestTag = (assert, nodeName, url, srcMethod, onerrorMethod) => { node.setAttribute('src', url); break; } + case SET_LINK_HREF_PROP: { + node.href = url; + node.rel = 'preload'; + node.as = 'script'; + break; + } + case SET_LINK_HREF_ATTRIBUTE: { + node.setAttribute('href', url); + node.setAttribute('rel', 'preload'); + node.setAttribute('as', 'script'); + break; + } default: // do nothing } @@ -65,10 +79,42 @@ const createTestTag = (assert, nodeName, url, srcMethod, onerrorMethod) => { return node; }; +const onErrorTestTag = (assert, url, testPassed, shouldLoad) => { + const done = assert.async(); + // Used in onload event + window.scriptLoaded = () => { + if (shouldLoad) { + testPassed = true; + assert.strictEqual(testPassed, true, 'onload event fired'); + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + } else { + assert.strictEqual(window.hit, undefined, 'hit should NOT fire'); + } + done(); + }; + // Used in onerror event + window.scriptBlocked = () => { + if (shouldLoad) { + assert.strictEqual(testPassed, true, 'onload event fired'); + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + } else { + assert.strictEqual(window.hit, undefined, 'hit should NOT fire'); + } + done(); + }; + // It's necessary to use the slash like this, + // otherwise the tests will be displayed incorrectly after the build + const slash = '/'; + const html = `