Skip to content

Commit 4e73476

Browse files
Adding runtime element filtering (#283)
* Adding runtime element filtering
1 parent 3b2db7d commit 4e73476

File tree

4 files changed

+127
-2
lines changed

4 files changed

+127
-2
lines changed

integration-test/test-runtime-checks.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,100 @@ describe('Runtime checks: should allow element modification', () => {
199199
type: 'application/javascript'
200200
})
201201
})
202+
203+
it('Script that should filter props', async () => {
204+
const port = server.address().port
205+
const page = await browser.newPage()
206+
await gotoAndWait(page, `http://localhost:${port}/blank.html`, {
207+
site: {
208+
enabledFeatures: ['runtimeChecks']
209+
},
210+
featureSettings: {
211+
runtimeChecks: {
212+
taintCheck: 'enabled',
213+
matchAllDomains: 'enabled',
214+
matchAllStackDomains: 'enabled',
215+
overloadInstanceOf: 'enabled',
216+
tagModifiers: {
217+
script: {
218+
filters: {
219+
property: ['madeUpProp1', 'madeUpProp3'],
220+
attribute: ['madeupattr1', 'madeupattr3']
221+
}
222+
}
223+
}
224+
}
225+
}
226+
})
227+
// And now with a script that will execute
228+
const scriptResult4 = await page.evaluate(
229+
() => {
230+
function getAttributeValues (el) {
231+
const attributes = {}
232+
for (const attribute of el.getAttributeNames()) {
233+
attributes[attribute] = el.getAttribute(attribute)
234+
}
235+
return attributes
236+
}
237+
function getProps (el) {
238+
const props = {}
239+
for (const prop of Object.keys(el)) {
240+
props[prop] = el[prop]
241+
}
242+
return props
243+
}
244+
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
245+
window.scripty4Ran = false
246+
const scriptElement = document.createElement('script')
247+
scriptElement.innerText = 'window.scripty4Ran = true'
248+
scriptElement.id = 'scripty4'
249+
scriptElement.setAttribute('type', 'application/javascript')
250+
// @ts-expect-error made up prop is unknown to TS
251+
scriptElement.madeUpProp1 = 'val'
252+
// @ts-expect-error made up prop is unknown to TS
253+
scriptElement.madeUpProp2 = 'val'
254+
scriptElement.setAttribute('madeUpAttr1', '1')
255+
scriptElement.setAttribute('madeUpAttr2', '2')
256+
document.body.appendChild(scriptElement)
257+
const hadInspectorNode = !!document.querySelector('ddg-runtime-checks')
258+
// Continue to modify the script element after it has been added to the DOM
259+
// @ts-expect-error made up prop is unknown to TS
260+
scriptElement.madeUpProp1 = 'val'
261+
// @ts-expect-error made up prop is unknown to TS
262+
scriptElement.madeUpProp2 = 'val'
263+
scriptElement.setAttribute('madeUpAttr3', '3')
264+
scriptElement.setAttribute('madeUpAttr4', '4')
265+
const instanceofResult = scriptElement instanceof HTMLScriptElement
266+
const scripty = document.querySelector('script#scripty4')
267+
const nodeAndFakeNodeMatch = scripty === scriptElement
268+
269+
return {
270+
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
271+
scripty4: window.scripty4Ran,
272+
hadInspectorNode,
273+
instanceofResult,
274+
attributes: getAttributeValues(scripty),
275+
props: getProps(scripty),
276+
nodeAndFakeNodeMatch
277+
}
278+
}
279+
)
280+
expect(scriptResult4).toEqual({
281+
scripty4: true,
282+
hadInspectorNode: true,
283+
instanceofResult: true,
284+
attributes: {
285+
id: 'scripty4',
286+
type: 'application/javascript',
287+
// madeupattr1: undefined,
288+
madeupattr2: '2',
289+
// madeupattr3: undefined,
290+
madeupattr4: '4'
291+
},
292+
props: {
293+
madeUpProp2: 'val'
294+
},
295+
nodeAndFakeNodeMatch: false
296+
})
297+
})
202298
})

src/features/runtime-checks.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ let stackDomains = []
44
let matchAllStackDomains = false
55
let taintCheck = false
66
let initialCreateElement
7+
let tagModifiers = {}
8+
9+
/**
10+
* @param {string} tagName
11+
* @param {'property' | 'attribute' | 'handler' | 'listener'} filterName
12+
* @param {string} key
13+
* @returns {boolean}
14+
*/
15+
function shouldFilterKey (tagName, filterName, key) {
16+
if (filterName === 'attribute') {
17+
key = key.toLowerCase()
18+
}
19+
return tagModifiers?.[tagName]?.filters?.[filterName]?.includes(key)
20+
}
721

