Skip to content

Commit 0f00e5f

Browse files
Taint script verify (#478)
* Port over code from example * Add basic overrides * Fixes and snapshot restructure * Split out method * Remove implementations from this commit
1 parent 3562835 commit 0f00e5f

File tree

19 files changed

+590
-191
lines changed

19 files changed

+590
-191
lines changed

.eslintignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ Sources/ContentScopeScripts/dist/
55
integration-test/extension/contentScope.js
66
integration-test/pages/build
77
packages/special-pages/pages/**/public
8-
unit-test/script-overload-snapshots/
8+
script-overload-snapshots/

integration-test/test-pages/runtime-checks/pages/shadow-dom.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
{ name: 'scripty.type', result: scripty.type, expected: 'application/javascript' },
3434
{ name: 'scripty.id', result: scripty.id, expected: 'scripty' },
3535
{ name: 'script ran', result: window.scripty1Ran, expected: true },
36+
// Ensure no script changes happen without scriptOverload properties
3637
{ name: 'script ran', result: scriptElement.innerText, expected: 'window.scripty1Ran = true' },
3738
];
3839
});

scripts/generateOverloadSnapshots.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { join } from 'node:path'
44
import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
55
import { cwd } from './script-utils.js'
66
const ROOT = join(cwd(import.meta.url))
7-
const configPath = join(ROOT, '../unit-test/script-overload-snapshots/config')
7+
const configPath = join(ROOT, '../snapshots/script-overload-snapshots/config')
88

