Skip to content

Commit f539802

Browse files
Runtime origin taint checking
1 parent c43c21a commit f539802

File tree

4 files changed

+83
-38
lines changed

4 files changed

+83
-38
lines changed

integration-test/test-pages/runtime-checks/config/basic-run.json

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,43 @@
2323
{"height": 812, "width": 375}
2424
],
2525
"injectGenericOverloads": {
26-
"Date": {},
27-
"Date.prototype.getTimezoneOffset": {},
28-
"NavigatorUAData.prototype.getHighEntropyValues": {},
26+
"Date": {
27+
"stackCheck": true
28+
},
29+
"Date.prototype.getTimezoneOffset": {
30+
"stackCheck": true
31+
},
32+
"NavigatorUAData.prototype.getHighEntropyValues": {
33+
"stackCheck": true
34+
},
2935
"localStorage": {
36+
"stackCheck": true,
3037
"scheme": "session"
3138
},
3239
"sessionStorage": {
40+
"stackCheck": true,
3341
"scheme": "memory"
3442
},
3543
"innerHeight": {
44+
"stackCheck": true,
3645
"offset": 100
3746
},
3847
"innerWidth": {
48+
"stackCheck": true,
3949
"offset": 100
4050
},
41-
"Screen.prototype.height": {},
42-
"Screen.prototype.width": {}
51+
"outerHeight": {
52+
"stackCheck": true
53+
},
54+
"outerWidth": {
55+
"stackCheck": true
56+
},
57+
"Screen.prototype.height": {
58+
"stackCheck": true
59+
},
60+
"Screen.prototype.width": {
61+
"stackCheck": true
62+
}
4363
}
4464
}
4565
}

src/features/navigator-interface.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ function injectNavigatorInterface (args) {
1717
isDuckDuckGo () {
1818
return DDGPromise.resolve(true)
1919
},
20-
taints: new Set()
20+
taints: new Set(),
21+
taintedOrigins: new Set()
2122
},
2223
enumerable: true,
2324
configurable: false,

src/features/runtime-checks.js

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { glob } from 'typedoc/dist/lib/utils/fs.js'
44
import ContentFeature from '../content-feature.js'
5-
import { DDGProxy, getStackTraceOrigins, getStack, matchHostname, injectGlobalStyles, createStyleElement, postDebugMessage, taintSymbol, hasTaintedMethod, defineProperty } from '../utils.js'
5+
import { DDGProxy, getStackTraceOrigins, getStack, matchHostname, injectGlobalStyles, createStyleElement, postDebugMessage, taintSymbol, hasTaintedMethod, defineProperty, getTabHostname } from '../utils.js'
66
import { wrapScriptCodeOverload } from './runtime-checks/script-overload.js'
77
import { Reflect } from '../captured-globals.js'
88

@@ -66,13 +66,11 @@ const jsMimeTypes = [
6666
'text/x-javascript'
6767
]
6868

69-
function getTaintFromScope (scope, args) {
69+
function getTaintFromScope (scope, args, shouldStackCheck = false) {
7070
try {
7171
scope = args.callee.caller
72-
} catch {
73-
console.log('taint: failed to get callers scope', args, scope)
74-
}
75-
return hasTaintedMethod(scope)
72+
} catch {}
73+
return hasTaintedMethod(scope, shouldStackCheck)
7674
}
7775