822
let elementRemovalTimeout
923
const featureName = 'runtimeChecks'
@@ -37,12 +51,15 @@ class DDGRuntimeChecks extends HTMLElement {
3751
monitorProperties (el) {
3852
// Mutation oberver and observedAttributes don't work on property accessors
3953
// So instead we need to monitor all properties on the prototypes and forward them to the real element
40-
const propertyNames = []
54+
let propertyNames = []
4155
let proto = Object.getPrototypeOf(el)
4256
while (proto && proto !== Object.prototype) {
4357
propertyNames.push(...Object.getOwnPropertyNames(proto))
4458
proto = Object.getPrototypeOf(proto)
4559
}
60+
const classMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(this))
61+
// Filter away the methods we don't want to monitor from our own class
62+
propertyNames = propertyNames.filter(prop => !classMethods.includes(prop))
4663
propertyNames.forEach(prop => {
4764
if (prop === 'constructor') return
4865
// May throw, but this is best effort monitoring.
@@ -52,6 +69,7 @@ class DDGRuntimeChecks extends HTMLElement {
5269
return el[prop]
5370
},
5471
set (value) {
72+
if (shouldFilterKey(this.#tagName, 'property', prop)) return
5573
el[prop] = value
5674
}
5775
})
@@ -74,23 +92,27 @@ class DDGRuntimeChecks extends HTMLElement {
7492

7593
// Reflect all attrs to the new element
7694
for (const attribute of this.getAttributeNames()) {
95+
if (shouldFilterKey(this.#tagName, 'attribute', attribute)) continue
7796
el.setAttribute(attribute, this.getAttribute(attribute))
7897
}
7998

8099
// Reflect all props to the new element
81100
for (const param of Object.keys(this)) {
101+
if (shouldFilterKey(this.#tagName, 'property', param)) continue
82102
el[param] = this[param]
83103
}
84104

85105
// Reflect all listeners to the new element
86106
for (const [...args] of this.#listeners) {
107+
if (shouldFilterKey(this.#tagName, 'listener', args[0])) continue
87108
el.addEventListener(...args)
88109
}
89110
this.#listeners = []
90111

91112
// Reflect all 'on' event handlers to the new element
92113
for (const propName in this) {
93114
if (propName.startsWith('on')) {
115+
if (shouldFilterKey(this.#tagName, 'handler', propName)) continue
94116
const prop = this[propName]
95117
if (typeof prop === 'function') {
96118
el[propName] = prop
@@ -123,6 +145,7 @@ class DDGRuntimeChecks extends HTMLElement {
123145
}
124146

125147
setAttribute (name, value) {
148+
if (shouldFilterKey(this.#tagName, 'attribute', name)) return
126149
const el = this.getElement()
127150
if (el) {
128151
return el.setAttribute(name, value)
@@ -131,6 +154,7 @@ class DDGRuntimeChecks extends HTMLElement {
131154
}
132155

133156
removeAttribute (name) {
157+
if (shouldFilterKey(this.#tagName, 'attribute', name)) return
134158
const el = this.getElement()
135159
if (el) {
136160
return el.removeAttribute(name)
@@ -139,6 +163,7 @@ class DDGRuntimeChecks extends HTMLElement {
139163
}
140164

141165
addEventListener (...args) {
166+
if (shouldFilterKey(this.#tagName, 'listener', args[0])) return
142167
const el = this.getElement()
143168
if (el) {
144169
return el.addEventListener(...args)
@@ -147,6 +172,7 @@ class DDGRuntimeChecks extends HTMLElement {
147172
}
148173

149174
removeEventListener (...args) {
175+
if (shouldFilterKey(this.#tagName, 'listener', args[0])) return
150176
const el = this.getElement()
151177
if (el) {
152178
return el.removeEventListener(...args)
@@ -272,6 +298,7 @@ export function init (args) {
272298
matchAllStackDomains = getFeatureSettingEnabled(featureName, args, 'matchAllStackDomains')
273299
stackDomains = getFeatureSetting(featureName, args, 'stackDomains') || []
274300
elementRemovalTimeout = getFeatureSetting(featureName, args, 'elementRemovalTimeout') || 1000
301+
tagModifiers = getFeatureSetting(featureName, args, 'tagModifiers') || {}
275302

276303
overrideCreateElement()
277304

src/features/windows-permission-usage.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function init () {
8787
const videoTracks = new Set()
8888
const audioTracks = new Set()
8989

90+
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
9091
function getTracks (permission) {
9192
switch (permission) {
9293
case Permission.Camera:

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"alwaysStrict": true,
66
"allowJs": true,
77
"checkJs": true,
8-
"noEmit": true
8+
"noEmit": true,
9+
"noImplicitReturns": true
910
},
1011
"include": [
1112
"src",

0 commit comments

Comments
 (0)