99
/**
1010
* Generates a bunch of snapshots for script-overload.js results using the configs.
@@ -21,7 +21,7 @@ function generateOut () {
2121
const config = readFileSync(join(configPath, fileName)).toString()
2222
const out = wrapScriptCodeOverload('console.log(1)', JSON.parse(config))
2323
const outName = fileName.replace(/.json$/, '.js')
24-
writeFileSync(join(ROOT, '../unit-test/script-overload-snapshots/out/', outName), out)
24+
writeFileSync(join(ROOT, '../snapshots/script-overload-snapshots/out/', outName), out)
2525
}
2626
}
2727

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
(function (parentScope) {
2+
/**
3+
* DuckDuckGo Runtime Checks injected code.
4+
* If you're reading this, you're probably trying to debug a site that is breaking due to our runtime checks.
5+
* Please raise an issues on our GitHub repo: https://github.com/duckduckgo/content-scope-scripts/
6+
*/
7+
function constructProxy (scope, outputs) {
8+
const taintString = '__ddg_taint__'
9+
// @ts-expect-error - Expected 2 arguments, but got 1
10+
if (Object.is(scope)) {
11+
// Should not happen, but just in case fail safely
12+
console.error('Runtime checks: Scope must be an object', scope, outputs)
13+
return scope
14+
}
15+
return new Proxy(scope, {
16+
get (target, property, receiver) {
17+
const targetObj = target[property]
18+
let targetOut = target
19+
if (typeof property === 'string' && property in outputs) {
20+
targetOut = outputs
21+
}
22+
// Reflects functions with the correct 'this' scope
23+
if (typeof targetObj === 'function') {
24+
return (...args) => {
25+
return Reflect.apply(targetOut[property], target, args)
26+
}
27+
} else {
28+
return Reflect.get(targetOut, property, scope)
29+
}
30+
},
31+
getOwnPropertyDescriptor (target, property) {
32+
if (typeof property === 'string' && property === taintString) {
33+
return { configurable: true, enumerable: false, value: true }
34+
}
35+
return Reflect.getOwnPropertyDescriptor(target, property)
36+
}
37+
})
38+
}
39+
let _ddg_b = parentScope?.navigator ? parentScope.navigator : Object.bind(null);
40+
let _ddg_c = "testingThisOut";
41+
let _ddg_e = parentScope?.navigator?.mediaSession ? parentScope.navigator.mediaSession : Object.bind(null);
42+
let _ddg_f = "playing";
43+
let _ddg_h = parentScope?.navigator?.mediaSession?.doesNotExist ? parentScope.navigator.mediaSession.doesNotExist : Object.bind(null);
44+
let _ddg_j = parentScope?.navigator?.mediaSession?.doesNotExist?.depth ? parentScope.navigator.mediaSession.doesNotExist.depth : Object.bind(null);
45+
let _ddg_l = parentScope?.navigator?.mediaSession?.doesNotExist?.depth?.a ? parentScope.navigator.mediaSession.doesNotExist.depth.a : Object.bind(null);
46+
let _ddg_m = "boop";
47+
let _ddg_k = constructProxy(_ddg_l, {
48+
lot: _ddg_m
49+
});
50+
let _ddg_i = constructProxy(_ddg_j, {
51+
a: _ddg_k
52+
});
53+
let _ddg_g = constructProxy(_ddg_h, {
54+
depth: _ddg_i
55+
});
56+
let _ddg_d = constructProxy(_ddg_e, {
57+
playbackState: _ddg_f,
58+
doesNotExist: _ddg_g
59+
});
60+
let _ddg_a = constructProxy(_ddg_b, {
61+
userAgent: _ddg_c,
62+
mediaSession: _ddg_d
63+
});
64+
let navigator = _ddg_a;
65+
let _ddg_o = parentScope?.document ? parentScope.document : Object.bind(null);
66+
let _ddg_p = "testingThisOut";
67+
let _ddg_n = constructProxy(_ddg_o, {
68+
cookie: _ddg_p
69+
});
70+
let document = _ddg_n;
71+
const window = constructProxy(parentScope, {
72+
navigator: _ddg_a,
73+
document: _ddg_n
74+
});
75+
// Ensure globalThis === window
76+
const globalThis = window
77+
function getContextId (scope) {
78+
if (document?.currentScript && 'contextID' in document.currentScript) {
79+
return document.currentScript.contextID
80+
}
81+
if (scope.contextID) {
82+
return scope.contextID
83+
}
84+
// @ts-expect-error - contextID is a global variable
85+
if (typeof contextID !== 'undefined') {
86+
// @ts-expect-error - contextID is a global variable
87+
// eslint-disable-next-line no-undef
88+
return contextID
89+
}
90+
}
91+
function generateUniqueID () {
92+
const debug = false
93+
if (debug) {
94+
// Easier to debug
95+
return Symbol(globalThis?.crypto?.randomUUID())
96+
}
97+
return Symbol(undefined)
98+
}
99+
function createContextAwareFunction (fn) {
100+
return function (...args) {
101+
// eslint-disable-next-line @typescript-eslint/no-this-alias
102+
let scope = this
103+
// Save the previous contextID and set the new one
104+
const prevContextID = this?.contextID
105+
// @ts-expect-error - contextID is undefined on window
106+
// eslint-disable-next-line no-undef
107+
const changeToContextID = getContextId(this) || contextID
108+
if (typeof args[0] === 'function') {
109+
args[0].contextID = changeToContextID
110+
}
111+
// @ts-expect-error - scope doesn't match window
112+
if (scope && scope !== globalThis) {
113+
scope.contextID = changeToContextID
114+
} else if (!scope) {
115+
scope = new Proxy(scope, {
116+
get (target, prop) {
117+
if (prop === 'contextID') {
118+
return changeToContextID
119+
}
120+
return Reflect.get(target, prop)
121+
}
122+
})
123+
}
124+
// Run the original function with the new contextID
125+
const result = Reflect.apply(fn, scope, args)
126+
// Restore the previous contextID
127+
scope.contextID = prevContextID
128+
return result
129+
}
130+
}
131+
function addTaint () {
132+
const contextID = generateUniqueID()
133+
if ('duckduckgo' in navigator &&
134+
navigator.duckduckgo &&
135+
typeof navigator.duckduckgo === 'object' &&
136+
'taints' in navigator.duckduckgo &&
137+
navigator.duckduckgo.taints instanceof Set) {
138+
if (document.currentScript) {
139+
// @ts-expect-error - contextID is undefined on currentScript
140+
document.currentScript.contextID = contextID
141+
}
142+
navigator?.duckduckgo?.taints.add(contextID)
143+
}
144+
return contextID
145+
}
146+
const contextID = addTaint()
147+
const originalSetTimeout = setTimeout
148+
setTimeout = createContextAwareFunction(originalSetTimeout)
149+
const originalSetInterval = setInterval
150+
setInterval = createContextAwareFunction(originalSetInterval)
151+
const originalPromiseThen = Promise.prototype.then
152+
Promise.prototype.then = createContextAwareFunction(originalPromiseThen)
153+
const originalPromiseCatch = Promise.prototype.catch
154+
Promise.prototype.catch = createContextAwareFunction(originalPromiseCatch)
155+
const originalPromiseFinally = Promise.prototype.finally
156+
Promise.prototype.finally = createContextAwareFunction(originalPromiseFinally)
157+
console.log(1)
158+
})(globalThis)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
(function (parentScope) {
2+
/**
3+
* DuckDuckGo Runtime Checks injected code.
4+
* If you're reading this, you're probably trying to debug a site that is breaking due to our runtime checks.
5+
* Please raise an issues on our GitHub repo: https://github.com/duckduckgo/content-scope-scripts/
6+
*/
7+
function constructProxy (scope, outputs) {
8+
const taintString = '__ddg_taint__'
9+
// @ts-expect-error - Expected 2 arguments, but got 1
10+
if (Object.is(scope)) {
11+
// Should not happen, but just in case fail safely
12+
console.error('Runtime checks: Scope must be an object', scope, outputs)
13+
return scope
14+
}
15+
return new Proxy(scope, {
16+
get (target, property, receiver) {
17+
const targetObj = target[property]
18+
let targetOut = target
19+
if (typeof property === 'string' && property in outputs) {
20+
targetOut = outputs
21+
}
22+
// Reflects functions with the correct 'this' scope
23+
if (typeof targetObj === 'function') {
24+
return (...args) => {
25+
return Reflect.apply(targetOut[property], target, args)
26+
}
27+
} else {
28+
return Reflect.get(targetOut, property, scope)
29+
}
30+
},
31+
getOwnPropertyDescriptor (target, property) {
32+
if (typeof property === 'string' && property === taintString) {
33+
return { configurable: true, enumerable: false, value: true }
34+
}
35+
return Reflect.getOwnPropertyDescriptor(target, property)
36+
}
37+
})
38+
}
39+
let _ddg_q = "meep";
40+
const window = constructProxy(parentScope, {
41+
single: _ddg_q
42+
});
43+
// Ensure globalThis === window
44+
const globalThis = window
45+
function getContextId (scope) {
46+
if (document?.currentScript && 'contextID' in document.currentScript) {
47+
return document.currentScript.contextID
48+
}
49+
if (scope.contextID) {
50+
return scope.contextID
51+
}
52+
// @ts-expect-error - contextID is a global variable
53+
if (typeof contextID !== 'undefined') {
54+
// @ts-expect-error - contextID is a global variable
55+
// eslint-disable-next-line no-undef
56+
return contextID
57+
}
58+
}
59+
function generateUniqueID () {
60+
const debug = false
61+
if (debug) {
62+
// Easier to debug
63+
return Symbol(globalThis?.crypto?.randomUUID())
64+
}
65+
return Symbol(undefined)
66+
}
67+
function createContextAwareFunction (fn) {
68+
return function (...args) {
69+
// eslint-disable-next-line @typescript-eslint/no-this-alias
70+
let scope = this
71+
// Save the previous contextID and set the new one
72+
const prevContextID = this?.contextID
73+
// @ts-expect-error - contextID is undefined on window
74+
// eslint-disable-next-line no-undef
75+
const changeToContextID = getContextId(this) || contextID
76+
if (typeof args[0] === 'function') {
77+
args[0].contextID = changeToContextID
78+
}
79+
// @ts-expect-error - scope doesn't match window
80+
if (scope && scope !== globalThis) {
81+
scope.contextID = changeToContextID
82+
} else if (!scope) {
83+
scope = new Proxy(scope, {
84+
get (target, prop) {
85+
if (prop === 'contextID') {
86+
return changeToContextID
87+
}
88+
return Reflect.get(target, prop)
89+
}
90+
})
91+
}
92+
// Run the original function with the new contextID
93+
const result = Reflect.apply(fn, scope, args)
94+
// Restore the previous contextID
95+
scope.contextID = prevContextID
96+
return result
97+
}
98+
}
99+
function addTaint () {
100+
const contextID = generateUniqueID()
101+
if ('duckduckgo' in navigator &&
102+
navigator.duckduckgo &&
103+
typeof navigator.duckduckgo === 'object' &&
104+
'taints' in navigator.duckduckgo &&
105+
navigator.duckduckgo.taints instanceof Set) {
106+
if (document.currentScript) {
107+
// @ts-expect-error - contextID is undefined on currentScript
108+
document.currentScript.contextID = contextID
109+
}
110+
navigator?.duckduckgo?.taints.add(contextID)
111+
}
112+
return contextID
113+
}
114+
const contextID = addTaint()
115+
const originalSetTimeout = setTimeout
116+
setTimeout = createContextAwareFunction(originalSetTimeout)
117+
const originalSetInterval = setInterval
118+
setInterval = createContextAwareFunction(originalSetInterval)
119+
const originalPromiseThen = Promise.prototype.then
120+
Promise.prototype.then = createContextAwareFunction(originalPromiseThen)
121+
const originalPromiseCatch = Promise.prototype.catch
122+
Promise.prototype.catch = createContextAwareFunction(originalPromiseCatch)
123+
const originalPromiseFinally = Promise.prototype.finally
124+
Promise.prototype.finally = createContextAwareFunction(originalPromiseFinally)
125+
console.log(1)
126+
})(globalThis)

0 commit comments

Comments
 (0)