7876
class DDGRuntimeChecks extends HTMLElement {
@@ -81,6 +79,7 @@ class DDGRuntimeChecks extends HTMLElement {
8179
#listeners
8280
#connected
8381
#sinks
82+
#debug
8483

8584
constructor () {
8685
super()
@@ -89,6 +88,7 @@ class DDGRuntimeChecks extends HTMLElement {
8988
this.#listeners = []
9089
this.#connected = false
9190
this.#sinks = {}
91+
this.#debug = false
9292
if (shadowDomEnabled) {
9393
const shadow = this.attachShadow({ mode: 'open' })
9494
const style = createStyleElement(`
@@ -103,8 +103,9 @@ class DDGRuntimeChecks extends HTMLElement {
103103
/**
104104
* This method is called once and externally so has to remain public.
105105
**/
106-
setTagName (tagName) {
106+
setTagName (tagName, debug = false) {
107107
this.#tagName = tagName
108+
this.#debug = debug
108109

109110
// Clear the method so it can't be called again
110111
// @ts-expect-error - error TS2790: The operand of a 'delete' operator must be optional.
@@ -178,6 +179,16 @@ class DDGRuntimeChecks extends HTMLElement {
178179
if (taintCheck) {
179180
// Add a symbol to the element so we can identify it as a runtime checked element
180181
Object.defineProperty(el, taintSymbol, { value: true, configurable: false, enumerable: false, writable: false })
182+
// Only show this attribute whilst debugging
183+
if (this.#debug) {
184+
el.setAttribute('data-ddg-runtime-checks', 'true')
185+
}
186+
try {
187+
const origin = this.src && new URL(this.src, window.location.href).hostname
188+
if (origin && navigator?.duckduckgo?.taintedOrigins) {
189+
navigator.duckduckgo.taintedOrigins.add(origin)
190+
}
191+
} catch {}
181192
}
182193

183194
// Reflect all attrs to the new element
@@ -461,7 +472,7 @@ function isInterrogatingDebugMessage (matchType, matchedStackDomain, stack, scri
461472
})
462473
}
463474

464-
function overrideCreateElement () {
475+
function overrideCreateElement (debug) {
465476
const proxy = new DDGProxy(featureName, Document.prototype, 'createElement', {
466477
apply (fn, scope, args) {
467478
if (args.length >= 1) {
@@ -470,7 +481,7 @@ function overrideCreateElement () {
470481
if (shouldInterrogate(initialTagName)) {
471482
args[0] = 'ddg-runtime-checks'
472483
const el = Reflect.apply(fn, scope, args)
473-
el.setTagName(initialTagName)
484+
el.setTagName(initialTagName, debug)
474485
return el
475486
}
476487
}
@@ -541,7 +552,7 @@ export default class RuntimeChecks extends ContentFeature {
541552
replaceElement = this.getFeatureSettingEnabled('replaceElement') || false
542553
monitorProperties = this.getFeatureSettingEnabled('monitorProperties') || true
543554

544-
overrideCreateElement()
555+
overrideCreateElement(this.isDebug)
545556

546557
if (this.getFeatureSettingEnabled('overloadInstanceOf')) {
547558
overloadInstanceOfChecks(HTMLScriptElement)
@@ -569,40 +580,40 @@ export default class RuntimeChecks extends ContentFeature {
569580
injectGenericOverloads () {
570581
const genericOverloads = this.getFeatureSetting('injectGenericOverloads')
571582
if ('Date' in genericOverloads) {
572-
this.overloadDate()
583+
this.overloadDate(genericOverloads.Date)
573584
}
574585
if ('Date.prototype.getTimezoneOffset' in genericOverloads) {
575-
this.overloadDateGetTimezoneOffset()
586+
this.overloadDateGetTimezoneOffset(genericOverloads['Date.prototype.getTimezoneOffset'])
576587
}
577588
if ('NavigatorUAData.prototype.getHighEntropyValues' in genericOverloads) {
578-
this.overloadHighEntropyValues()
589+
this.overloadHighEntropyValues(genericOverloads['NavigatorUAData.prototype.getHighEntropyValues'])
579590
}
580591
['localStorage', 'sessionStorage'].forEach(storageType => {
581592
if (storageType in genericOverloads) {
582593
const storageConfig = genericOverloads[storageType]
583594
if (storageConfig.scheme === 'memory') {
584-
this.overloadStorageWithMemory(storageType)
595+
this.overloadStorageWithMemory(storageConfig, storageType)
585596
} else if (storageConfig.scheme === 'session') {
586-
this.overloadStorageWithSession(storageType)
597+
this.overloadStorageWithSession(storageConfig, storageType)
587598
}
588599
}
589600
})
590601
const breakpoints = this.getFeatureSetting('breakpoints')
591602
const screenSize = { height: screen.height, width: screen.width };
592-
['innerHeight', 'innerWidth', 'Screen.prototype.height', 'Screen.prototype.width'].forEach(sizing => {
603+
['innerHeight', 'innerWidth', 'outerHeight', 'outerWidth', 'Screen.prototype.height', 'Screen.prototype.width'].forEach(sizing => {
593604
if (sizing in genericOverloads) {
594605
const sizingConfig = genericOverloads[sizing]
595-
this.overloadScreenSizes(breakpoints, screenSize, sizing, sizingConfig.offset || 0)
606+
this.overloadScreenSizes(sizingConfig, breakpoints, screenSize, sizing, sizingConfig.offset || 0)
596607
}
597608
})
598609
}
599610

600-
overloadDate () {
611+
overloadDate (config) {
601612
const offset = (new Date()).getTimezoneOffset()
602613
globalThis.Date = new Proxy(globalThis.Date, {
603614
construct (target, args) {
604615
const constructed = Reflect.construct(target, args)
605-
if (getTaintFromScope(this, arguments)) {
616+
if (getTaintFromScope(this, arguments, config.stackCheck)) {
606617
// Falible in that the page could brute force the offset to match. We should fix this.
607618
if (constructed.getTimezoneOffset() === offset) {
608619
return constructed.getUTCDate()
@@ -613,22 +624,22 @@ export default class RuntimeChecks extends ContentFeature {
613624
})
614625
}
615626

616-
overloadDateGetTimezoneOffset () {
627+
overloadDateGetTimezoneOffset (config) {
617628
const offset = (new Date()).getTimezoneOffset()
618629
defineProperty(globalThis.Date.prototype, 'getTimezoneOffset', {
619630
configurable: true,
620631
enumerable: true,
621632
writable: true,
622633
value () {
623-
if (getTaintFromScope(this, arguments)) {
634+
if (getTaintFromScope(this, arguments, config.stackCheck)) {
624635
return 0
625636
}
626637
return offset
627638
}
628639
})
629640
}
630641

631-
overloadHighEntropyValues () {
642+
overloadHighEntropyValues (config) {
632643
if (!('NavigatorUAData' in globalThis)) {
633644
return
634645
}
@@ -640,7 +651,7 @@ export default class RuntimeChecks extends ContentFeature {
640651
writable: true,
641652
value (hints) {
642653
let hintsOut = hints
643-
if (getTaintFromScope(this, arguments)) {
654+
if (getTaintFromScope(this, arguments, config.stackCheck)) {
644655
// If tainted override with default values (using empty array)
645656
hintsOut = []
646657
}
@@ -649,7 +660,7 @@ export default class RuntimeChecks extends ContentFeature {
649660
})
650661
}
651662

652-
overloadStorageWithMemory (key) {
663+
overloadStorageWithMemory (config, key) {
653664
class MemoryStorage {
654665
#data = {}
655666

@@ -680,19 +691,19 @@ export default class RuntimeChecks extends ContentFeature {
680691
}
681692

682693
const storage = new MemoryStorage()
683-
this.overrideStorage(key, storage)
694+
this.overrideStorage(config, key, storage)
684695
}
685696

686-
overloadStorageWithSession (key) {
697+
overloadStorageWithSession (key, config) {
687698
const storage = globalThis.sessionStorage
688-
this.overrideStorage(key, storage)
699+
this.overrideStorage(config, key, storage)
689700
}
690701

691-
overrideStorage (key, storage) {
702+
overrideStorage (config, key, storage) {
692703
const originalStorage = globalThis[key]
693704
defineProperty(globalThis, key, {
694705
get () {
695-
if (getTaintFromScope(this, arguments)) {
706+
if (getTaintFromScope(this, arguments, config.stackCheck)) {
696707
return storage
697708
}
698709
return originalStorage
@@ -713,7 +724,7 @@ export default class RuntimeChecks extends ContentFeature {
713724
* @param {string} key
714725
* @param {number} [offset]
715726
*/
716-
overloadScreenSizes (breakpoints, screenSize, key, offset) {
727+
overloadScreenSizes (config, breakpoints, screenSize, key, offset) {
717728
const closest = findClosestBreakpoint(breakpoints, screenSize)
718729
if (!closest) {
719730
return
@@ -725,9 +736,11 @@ export default class RuntimeChecks extends ContentFeature {
725736
let receiver
726737
switch (key) {
727738
case 'innerHeight':
739+
case 'outerHeight':
728740
returnVal = closest.height - offset
729741
break
730742
case 'innerWidth':
743+
case 'outerWidth':
731744
returnVal = closest.width - offset
732745
break
733746
case 'Screen.prototype.height':
@@ -746,7 +759,7 @@ export default class RuntimeChecks extends ContentFeature {
746759
const defaultVal = Reflect.get(scope, overrideKey, receiver)
747760
defineProperty(scope, overrideKey, {
748761
get () {
749-
if (getTaintFromScope(this, arguments)) {
762+
if (getTaintFromScope(this, arguments, config.stackCheck)) {
750763
return returnVal
751764
}
752765
return defaultVal

src/utils.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,21 @@ export function getContextId (scope) {
351351
}
352352
}
353353

354-
export function hasTaintedMethod (scope) {
354+
const taintedOrigins = createSet()
355+
export function hasTaintedMethod (scope, shouldStackCheck = false) {
355356
if (document?.currentScript?.[taintSymbol]) return true
356357
if ('__ddg_taint__' in window) return true
357358
if (getContextId(scope)) return true
359+
if (!shouldStackCheck || !navigator?.duckduckgo?.taintedOrigins) {
360+
return false
361+
}
362+
const stackOrigins = getStackTraceOrigins(getStack())
363+
for (const stackOrigin of stackOrigins) {
364+
if (navigator.duckduckgo.taintedOrigins.has(stackOrigin)) {
365+
console.log('found tainted origin', stackOrigin)
366+
return true
367+
}
368+
}
358369
return false
359370
}
360371

0 commit comments

Comments
 (0)