Skip to content

Commit 2670ee4

Browse files
committed
add prevent-innerHTML scriptlet. #488 AG-40911
Squashed commit of the following: commit 507a3bf Merge: db89e1d 0a2db9b Author: slvvko <v.leleka@adguard.com> Date: Tue Dec 16 10:46:07 2025 -0500 merge parent branch into current one, resolve conflicts commit db89e1d Author: slvvko <v.leleka@adguard.com> Date: Tue Dec 16 02:14:57 2025 -0500 prepare release 2.2.14 commit dc16428 Author: slvvko <v.leleka@adguard.com> Date: Tue Dec 16 02:13:17 2025 -0500 add error handling for invalid selector commit d9e1003 Author: slvvko <v.leleka@adguard.com> Date: Tue Dec 16 02:09:00 2025 -0500 add getter replacement support for prevent-innerHTML commit fb16caa Author: slvvko <v.leleka@adguard.com> Date: Fri Dec 12 08:44:27 2025 -0500 update agtree to 3.4.3 commit 8750bdf Merge: 694f2d7 49f701a Author: slvvko <v.leleka@adguard.com> Date: Fri Dec 12 08:41:23 2025 -0500 merge parent branch into current one, resolve conflicts commit 694f2d7 Author: slvvko <v.leleka@adguard.com> Date: Thu Dec 11 01:32:08 2025 -0500 update compatibility-table.json commit a05f8fd Author: slvvko <v.leleka@adguard.com> Date: Thu Dec 11 01:19:04 2025 -0500 add prevent-innerHTML scriptlet
1 parent 0a2db9b commit 2670ee4

8 files changed

Lines changed: 457 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,23 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1010
<!-- TODO: change `@added unknown` tag due to the actual version -->
1111
<!-- during new scriptlets or redirects releasing -->
1212

13-
## Unreleased
13+
## [v2.2.14] - 2025-12-16
1414

1515
### Added
1616

