Skip to content

Commit

Permalink
improve prevent-element-src-loading scriptlet #228 AG-15346
Browse files Browse the repository at this point in the history
Merge in ADGUARD-FILTERS/scriptlets from fix/AG-15346 to release/v1.7

Squashed commit of the following:

commit 16dcfc3
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Fri Sep 9 13:37:32 2022 +0300

    configure switchCase indentations

commit 918be2d
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Fri Sep 9 13:32:46 2022 +0300

    swap append to appendChild and remove corresponding check

commit 90a1a7c
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Fri Sep 9 13:26:49 2022 +0300

    move magic strings to constants

commit d6cadd4
Author: Stanislav A <s.atroschenko@adguard.com>
Date:   Thu Sep 8 16:08:33 2022 +0300

    improve prevent-element-src-loading scriptlet
  • Loading branch information
stanislav-atr committed Sep 12, 2022
1 parent 3db9c07 commit e1a096d
Show file tree
Hide file tree
Showing 4 changed files with 352 additions and 184 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"qunit": true
},
"rules": {
"indent": ["error", 4],
"indent": ["error", 4, { "SwitchCase": 1}],
"no-param-reassign": 0,
"no-shadow": 0,
"import/prefer-default-export": 0,
Expand Down
70 changes: 35 additions & 35 deletions src/helpers/parse-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,27 @@ export const parseRule = (ruleText) => {
const char = rule[index];
let transition;
switch (char) {
case ' ':
case '(':
case ',': {
transition = TRANSITION.OPENED;
break;
}
case '\'':
case '"': {
sep.symb = char;
transition = TRANSITION.PARAM;
break;
}
case ')': {
transition = index === rule.length - 1
? TRANSITION.CLOSED
: TRANSITION.OPENED;
break;
}
default: {
throw new Error('The rule is not a scriptlet');
}
case ' ':
case '(':
case ',': {
transition = TRANSITION.OPENED;
break;
}
case '\'':
case '"': {
sep.symb = char;
transition = TRANSITION.PARAM;
break;
}
case ')': {
transition = index === rule.length - 1
? TRANSITION.CLOSED
: TRANSITION.OPENED;
break;
}
default: {
throw new Error('The rule is not a scriptlet');
}
}

return transition;
Expand All @@ -108,21 +108,21 @@ export const parseRule = (ruleText) => {
const param = (rule, index, { saver, sep }) => {
const char = rule[index];
switch (char) {
case '\'':
case '"': {
const preIndex = index - 1;
const before = rule[preIndex];
if (char === sep.symb && before !== '\\') {
sep.symb = null;
saver.saveStr();
return TRANSITION.OPENED;
case '\'':
case '"': {
const preIndex = index - 1;
const before = rule[preIndex];
if (char === sep.symb && before !== '\\') {
sep.symb = null;
saver.saveStr();
return TRANSITION.OPENED;
}
}
// eslint-disable-next-line no-fallthrough
default: {
saver.saveSymb(char);
return TRANSITION.PARAM;
}
}
// eslint-disable-next-line no-fallthrough
default: {
saver.saveSymb(char);
return TRANSITION.PARAM;
}
}
};
const transitions = {
Expand Down
76 changes: 69 additions & 7 deletions src/scriptlets/prevent-element-src-loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
hit,
toRegExp,
safeGetDescriptor,
noopFunc,
} from '../helpers/index';

/* eslint-disable max-len, consistent-return */
Expand Down Expand Up @@ -34,6 +35,7 @@ export function preventElementSrcLoading(source, tagName, match) {
if (typeof Proxy === 'undefined' || typeof Reflect === 'undefined') {
return;
}

const srcMockData = {
// "KCk9Pnt9" = "()=>{}"
script: 'data:text/javascript;base64,KCk9Pnt9',
Expand All @@ -42,6 +44,7 @@ export function preventElementSrcLoading(source, tagName, match) {
// Empty h1 tag
iframe: 'data:text/html;base64, PGRpdj48L2Rpdj4=',
};

let instance;
if (tagName === 'script') {
instance = HTMLScriptElement;
Expand All @@ -52,6 +55,7 @@ export function preventElementSrcLoading(source, tagName, match) {
} 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';
Expand All @@ -61,9 +65,15 @@ export function preventElementSrcLoading(source, tagName, match) {
createScriptURL: (arg) => arg,
});
}

const SOURCE_PROPERTY_NAME = 'src';
const ONERROR_PROPERTY_NAME = 'onerror';
const searchRegexp = toRegExp(match);

// This will be needed to silent error events on matched element,
// as url wont be available
const setMatchedAttribute = (elem) => elem.setAttribute(source.name, 'matched');

const setAttributeWrapper = (target, thisArg, args) => {
// Check if arguments are present
if (!args[0] || !args[1]) {
Expand All @@ -82,6 +92,7 @@ export function preventElementSrcLoading(source, tagName, match) {
}

hit(source);
setMatchedAttribute(thisArg);
// Forward the URI that corresponds with element's MIME type
return Reflect.apply(target, thisArg, [attrName, srcMockData[nodeName]]);
};
Expand All @@ -92,15 +103,15 @@ export function preventElementSrcLoading(source, tagName, match) {
// 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) {
const origSrcDescriptor = safeGetDescriptor(instance.prototype, SOURCE_PROPERTY_NAME);
if (!origSrcDescriptor) {
return;
}
Object.defineProperty(instance.prototype, SOURCE_PROPERTY_NAME, {
enumerable: true,
configurable: true,
get() {
return origDescriptor.get.call(this);
return origSrcDescriptor.get.call(this);
},
set(urlValue) {
const nodeName = this.nodeName.toLowerCase();
Expand All @@ -109,21 +120,71 @@ export function preventElementSrcLoading(source, tagName, match) {
&& searchRegexp.test(urlValue);

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

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

// https://github.com/AdguardTeam/Scriptlets/issues/228
// Prevent error event being triggered by other sources
const origOnerrorDescriptor = safeGetDescriptor(HTMLElement.prototype, ONERROR_PROPERTY_NAME);
if (!origOnerrorDescriptor) {
return;
}

Object.defineProperty(HTMLElement.prototype, ONERROR_PROPERTY_NAME, {
enumerable: true,
configurable: true,
get() {
return origOnerrorDescriptor.get.call(this);
},
set(cb) {
const isMatched = this.getAttribute(source.name) === 'matched';

if (!isMatched) {
origOnerrorDescriptor.set.call(this, cb);
return true;
}

origOnerrorDescriptor.set.call(this, noopFunc);
return true;
},
});

const addEventListenerWrapper = (target, thisArg, args) => {
// Check if arguments are present
if (!args[0] || !args[1]) {
return Reflect.apply(target, thisArg, args);
}

const eventName = args[0];
const isMatched = thisArg.getAttribute(source.name) === 'matched'
&& eventName === 'error';

if (isMatched) {
return Reflect.apply(target, thisArg, [eventName, noopFunc]);
}

return Reflect.apply(target, thisArg, args);
};

const addEventListenerHandler = {
apply: addEventListenerWrapper,
};
// eslint-disable-next-line max-len
EventTarget.prototype.addEventListener = new Proxy(EventTarget.prototype.addEventListener, addEventListenerHandler);
}

preventElementSrcLoading.names = [
Expand All @@ -134,4 +195,5 @@ preventElementSrcLoading.injections = [
hit,
toRegExp,
safeGetDescriptor,
noopFunc,
];
Loading

0 comments on commit e1a096d

Please sign in to comment.