17+
- `prevent-innerHTML` scriptlet [#488].
1718
- Ability to configure observer timeout for `trusted-click-element` scriptlet
1819
with a new `observerTimeout` parameter [#400].
1920
- Support for `window.Fingerprint` variable in `fingerprintjs2` redirect (and
2021
scriptlet as well since it is an alias for redirect) [#541].
2122

23+
### Changed
24+
25+
- Updated [@adguard/agtree] to `3.4.3`.
26+
27+
[v2.2.14]: https://github.com/AdguardTeam/Scriptlets/compare/v2.2.13...v2.2.14
2228
[#400]: https://github.com/AdguardTeam/Scriptlets/issues/400
29+
[#488]: https://github.com/AdguardTeam/Scriptlets/issues/488
2330
[#541]: https://github.com/AdguardTeam/Scriptlets/issues/541
2431

2532
## [v2.2.13] - 2025-11-25

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@adguard/scriptlets",
3-
"version": "2.2.13",
3+
"version": "2.2.14",
44
"description": "AdGuard's JavaScript library of Scriptlets and Redirect resources",
55
"type": "module",
66
"scripts": {
@@ -64,7 +64,7 @@
6464
"neverBuiltDependencies": []
6565
},
6666
"dependencies": {
67-
"@adguard/agtree": "^3.3.1",
67+
"@adguard/agtree": "^3.4.3",
6868
"@types/trusted-types": "^2.0.7",
6969
"js-yaml": "^3.14.1"
7070
},

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/compatibility-table.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@
167167
"adg": "prevent-fetch",
168168
"ubo": "prevent-fetch.js (no-fetch-if.js)"
169169
},
170+
{
171+
"adg": "prevent-innerHTML",
172+
"ubo": "prevent-innerHTML"
173+
},
170174
{
171175
"adg": "prevent-xhr",
172176
"ubo": "prevent-xhr.js (no-xhr-if.js)"
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {
2+
hit,
3+
logMessage,
4+
toRegExp,
5+
parseMatchArg,
6+
} from '../helpers';
7+
import { type Source } from './scriptlets';
8+
9+
/**
10+
* @scriptlet prevent-innerHTML
11+
*
12+
* @description
13+
* Conditionally prevents assignment to `innerHTML` property
14+
* and can replace the value returned by the getter.
15+
*
16+
* Related UBO scriptlet:
17+
* https://github.com/gorhill/uBlock/wiki/Resources-Library#prevent-innerhtmljs-
18+
*
19+
* ### Syntax
20+
*
21+
* ```text
22+
* example.org#%#//scriptlet('prevent-innerHTML'[, selector[, pattern[, replacement]]])
23+
* ```
24+
*
25+
* - `selector` — optional, CSS selector to match element. If not specified, matches all elements.
26+
* - `pattern` — optional, string or regular expression to match against the assigned/returned value.
27+
* Prepend with `!` to invert the match. If not specified, matches all values.
28+
* - `replacement` — optional, replacement value to return from getter when pattern matches.
29+
* If not specified, the getter returns the original value unchanged (setter-only mode).
30+
* If specified, enables getter manipulation mode. Possible values:
31+
* - empty string — `''`,
32+
* - custom text.
33+
*
34+
* ### Examples
35+
*
36+
* 1. Prevent any `innerHTML` assignment
37+
*
38+
* ```adblock
39+
* example.org#%#//scriptlet('prevent-innerHTML')
40+
* ```
41+
*
42+
* 1. Prevent `innerHTML` assignment on elements matching selector
43+
*
44+
* ```adblock
45+
* example.org#%#//scriptlet('prevent-innerHTML', '#ads')
46+
* ```
47+
*
48+
* 1. Prevent `innerHTML` assignment when the value contains "ad"
49+
*
50+
* ```adblock
51+
* example.org#%#//scriptlet('prevent-innerHTML', '', 'ad')
52+
* ```
53+
*
54+
* 1. Prevent `innerHTML` assignment on specific element when value matches regex
55+
*
56+
* ```adblock
57+
* example.org#%#//scriptlet('prevent-innerHTML', 'div.ads', '/banner|sponsor/')
58+
* ```
59+
*
60+
* 1. Prevent `innerHTML` assignment when value does NOT match pattern (inverted match)
61+
*
62+
* ```adblock
63+
* example.org#%#//scriptlet('prevent-innerHTML', '', '!allowed-content')
64+
* ```
65+
*
66+
* 1. Replace innerHTML getter value with empty string when it contains "delete window"
67+
*
68+
* ```adblock
69+
* example.org#%#//scriptlet('prevent-innerHTML', '', 'delete window', '')
70+
* ```
71+
*
72+
* 1. Replace innerHTML getter value with custom text when pattern matches
73+
*
74+
* ```adblock
75+
* example.org#%#//scriptlet('prevent-innerHTML', 'div.code', '/evil-script/', 'safe-replacement')
76+
* ```
77+
*
78+
* @added v2.2.14.
79+
*/
80+
export function preventInnerHTML(source: Source, selector = '', pattern = '', replacement?: string): void {
81+
const { isInvertedMatch, matchRegexp } = parseMatchArg(pattern);
82+
83+
const nativeDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
84+
if (nativeDescriptor === undefined) {
85+
return;
86+
}
87+
88+
/**
89+
* Determines whether the innerHTML assignment should be prevented.
90+
*
91+
* @param element Element being assigned to.
92+
* @param value Value being assigned.
93+
*
94+
* @returns True if assignment should be prevented.
95+
*/
96+
const shouldPrevent = (element: Element, value: string): boolean => {
97+
if (selector !== '') {
98+
if (typeof element.matches !== 'function') {
99+
return false;
100+
}
101+
102+
try {
103+
if (element.matches(selector) === false) {
104+
return false;
105+
}
106+
} catch (e) {
107+
// Invalid selector, do not prevent
108+
logMessage(source, `prevent-innerHTML: invalid selector "${selector}"`, true);
109+
return false;
110+
}
111+
}
112+
113+
const patternMatches = matchRegexp.test(String(value));
114+
115+
return isInvertedMatch ? !patternMatches : patternMatches;
116+
};
117+
118+
Object.defineProperty(Element.prototype, 'innerHTML', {
119+
configurable: true,
120+
enumerable: true,
121+
get() {
122+
const value = nativeDescriptor.get
123+
? nativeDescriptor.get.call(this)
124+
: nativeDescriptor.value;
125+
126+
// If replacement is specified, check if we should replace the getter value
127+
if (replacement !== undefined && shouldPrevent(this, value)) {
128+
hit(source);
129+
logMessage(source, 'Replaced innerHTML getter value');
130+
return replacement;
131+
}
132+
133+
return value;
134+
},
135+
set(value: string) {
136+
if (shouldPrevent(this, value)) {
137+
hit(source);
138+
logMessage(source, 'Prevented innerHTML assignment');
139+
return;
140+
}
141+
142+
if (nativeDescriptor.set) {
143+
nativeDescriptor.set.call(this, value);
144+
}
145+
},
146+
});
147+
}
148+
149+
export const preventInnerHTMLNames = [
150+
'prevent-innerHTML',
151+
// aliases are needed for matching the related scriptlet converted into our syntax
152+
'prevent-innerHTML.js',
153+
'ubo-prevent-innerHTML.js',
154+
'ubo-prevent-innerHTML',
155+
];
156+
157+
// eslint-disable-next-line prefer-destructuring
158+
preventInnerHTML.primaryName = preventInnerHTMLNames[0];
159+
160+
preventInnerHTML.injections = [
161+
hit,
162+
logMessage,
163+
toRegExp,
164+
parseMatchArg,
165+
];

src/scriptlets/scriptlets-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export { trustedDispatchEvent } from './trusted-dispatch-event';
7575
export { trustedReplaceOutboundText } from './trusted-replace-outbound-text';
7676
export { preventCanvas } from './prevent-canvas';
7777
export { trustedReplaceArgument } from './trusted-replace-argument';
78+
export { preventInnerHTML } from './prevent-innerHTML';
7879
// redirects as scriptlets
7980
// https://github.com/AdguardTeam/Scriptlets/issues/300
8081
export { AmazonApstag } from './amazon-apstag';

src/scriptlets/scriptlets-names-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export { trustedDispatchEventNames } from './trusted-dispatch-event';
7575
export { trustedReplaceOutboundTextNames } from './trusted-replace-outbound-text';
7676
export { preventCanvasNames } from './prevent-canvas';
7777
export { trustedReplaceArgumentNames } from './trusted-replace-argument';
78+
export { preventInnerHTMLNames } from './prevent-innerHTML';
7879
// redirects as scriptlets
7980
// https://github.com/AdguardTeam/Scriptlets/issues/300
8081
export { AmazonApstagNames } from './amazon-apstag';

0 commit comments

Comments
 (0)