diff --git a/CODEOWNERS b/CODEOWNERS index e6cb00c330..5478968390 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,4 +19,3 @@ inject/windows.js @jonathanKingston @q71114 @szanto90balazs # Test owners integration-tests/test-pages/ @kdzwinel @jonathanKingston -unit-tests/script-overload-snapshots/ @shakyShane @jonathanKingston @englehardt diff --git a/integration-test/playwright/remote-pages.spec.js b/integration-test/playwright/remote-pages.spec.js index 70a3424424..5511c7823c 100644 --- a/integration-test/playwright/remote-pages.spec.js +++ b/integration-test/playwright/remote-pages.spec.js @@ -8,9 +8,9 @@ function getHARPath (harFile) { return path.join(testRoot, 'data', 'har', harFile) } -const config = './integration-test/test-pages/runtime-checks/config/replace-element.json' const css = readFileSync('./build/integration/contentScope.js', 'utf8') -const parsedConfig = JSON.parse(readFileSync(config, 'utf8')) +// TODO flag on all features +const parsedConfig = {} function wrapScript (js, replacements) { for (const [find, replace] of Object.entries(replacements)) { diff --git a/integration-test/test-pages.js b/integration-test/test-pages.js index b9b638b116..700d4ee971 100644 --- a/integration-test/test-pages.js +++ b/integration-test/test-pages.js @@ -71,23 +71,6 @@ describe('Test integration pages', () => { } } - describe('Runtime checks', () => { - const pages = { - 'runtime-checks/pages/basic-run.html': 'runtime-checks/config/basic-run.json', - 'runtime-checks/pages/replace-element.html': 'runtime-checks/config/replace-element.json', - 'runtime-checks/pages/filter-props.html': 'runtime-checks/config/filter-props.json', - 'runtime-checks/pages/shadow-dom.html': 'runtime-checks/config/shadow-dom.json', - 'runtime-checks/pages/script-overload.html': 'runtime-checks/config/script-overload.json', - 'runtime-checks/pages/generic-overload.html': 'runtime-checks/config/generic-overload.json' - } - for (const pageName in pages) { - const configName = pages[pageName] - it(`${pageName}`, async () => { - await testPage(pageName, process.cwd() + '/integration-test/test-pages/' + configName) - }) - } - }) - it('Web compat shims correctness', async () => { await testPage( 'webcompat/pages/shims.html', diff --git a/integration-test/test-pages/runtime-checks/config/basic-run.json b/integration-test/test-pages/runtime-checks/config/basic-run.json deleted file mode 100644 index 1819238cae..0000000000 --- a/integration-test/test-pages/runtime-checks/config/basic-run.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "features": { - "runtimeChecks": { - "state": "enabled", - "exceptions": [], - "settings": { - "taintCheck": "enabled", - "matchAllDomains": "enabled", - "matchAllStackDomains": "enabled", - "overloadInstanceOf": "enabled", - "overloadRemoveChild": "enabled", - "overloadReplaceChild": "enabled", - "elementRemovalTimeout": 100, - "injectGlobalStyles": "enabled", - "domains": [ - ], - "stackDomains": [ - ], - "breakpoints": [ - {"height": 768, "width": 1024}, - {"height": 1024, "width": 768}, - {"height": 375, "width": 812}, - {"height": 812, "width": 375} - ], - "injectGenericOverloads": { - "Date": { - "stackCheck": true - }, - "Date.prototype.getTimezoneOffset": { - "stackCheck": true - }, - "NavigatorUAData.prototype.getHighEntropyValues": { - "stackCheck": true - }, - "localStorage": { - "stackCheck": true, - "scheme": "session" - }, - "sessionStorage": { - "stackCheck": true, - "scheme": "memory" - }, - "innerHeight": { - "stackCheck": true, - "offset": 100 - }, - "innerWidth": { - "stackCheck": true, - "offset": 100 - }, - "outerHeight": { - "stackCheck": true - }, - "outerWidth": { - "stackCheck": true - }, - "Screen.prototype.height": { - "stackCheck": true - }, - "Screen.prototype.width": { - "stackCheck": true - } - } - } - } - } -} \ No newline at end of file diff --git a/integration-test/test-pages/runtime-checks/config/filter-props.json b/integration-test/test-pages/runtime-checks/config/filter-props.json deleted file mode 100644 index fb9e7d6ddf..0000000000 --- a/integration-test/test-pages/runtime-checks/config/filter-props.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "features": { - "runtimeChecks": { - "state": "enabled", - "exceptions": [], - "settings": { - "taintCheck": "enabled", - "matchAllDomains": "enabled", - "matchAllStackDomains": "enabled", - "overloadInstanceOf": "enabled", - "domains": [ - ], - "stackDomains": [ - ], - "tagModifiers": { - "script": { - "filters": { - "property": ["madeUpProp1", "madeUpProp3"], - "attribute": ["madeupattr1", "madeupattr3"] - } - } - }, - "breakpoints": [ - {"height": 768, "width": 1024}, - {"height": 1024, "width": 768}, - {"height": 375, "width": 812}, - {"height": 812, "width": 375} - ], - "injectGenericOverloads": { - "Date": {}, - "Date.prototype.getTimezoneOffset": {}, - "NavigatorUAData.prototype.getHighEntropyValues": {}, - "localStorage": { - "scheme": "session" - }, - "sessionStorage": { - "scheme": "memory" - }, - "innerHeight": { - "offset": 100 - }, - "innerWidth": { - "offset": 100 - }, - "Screen.prototype.height": {}, - "Screen.prototype.width": {} - } - } - } - } -} \ No newline at end of file diff --git a/integration-test/test-pages/runtime-checks/config/generic-overload.json b/integration-test/test-pages/runtime-checks/config/generic-overload.json deleted file mode 100644 index e16832878e..0000000000 --- a/integration-test/test-pages/runtime-checks/config/generic-overload.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "features": { - "runtimeChecks": { - "state": "enabled", - "exceptions": [], - "settings": { - "taintCheck": "enabled", - "matchAllDomains": "enabled", - "matchAllStackDomains": "enabled", - "overloadInstanceOf": "enabled", - "domains": [ - ], - "stackDomains": [ - ], - "scriptOverload": { - }, - "breakpoints": [ - {"height": 768, "width": 1024}, - {"height": 1024, "width": 768}, - {"height": 375, "width": 812}, - {"height": 812, "width": 375} - ], - "injectGenericOverloads": { - "Date": {}, - "Date.prototype.getTimezoneOffset": {}, - "NavigatorUAData.prototype.getHighEntropyValues": {}, - "localStorage": { - "scheme": "session" - }, - "sessionStorage": { - "scheme": "memory" - }, - "innerHeight": { - "offset": 100 - }, - "innerWidth": { - "offset": 100 - }, - "Screen.prototype.height": {}, - "Screen.prototype.width": {} - } - } - } - } -} \ No newline at end of file diff --git a/integration-test/test-pages/runtime-checks/config/replace-element.json b/integration-test/test-pages/runtime-checks/config/replace-element.json deleted file mode 100644 index dfe7325512..0000000000 --- a/integration-test/test-pages/runtime-checks/config/replace-element.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "features": { - "runtimeChecks": { - "state": "enabled", - "exceptions": [], - "settings": { - "taintCheck": "enabled", - "matchAllDomains": "enabled", - "matchAllStackDomains": "enabled", - "overloadInstanceOf": "enabled", - "overloadRemoveChild": "enabled", - "overloadReplaceChild": "enabled", - "elementRemovalTimeout": 100, - "injectGlobalStyles": "enabled", - "replaceElement": "enabled", - "domains": [ - ], - "stackDomains": [ - ], - "breakpoints": [ - {"height": 768, "width": 1024}, - {"height": 1024, "width": 768}, - {"height": 375, "width": 812}, - {"height": 812, "width": 375} - ], - "injectGenericOverloads": { - "Date": {}, - "Date.prototype.getTimezoneOffset": {}, - "NavigatorUAData.prototype.getHighEntropyValues": {}, - "localStorage": { - "scheme": "session" - }, - "sessionStorage": { - "scheme": "memory" - }, - "innerHeight": { - "offset": 100 - }, - "innerWidth": { - "offset": 100 - }, - "Screen.prototype.height": {}, - "Screen.prototype.width": {} - } - } - } - } -} \ No newline at end of file diff --git a/integration-test/test-pages/runtime-checks/config/script-overload.json b/integration-test/test-pages/runtime-checks/config/script-overload.json deleted file mode 100644 index b6d0cdfa7b..0000000000 --- a/integration-test/test-pages/runtime-checks/config/script-overload.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "features": { - "runtimeChecks": { - "state": "enabled", - "exceptions": [], - "settings": { - "taintCheck": "enabled", - "matchAllDomains": "enabled", - "matchAllStackDomains": "enabled", - "overloadInstanceOf": "enabled", - "domains": [ - ], - "stackDomains": [ - ], - "scriptOverload": { - "navigator.userAgent": { - "type": "string", - "value": "testingThisOut" - }, - "document.cookie": { - "type": "string", - "value": "testingThisOut" - }, - "navigator.mediaSession.playbackState": { - "type": "string", - "value": "playing" - }, - "navigator.mediaSession.doesNotExist.depth.a.lot": { - "type": "string", - "value": "boop" - }, - "navigator.getBattery": { - "type": "function", - "functionName": "noop" - } - }, - "breakpoints": [ - {"height": 768, "width": 1024}, - {"height": 1024, "width": 768}, - {"height": 375, "width": 812}, - {"height": 812, "width": 375} - ], - "injectGenericOverloads": { - "Date": {}, - "Date.prototype.getTimezoneOffset": {}, - "NavigatorUAData.prototype.getHighEntropyValues": {}, - "localStorage": { - "scheme": "session" - }, - "sessionStorage": { - "scheme": "memory" - }, - "innerHeight": { - "offset": 100 - }, - "innerWidth": { - "offset": 100 - }, - "Screen.prototype.height": {}, - "Screen.prototype.width": {} - } - } - } - } -} \ No newline at end of file diff --git a/integration-test/test-pages/runtime-checks/config/shadow-dom.json b/integration-test/test-pages/runtime-checks/config/shadow-dom.json deleted file mode 100644 index 3ac51726d1..0000000000 --- a/integration-test/test-pages/runtime-checks/config/shadow-dom.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "features": { - "runtimeChecks": { - "state": "enabled", - "exceptions": [], - "settings": { - "taintCheck": "enabled", - "matchAllDomains": "enabled", - "matchAllStackDomains": "enabled", - "overloadInstanceOf": "enabled", - "injectGlobalStyles": "disabled", - "shadowDom": "enabled", - "domains": [ - ], - "stackDomains": [ - ], - "breakpoints": [ - {"height": 768, "width": 1024}, - {"height": 1024, "width": 768}, - {"height": 375, "width": 812}, - {"height": 812, "width": 375} - ], - "injectGenericOverloads": { - "Date": {}, - "Date.prototype.getTimezoneOffset": {}, - "NavigatorUAData.prototype.getHighEntropyValues": {}, - "localStorage": { - "scheme": "session" - }, - "sessionStorage": { - "scheme": "memory" - }, - "innerHeight": { - "offset": 100 - }, - "innerWidth": { - "offset": 100 - }, - "Screen.prototype.height": {}, - "Screen.prototype.width": {} - } - } - } - } -} \ No newline at end of file diff --git a/integration-test/test-pages/runtime-checks/config/site-specific-settings.json b/integration-test/test-pages/runtime-checks/config/site-specific-settings.json deleted file mode 100644 index 092bad8365..0000000000 --- a/integration-test/test-pages/runtime-checks/config/site-specific-settings.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "features": { - "runtimeChecks": { - "state": "enabled", - "exceptions": [], - "settings": { - "taintCheck": "enabled", - "matchAllDomains": "enabled", - "matchAllStackDomains": "enabled", - "overloadInstanceOf": "enabled", - "elementRemovalTimeout": 1000, - "injectGlobalStyles": "enabled", - "domains": [ - { - "domain": "localhost", - "settings": { - "taintCheck": "enabled" - } - }, - { - "domain": "subdomain.localhost", - "settings": { - "taintCheck": "enabled" - } - } - ], - "stackDomains": [ - ], - "breakpoints": [ - {"height": 768, "width": 1024}, - {"height": 1024, "width": 768}, - {"height": 375, "width": 812}, - {"height": 812, "width": 375} - ], - "injectGenericOverloads": { - "Date": {}, - "Date.prototype.getTimezoneOffset": {}, - "NavigatorUAData.prototype.getHighEntropyValues": {}, - "localStorage": { - "scheme": "session" - }, - "sessionStorage": { - "scheme": "memory" - }, - "innerHeight": { - "offset": 100 - }, - "innerWidth": { - "offset": 100 - }, - "Screen.prototype.height": {}, - "Screen.prototype.width": {} - } - } - } - } -} \ No newline at end of file diff --git a/integration-test/test-pages/runtime-checks/index.html b/integration-test/test-pages/runtime-checks/index.html deleted file mode 100644 index 7035a84dee..0000000000 --- a/integration-test/test-pages/runtime-checks/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Runtime checks - - -

[Home]

- -

Runtime element interrogation (runtimeChecks) allows our clients the ability to validate, inspect and modify elements as they get injected into a web page by website scripts.

- - - - diff --git a/integration-test/test-pages/runtime-checks/pages/basic-run.html b/integration-test/test-pages/runtime-checks/pages/basic-run.html deleted file mode 100644 index 8e863b426e..0000000000 --- a/integration-test/test-pages/runtime-checks/pages/basic-run.html +++ /dev/null @@ -1,369 +0,0 @@ - - - - - - Runtime checks - - - - -

[Runtime checks]

- -

This page verifies that runtime checking is enabled given the corresponding config

- - - - diff --git a/integration-test/test-pages/runtime-checks/pages/filter-props.html b/integration-test/test-pages/runtime-checks/pages/filter-props.html deleted file mode 100644 index 0e588d9ccc..0000000000 --- a/integration-test/test-pages/runtime-checks/pages/filter-props.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - Runtime checks - - - - -

[Runtime checks]

- -

This page verifies that runtime checking filters HTML properties config

- - - - diff --git a/integration-test/test-pages/runtime-checks/pages/generic-overload.html b/integration-test/test-pages/runtime-checks/pages/generic-overload.html deleted file mode 100644 index 55e1dea44a..0000000000 --- a/integration-test/test-pages/runtime-checks/pages/generic-overload.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - Runtime checks - - - - -

[Runtime checks]

- -

This page verifies that script overloading works config

- - - - diff --git a/integration-test/test-pages/runtime-checks/pages/replace-element.html b/integration-test/test-pages/runtime-checks/pages/replace-element.html deleted file mode 100644 index 98aeb4746c..0000000000 --- a/integration-test/test-pages/runtime-checks/pages/replace-element.html +++ /dev/null @@ -1,306 +0,0 @@ - - - - - - Runtime checks - - - - -

[Runtime checks]

- -

This page verifies that runtime checking is enabled given the corresponding config

- - - - diff --git a/integration-test/test-pages/runtime-checks/pages/script-overload.html b/integration-test/test-pages/runtime-checks/pages/script-overload.html deleted file mode 100644 index 60cab78250..0000000000 --- a/integration-test/test-pages/runtime-checks/pages/script-overload.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - Runtime checks - - - - -

[Runtime checks]

- -

This page verifies that script overloading works config

- - - - diff --git a/integration-test/test-pages/runtime-checks/pages/shadow-dom.html b/integration-test/test-pages/runtime-checks/pages/shadow-dom.html deleted file mode 100644 index 639b982ebb..0000000000 --- a/integration-test/test-pages/runtime-checks/pages/shadow-dom.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - Runtime checks - - - - -

[Runtime checks]

- -

This page verifies shadow dom is working correctly config

- - - - diff --git a/integration-test/test-performance.js b/integration-test/test-performance.js deleted file mode 100644 index 01222c294f..0000000000 --- a/integration-test/test-performance.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Tests for basic performance - */ -import { setup } from './helpers/harness.js' - -describe('Runtime checks: should check basic performance', () => { - let browser - let server - let teardown - let setupServer - let gotoAndWait - beforeAll(async () => { - ({ browser, setupServer, teardown, gotoAndWait } = await setup({ withExtension: true })) - server = setupServer() - }) - afterAll(async () => { - await server?.close() - await teardown() - }) - - it('Should perform within a resonable timeframe', async () => { - const port = server.address().port - const page = await browser.newPage() - await gotoAndWait(page, `http://localhost:${port}/blank.html`, { - debug: true, - site: { - enabledFeatures: ['runtimeChecks', 'fingerprintingCanvas'] - }, - featureSettings: { - runtimeChecks: { - taintCheck: 'enabled', - matchAllDomains: 'enabled', - matchAllStackDomains: 'disabled', - overloadInstanceOf: 'enabled', - stackDomains: [ - { - domain: 'localhost' - } - ] - } - } - }) - const perfResult = await page.evaluate( - () => { - return { - load: performance.getEntriesByName('load')[0].duration, - init: performance.getEntriesByName('init')[0].duration, - runtimeChecks: performance.getEntriesByName('runtimeChecksCallInit')[0].duration - } - }) - expect(perfResult.runtimeChecks).toBeLessThan(2) - expect(perfResult.load).toBeLessThan(3) - expect(perfResult.init).toBeLessThan(15) - }) -}) diff --git a/integration-test/test-runtime-checks.js b/integration-test/test-runtime-checks.js deleted file mode 100644 index b3bee222f7..0000000000 --- a/integration-test/test-runtime-checks.js +++ /dev/null @@ -1,274 +0,0 @@ -/** - * Tests for runtime checks - */ -import { setup } from './helpers/harness.js' - -describe('Runtime checks: should allow element modification', () => { - let browser - let server - let teardown - let setupServer - let gotoAndWait - beforeAll(async () => { - ({ browser, setupServer, teardown, gotoAndWait } = await setup({ withExtension: true })) - server = setupServer() - }) - afterAll(async () => { - await server?.close() - await teardown() - }) - - it('Script that should not execute', async () => { - const port = server.address().port - const page = await browser.newPage() - await gotoAndWait(page, `http://localhost:${port}/blank.html`, { - site: { - enabledFeatures: ['runtimeChecks'] - }, - featureSettings: { - runtimeChecks: { - taintCheck: 'enabled', - matchAllDomains: 'enabled', - matchAllStackDomains: 'enabled', - overloadInstanceOf: 'enabled' - } - } - }) - // First, a script that will not execute - const scriptResult = await page.evaluate( - () => { - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - window.scripty1Ran = false - const scriptElement = document.createElement('script') - scriptElement.innerText = 'window.scripty1Ran = true' - scriptElement.id = 'scripty' - scriptElement.setAttribute('type', 'application/evilscript') - document.body.appendChild(scriptElement) - const hadInspectorNode = scriptElement === document.querySelector('ddg-runtime-checks') - // Continue to modify the script element after it has been added to the DOM - scriptElement.integrity = 'sha256-123' - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - scriptElement.madeUpProp = 'val' - const instanceofResult = scriptElement instanceof HTMLScriptElement - const scripty = document.querySelector('#scripty') - - return { - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - scripty1: window.scripty1Ran, - hadInspectorNode, - instanceofResult, - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - integrity: scripty.integrity, - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - madeUpProp: scripty.madeUpProp, - // @ts-expect-error - error TS18047: 'scripty' is possibly 'null'. - type: scripty.getAttribute('type') - } - } - ) - expect(scriptResult).toEqual({ - scripty1: false, - hadInspectorNode: true, - instanceofResult: true, - integrity: 'sha256-123', - madeUpProp: 'val', - type: 'application/evilscript' - }) - }) - - it('Script that should filter props and attributes', async () => { - const port = server.address().port - const page = await browser.newPage() - await gotoAndWait(page, `http://localhost:${port}/blank.html`, { - site: { - enabledFeatures: ['runtimeChecks'] - }, - featureSettings: { - runtimeChecks: { - taintCheck: 'enabled', - matchAllDomains: 'enabled', - matchAllStackDomains: 'enabled', - overloadInstanceOf: 'enabled' - } - } - }) - const scriptResult5 = await page.evaluate( - () => { - // @ts-expect-error Undefined property for testing - window.scripty5Ran = false - const myScript = document.createElement('script') - myScript.innerText = 'window.scripty5Ran = true' - Object.setPrototypeOf(myScript, HTMLScriptElement.prototype) - document.body.appendChild(myScript) - // @ts-expect-error Undefined property for testing - return window.scripty5Ran - }) - expect(scriptResult5).toBe(true) - }) - - it('Script should support trusted types', async () => { - const port = server.address().port - const page = await browser.newPage() - await gotoAndWait(page, `http://localhost:${port}/blank.html`, { - site: { - enabledFeatures: ['runtimeChecks'] - }, - featureSettings: { - runtimeChecks: { - taintCheck: 'enabled', - matchAllDomains: 'enabled', - matchAllStackDomains: 'enabled', - overloadInstanceOf: 'enabled' - } - } - }) - const scriptResult6 = await page.evaluate( - () => { - // @ts-expect-error Trusted types are not defined on all browsers - const policy = window.trustedTypes.createPolicy('test', { - createScriptURL: (url) => url - }) - const myScript = document.createElement('script') - myScript.src = policy.createScriptURL('http://example.com') - const srcVal = myScript.src - - myScript.setAttribute('src', policy.createScriptURL('http://example2.com')) - const srcVal2 = myScript.getAttribute('src') - const srcVal3 = myScript.src - - document.body.appendChild(myScript) - - // After append - myScript.setAttribute('src', policy.createScriptURL('http://example3.com')) - const srcVal4 = myScript.getAttribute('src') - const srcVal5 = myScript.src - - myScript.src = policy.createScriptURL('http://example4.com') - const srcVal6 = myScript.getAttribute('src') - const srcVal7 = myScript.src - return { - srcVal, - srcVal2, - srcVal3, - srcVal4, - srcVal5, - srcVal6, - srcVal7 - } - }) - expect(scriptResult6).toEqual({ - srcVal: 'http://example.com', - srcVal2: 'http://example2.com', - srcVal3: 'http://example2.com', - srcVal4: 'http://example3.com/', - srcVal5: 'http://example3.com/', - srcVal6: 'http://example4.com/', - srcVal7: 'http://example4.com/' - }) - }) - - it('Script using parent prototype should execute checking', async () => { - const port = server.address().port - const page = await browser.newPage() - await gotoAndWait(page, `http://localhost:${port}/blank.html`, { - site: { - enabledFeatures: ['runtimeChecks'] - }, - featureSettings: { - runtimeChecks: { - taintCheck: 'enabled', - matchAllDomains: 'enabled', - matchAllStackDomains: 'enabled', - overloadInstanceOf: 'enabled' - } - } - }) - // And now with a script that will execute - const scriptResult = await page.evaluate( - () => { - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - window.scriptDocumentPrototypeRan = false - const scriptElement = Document.prototype.createElement.call(window.document, 'script') - scriptElement.innerText = 'window.scriptDocumentPrototypeRan = true' - scriptElement.id = 'scriptDocumentPrototype' - scriptElement.setAttribute('type', 'application/javascript') - document.body.appendChild(scriptElement) - const hadInspectorNode = scriptElement === document.querySelector('ddg-runtime-checks') - const instanceofResult = scriptElement instanceof HTMLScriptElement - const scripty = document.querySelector('script#scriptDocumentPrototype') - - return { - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - scriptRan: window.scriptDocumentPrototypeRan, - hadInspectorNode, - instanceofResult, - // @ts-expect-error - scripty is possibly null - type: scripty.getAttribute('type') - } - } - ) - expect(scriptResult).toEqual({ - scriptRan: true, - hadInspectorNode: true, - instanceofResult: true, - type: 'application/javascript' - }) - }) - - it('Verify stack tracing', async () => { - const port = server.address().port - const page = await browser.newPage() - await gotoAndWait(page, `http://localhost:${port}/runtimeChecks/index.html`, { - site: { - enabledFeatures: ['runtimeChecks'] - }, - featureSettings: { - runtimeChecks: { - taintCheck: 'enabled', - matchAllDomains: 'enabled', - matchAllStackDomains: 'disabled', - stackDomains: [ - { - domain: 'localhost' - } - ], - tagModifiers: { - script: { - filters: { - // verify the runtime check did run for the stack traced script and filtered the attribute - attribute: ['magicalattribute'] - } - } - } - } - } - }) - // And now with a script that will execute - const pageResults = await page.evaluate( - async () => { - window.dispatchEvent(new Event('initialize')) - await new Promise(resolve => { - window.addEventListener('initializeFinished', () => { - // @ts-expect-error - error TS2810: Expected 1 argument, but got 0. 'new Promise()' needs a JSDoc hint to produce a 'resolve' that can be called without arguments. - resolve() - }) - }) - const scripty = document.querySelector('script#script2') - - return { - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - script1: window.script1Ran, - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - script2: window.script2Ran, - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - magicalProperty: scripty.magicalProperty - } - } - ) - expect(pageResults).toEqual({ - script1: true, - script2: true - // no magical property - }) - }) -}) diff --git a/package.json b/package.json index 046378c406..97d6c6f554 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "lint": "eslint . && npm run tsc", "lint-no-output-globals": "eslint --no-eslintrc --config=build-output.eslintrc --no-ignore Sources/ContentScopeScripts/dist/contentScope.js", "lint-fix": "eslint . --fix && npm run tsc", - "generate-snapshots": "node scripts/generateOverloadSnapshots.js", "test-unit": "jasmine --config=unit-test/config.json", "test-int": "npm run build-integration && jasmine --config=integration-test/config.js", "test-int-x": "xvfb-run --server-args='-screen 0 1024x768x24' npm run test-int", diff --git a/scripts/generateOverloadSnapshots.js b/scripts/generateOverloadSnapshots.js deleted file mode 100644 index 919a88d539..0000000000 --- a/scripts/generateOverloadSnapshots.js +++ /dev/null @@ -1,28 +0,0 @@ -import { wrapScriptCodeOverload } from '../src/features/runtime-checks/script-overload.js' - -import { join } from 'node:path' -import { readFileSync, writeFileSync, readdirSync } from 'node:fs' -import { cwd } from './script-utils.js' -const ROOT = join(cwd(import.meta.url)) -const configPath = join(ROOT, '../snapshots/script-overload-snapshots/config') - -/** - * Generates a bunch of snapshots for script-overload.js results using the configs. - * These are used in unit-test/script-overload.js and ran automatically in automation so that we verify the output is correct. - */ -function generateOut () { - if (process.platform === 'win32') { - console.log('skipping test generation on windows') - return - } - - const files = readdirSync(configPath) - for (const fileName of files) { - const config = readFileSync(join(configPath, fileName)).toString() - const out = wrapScriptCodeOverload('console.log(1)', JSON.parse(config)) - const outName = fileName.replace(/.json$/, '.js') - writeFileSync(join(ROOT, '../snapshots/script-overload-snapshots/out/', outName), out) - } -} - -generateOut() diff --git a/scripts/utils/build.js b/scripts/utils/build.js index d7a213daf6..34e82b8648 100644 --- a/scripts/utils/build.js +++ b/scripts/utils/build.js @@ -5,55 +5,9 @@ import replace from '@rollup/plugin-replace' import resolve from '@rollup/plugin-node-resolve' import css from 'rollup-plugin-import-css' import svg from 'rollup-plugin-svg-import' -import { runtimeInjected, platformSupport } from '../../src/features.js' +import { platformSupport } from '../../src/features.js' import { readFileSync } from 'fs' -/** - * This is a helper function to require all files in a directory - * @param {string} pathName - * @param {string} platform - */ -async function getAllFeatureCode (pathName, platform) { - const fileContents = {} - for (const featureName of runtimeInjected) { - const fileName = getFileName(featureName) - const fullPath = `${pathName}/${fileName}.js` - const code = await rollupScript({ - scriptPath: fullPath, - name: featureName, - supportsMozProxies: false, - platform - }) - fileContents[featureName] = code - } - return fileContents -} - -/** - * Allows importing of all features into a custom runtimeInjects export - * @param {string} platform - * @returns {import('rollup').Plugin} - */ -function runtimeInjections (platform) { - const customId = 'ddg:runtimeInjects' - return { - name: customId, - resolveId (id) { - if (id === customId) { - return id - } - return null - }, - async load (id) { - if (id === customId) { - const code = await getAllFeatureCode('src/features', platform) - return `export default ${JSON.stringify(code, undefined, 4)}` - } - return null - } - } -} - function prefixPlugin (prefixMessage) { return { name: 'prefix-plugin', @@ -108,7 +62,6 @@ export async function rollupScript (params) { stringify: true }), loadFeatures(platform, featureNames), - runtimeInjections(platform), resolve(), commonjs(), replace({ diff --git a/snapshots/script-overload-snapshots/config/1.json b/snapshots/script-overload-snapshots/config/1.json deleted file mode 100644 index e31a13bae6..0000000000 --- a/snapshots/script-overload-snapshots/config/1.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "navigator.userAgent": { - "type": "string", - "value": "testingThisOut" - }, - "document.cookie": { - "type": "string", - "value": "testingThisOut" - }, - "navigator.mediaSession.playbackState": { - "type": "string", - "value": "playing" - }, - "navigator.mediaSession.doesNotExist.depth.a.lot": { - "type": "string", - "value": "boop" - } -} \ No newline at end of file diff --git a/snapshots/script-overload-snapshots/config/2.json b/snapshots/script-overload-snapshots/config/2.json deleted file mode 100644 index 9940fe6724..0000000000 --- a/snapshots/script-overload-snapshots/config/2.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "single": { - "type": "string", - "value": "meep" - } -} \ No newline at end of file diff --git a/snapshots/script-overload-snapshots/config/3.json b/snapshots/script-overload-snapshots/config/3.json deleted file mode 100644 index f4998f70af..0000000000 --- a/snapshots/script-overload-snapshots/config/3.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "fn": { - "type": "function", - "functionName": "debug" - }, - "noop": { - "type": "function", - "functionName": "noop" - }, - "navigator.noop": { - "type": "function", - "functionName": "noop" - }, - "nonexistent": { - "type": "function", - "functionName": "nonexistent" - } -} \ No newline at end of file diff --git a/snapshots/script-overload-snapshots/out/1.js b/snapshots/script-overload-snapshots/out/1.js deleted file mode 100644 index 6dc0b7c6e5..0000000000 --- a/snapshots/script-overload-snapshots/out/1.js +++ /dev/null @@ -1,158 +0,0 @@ -(function (parentScope) { - /** - * DuckDuckGo Runtime Checks injected code. - * If you're reading this, you're probably trying to debug a site that is breaking due to our runtime checks. - * Please raise an issues on our GitHub repo: https://github.com/duckduckgo/content-scope-scripts/ - */ - function constructProxy (scope, outputs) { - const taintString = '__ddg_taint__' - // @ts-expect-error - Expected 2 arguments, but got 1 - if (Object.is(scope)) { - // Should not happen, but just in case fail safely - console.error('Runtime checks: Scope must be an object', scope, outputs) - return scope - } - return new Proxy(scope, { - get (target, property) { - const targetObj = target[property] - let targetOut = target - if (typeof property === 'string' && property in outputs) { - targetOut = outputs - } - // Reflects functions with the correct 'this' scope - if (typeof targetObj === 'function') { - return (...args) => { - return Reflect.apply(targetOut[property], target, args) - } - } else { - return Reflect.get(targetOut, property, scope) - } - }, - getOwnPropertyDescriptor (target, property) { - if (typeof property === 'string' && property === taintString) { - return { configurable: true, enumerable: false, value: true } - } - return Reflect.getOwnPropertyDescriptor(target, property) - } - }) - } - let _ddg_b = parentScope?.navigator ? parentScope.navigator : Object.bind(null); - let _ddg_c = "testingThisOut"; - let _ddg_e = parentScope?.navigator?.mediaSession ? parentScope.navigator.mediaSession : Object.bind(null); - let _ddg_f = "playing"; - let _ddg_h = parentScope?.navigator?.mediaSession?.doesNotExist ? parentScope.navigator.mediaSession.doesNotExist : Object.bind(null); - let _ddg_j = parentScope?.navigator?.mediaSession?.doesNotExist?.depth ? parentScope.navigator.mediaSession.doesNotExist.depth : Object.bind(null); - let _ddg_l = parentScope?.navigator?.mediaSession?.doesNotExist?.depth?.a ? parentScope.navigator.mediaSession.doesNotExist.depth.a : Object.bind(null); - let _ddg_m = "boop"; - let _ddg_k = constructProxy(_ddg_l, { - lot: _ddg_m - }); - let _ddg_i = constructProxy(_ddg_j, { - a: _ddg_k - }); - let _ddg_g = constructProxy(_ddg_h, { - depth: _ddg_i - }); - let _ddg_d = constructProxy(_ddg_e, { - playbackState: _ddg_f, - doesNotExist: _ddg_g - }); - let _ddg_a = constructProxy(_ddg_b, { - userAgent: _ddg_c, - mediaSession: _ddg_d - }); - let navigator = _ddg_a; - let _ddg_o = parentScope?.document ? parentScope.document : Object.bind(null); - let _ddg_p = "testingThisOut"; - let _ddg_n = constructProxy(_ddg_o, { - cookie: _ddg_p - }); - let document = _ddg_n; - const window = constructProxy(parentScope, { - navigator: _ddg_a, - document: _ddg_n - }); - // Ensure globalThis === window - const globalThis = window - function getContextId (scope) { - if (document?.currentScript && 'contextID' in document.currentScript) { - return document.currentScript.contextID - } - if (scope.contextID) { - return scope.contextID - } - // @ts-expect-error - contextID is a global variable - if (typeof contextID !== 'undefined') { - // @ts-expect-error - contextID is a global variable - // eslint-disable-next-line no-undef - return contextID - } - } - function generateUniqueID () { - const debug = false - if (debug) { - // Easier to debug - return Symbol(globalThis?.crypto?.randomUUID()) - } - return Symbol(undefined) - } - function createContextAwareFunction (fn) { - return function (...args) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let scope = this - // Save the previous contextID and set the new one - const prevContextID = this?.contextID - // @ts-expect-error - contextID is undefined on window - // eslint-disable-next-line no-undef - const changeToContextID = getContextId(this) || contextID - if (typeof args[0] === 'function') { - args[0].contextID = changeToContextID - } - // @ts-expect-error - scope doesn't match window - if (scope && scope !== globalThis) { - scope.contextID = changeToContextID - } else if (!scope) { - scope = new Proxy(scope, { - get (target, prop) { - if (prop === 'contextID') { - return changeToContextID - } - return Reflect.get(target, prop) - } - }) - } - // Run the original function with the new contextID - const result = Reflect.apply(fn, scope, args) - // Restore the previous contextID - scope.contextID = prevContextID - return result - } - } - function addTaint () { - const contextID = generateUniqueID() - if ('duckduckgo' in navigator && - navigator.duckduckgo && - typeof navigator.duckduckgo === 'object' && - 'taints' in navigator.duckduckgo && - navigator.duckduckgo.taints instanceof Set) { - if (document.currentScript) { - // @ts-expect-error - contextID is undefined on currentScript - document.currentScript.contextID = contextID - } - navigator?.duckduckgo?.taints.add(contextID) - } - return contextID - } - const contextID = addTaint() - const originalSetTimeout = setTimeout - setTimeout = createContextAwareFunction(originalSetTimeout) - const originalSetInterval = setInterval - setInterval = createContextAwareFunction(originalSetInterval) - const originalPromiseThen = Promise.prototype.then - Promise.prototype.then = createContextAwareFunction(originalPromiseThen) - const originalPromiseCatch = Promise.prototype.catch - Promise.prototype.catch = createContextAwareFunction(originalPromiseCatch) - const originalPromiseFinally = Promise.prototype.finally - Promise.prototype.finally = createContextAwareFunction(originalPromiseFinally) - console.log(1) -})(globalThis) \ No newline at end of file diff --git a/snapshots/script-overload-snapshots/out/2.js b/snapshots/script-overload-snapshots/out/2.js deleted file mode 100644 index 1d79904924..0000000000 --- a/snapshots/script-overload-snapshots/out/2.js +++ /dev/null @@ -1,126 +0,0 @@ -(function (parentScope) { - /** - * DuckDuckGo Runtime Checks injected code. - * If you're reading this, you're probably trying to debug a site that is breaking due to our runtime checks. - * Please raise an issues on our GitHub repo: https://github.com/duckduckgo/content-scope-scripts/ - */ - function constructProxy (scope, outputs) { - const taintString = '__ddg_taint__' - // @ts-expect-error - Expected 2 arguments, but got 1 - if (Object.is(scope)) { - // Should not happen, but just in case fail safely - console.error('Runtime checks: Scope must be an object', scope, outputs) - return scope - } - return new Proxy(scope, { - get (target, property) { - const targetObj = target[property] - let targetOut = target - if (typeof property === 'string' && property in outputs) { - targetOut = outputs - } - // Reflects functions with the correct 'this' scope - if (typeof targetObj === 'function') { - return (...args) => { - return Reflect.apply(targetOut[property], target, args) - } - } else { - return Reflect.get(targetOut, property, scope) - } - }, - getOwnPropertyDescriptor (target, property) { - if (typeof property === 'string' && property === taintString) { - return { configurable: true, enumerable: false, value: true } - } - return Reflect.getOwnPropertyDescriptor(target, property) - } - }) - } - let _ddg_q = "meep"; - const window = constructProxy(parentScope, { - single: _ddg_q - }); - // Ensure globalThis === window - const globalThis = window - function getContextId (scope) { - if (document?.currentScript && 'contextID' in document.currentScript) { - return document.currentScript.contextID - } - if (scope.contextID) { - return scope.contextID - } - // @ts-expect-error - contextID is a global variable - if (typeof contextID !== 'undefined') { - // @ts-expect-error - contextID is a global variable - // eslint-disable-next-line no-undef - return contextID - } - } - function generateUniqueID () { - const debug = false - if (debug) { - // Easier to debug - return Symbol(globalThis?.crypto?.randomUUID()) - } - return Symbol(undefined) - } - function createContextAwareFunction (fn) { - return function (...args) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let scope = this - // Save the previous contextID and set the new one - const prevContextID = this?.contextID - // @ts-expect-error - contextID is undefined on window - // eslint-disable-next-line no-undef - const changeToContextID = getContextId(this) || contextID - if (typeof args[0] === 'function') { - args[0].contextID = changeToContextID - } - // @ts-expect-error - scope doesn't match window - if (scope && scope !== globalThis) { - scope.contextID = changeToContextID - } else if (!scope) { - scope = new Proxy(scope, { - get (target, prop) { - if (prop === 'contextID') { - return changeToContextID - } - return Reflect.get(target, prop) - } - }) - } - // Run the original function with the new contextID - const result = Reflect.apply(fn, scope, args) - // Restore the previous contextID - scope.contextID = prevContextID - return result - } - } - function addTaint () { - const contextID = generateUniqueID() - if ('duckduckgo' in navigator && - navigator.duckduckgo && - typeof navigator.duckduckgo === 'object' && - 'taints' in navigator.duckduckgo && - navigator.duckduckgo.taints instanceof Set) { - if (document.currentScript) { - // @ts-expect-error - contextID is undefined on currentScript - document.currentScript.contextID = contextID - } - navigator?.duckduckgo?.taints.add(contextID) - } - return contextID - } - const contextID = addTaint() - const originalSetTimeout = setTimeout - setTimeout = createContextAwareFunction(originalSetTimeout) - const originalSetInterval = setInterval - setInterval = createContextAwareFunction(originalSetInterval) - const originalPromiseThen = Promise.prototype.then - Promise.prototype.then = createContextAwareFunction(originalPromiseThen) - const originalPromiseCatch = Promise.prototype.catch - Promise.prototype.catch = createContextAwareFunction(originalPromiseCatch) - const originalPromiseFinally = Promise.prototype.finally - Promise.prototype.finally = createContextAwareFunction(originalPromiseFinally) - console.log(1) -})(globalThis) \ No newline at end of file diff --git a/snapshots/script-overload-snapshots/out/3.js b/snapshots/script-overload-snapshots/out/3.js deleted file mode 100644 index 4b080399aa..0000000000 --- a/snapshots/script-overload-snapshots/out/3.js +++ /dev/null @@ -1,141 +0,0 @@ -(function (parentScope) { - /** - * DuckDuckGo Runtime Checks injected code. - * If you're reading this, you're probably trying to debug a site that is breaking due to our runtime checks. - * Please raise an issues on our GitHub repo: https://github.com/duckduckgo/content-scope-scripts/ - */ - function constructProxy (scope, outputs) { - const taintString = '__ddg_taint__' - // @ts-expect-error - Expected 2 arguments, but got 1 - if (Object.is(scope)) { - // Should not happen, but just in case fail safely - console.error('Runtime checks: Scope must be an object', scope, outputs) - return scope - } - return new Proxy(scope, { - get (target, property) { - const targetObj = target[property] - let targetOut = target - if (typeof property === 'string' && property in outputs) { - targetOut = outputs - } - // Reflects functions with the correct 'this' scope - if (typeof targetObj === 'function') { - return (...args) => { - return Reflect.apply(targetOut[property], target, args) - } - } else { - return Reflect.get(targetOut, property, scope) - } - }, - getOwnPropertyDescriptor (target, property) { - if (typeof property === 'string' && property === taintString) { - return { configurable: true, enumerable: false, value: true } - } - return Reflect.getOwnPropertyDescriptor(target, property) - } - }) - } - let _ddg_r = (...args) => { - console.log('debugger', ...args) - // eslint-disable-next-line no-debugger - debugger - }; - let _ddg_s = () => { }; - let _ddg_b = parentScope?.navigator ? parentScope.navigator : Object.bind(null); - let _ddg_t = () => { }; - let _ddg_a = constructProxy(_ddg_b, { - noop: _ddg_t - }); - let navigator = _ddg_a; - let _ddg_u = undefined; - const window = constructProxy(parentScope, { - fn: _ddg_r, - noop: _ddg_s, - navigator: _ddg_a, - nonexistent: _ddg_u - }); - // Ensure globalThis === window - const globalThis = window - function getContextId (scope) { - if (document?.currentScript && 'contextID' in document.currentScript) { - return document.currentScript.contextID - } - if (scope.contextID) { - return scope.contextID - } - // @ts-expect-error - contextID is a global variable - if (typeof contextID !== 'undefined') { - // @ts-expect-error - contextID is a global variable - // eslint-disable-next-line no-undef - return contextID - } - } - function generateUniqueID () { - const debug = false - if (debug) { - // Easier to debug - return Symbol(globalThis?.crypto?.randomUUID()) - } - return Symbol(undefined) - } - function createContextAwareFunction (fn) { - return function (...args) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let scope = this - // Save the previous contextID and set the new one - const prevContextID = this?.contextID - // @ts-expect-error - contextID is undefined on window - // eslint-disable-next-line no-undef - const changeToContextID = getContextId(this) || contextID - if (typeof args[0] === 'function') { - args[0].contextID = changeToContextID - } - // @ts-expect-error - scope doesn't match window - if (scope && scope !== globalThis) { - scope.contextID = changeToContextID - } else if (!scope) { - scope = new Proxy(scope, { - get (target, prop) { - if (prop === 'contextID') { - return changeToContextID - } - return Reflect.get(target, prop) - } - }) - } - // Run the original function with the new contextID - const result = Reflect.apply(fn, scope, args) - // Restore the previous contextID - scope.contextID = prevContextID - return result - } - } - function addTaint () { - const contextID = generateUniqueID() - if ('duckduckgo' in navigator && - navigator.duckduckgo && - typeof navigator.duckduckgo === 'object' && - 'taints' in navigator.duckduckgo && - navigator.duckduckgo.taints instanceof Set) { - if (document.currentScript) { - // @ts-expect-error - contextID is undefined on currentScript - document.currentScript.contextID = contextID - } - navigator?.duckduckgo?.taints.add(contextID) - } - return contextID - } - const contextID = addTaint() - const originalSetTimeout = setTimeout - setTimeout = createContextAwareFunction(originalSetTimeout) - const originalSetInterval = setInterval - setInterval = createContextAwareFunction(originalSetInterval) - const originalPromiseThen = Promise.prototype.then - Promise.prototype.then = createContextAwareFunction(originalPromiseThen) - const originalPromiseCatch = Promise.prototype.catch - Promise.prototype.catch = createContextAwareFunction(originalPromiseCatch) - const originalPromiseFinally = Promise.prototype.finally - Promise.prototype.finally = createContextAwareFunction(originalPromiseFinally) - console.log(1) -})(globalThis) \ No newline at end of file diff --git a/src/content-scope-features.js b/src/content-scope-features.js index 6a85fe0f6e..9300ae7602 100644 --- a/src/content-scope-features.js +++ b/src/content-scope-features.js @@ -1,8 +1,6 @@ -/* global mozProxies */ -import { initStringExemptionLists, isFeatureBroken, registerMessageSecret, getInjectionElement } from './utils' +import { initStringExemptionLists, isFeatureBroken, registerMessageSecret } from './utils' import { platformSupport } from './features' import { PerformanceMonitor } from './performance' -import injectedFeaturesCode from 'ddg:runtimeInjects' import platformFeatures from 'ddg:platformFeatures' let initArgs = null @@ -49,10 +47,6 @@ export function load (args) { : [] for (const featureName of featureNames) { - // Short circuit if the feature is injected later in load() - if (isInjectedFeature(featureName)) { - continue - } const ContentFeature = platformFeatures['ddg_feature_' + featureName] const featureInstance = new ContentFeature(featureName) featureInstance.callLoad(args) @@ -61,56 +55,6 @@ export function load (args) { mark.end() } -/** - * Injects features that we wish to inject into the page as a script tag and runs it. - * This currently is for runtime-checks.js for Firefox only. - */ -function injectFeatures (args) { - const codeFeatures = [] - const argsCopy = structuredClone(args) - // Clear out featureSettings to reduce injection overhead - argsCopy.featureSettings = {} - for (const featureName of Object.keys(injectedFeaturesCode)) { - if (!isFeatureBroken(args, featureName)) { - // Clone back in supported injected feature settings - argsCopy.featureSettings[featureName] = structuredClone(args.featureSettings[featureName]) - const codeImport = injectedFeaturesCode[featureName] - const codeFeature = `;((args) => { - ${codeImport} - const featureInstance = new ${featureName}('${featureName}') - featureInstance.callLoad(args) - featureInstance.callInit(args) - })(args);` - codeFeatures.push(codeFeature) - } - } - const script = document.createElement('script') - const code = `;(() => { - const args = ${JSON.stringify(argsCopy)}; - ${codeFeatures.join('\n')} - })();` - script.src = 'data:text/javascript;base64,' + btoa(code) - getInjectionElement().appendChild(script) - script.remove() -} - -/** - * Returns true if the feature is injected into the page via a script tag - * @param {string} featureName - * @returns {boolean} - */ -function isInjectedFeature (featureName) { - return supportsInjectedFeatures() && featureName in injectedFeaturesCode -} - -/** - * If the browser supports injected features (currently only Firefox) - * @returns {boolean} true if the browser supports injected features - */ -function supportsInjectedFeatures () { - return mozProxies -} - export async function init (args) { const mark = performanceMonitor.mark('init') initArgs = args @@ -125,9 +69,6 @@ export async function init (args) { featureInstance.callInit(args) } }) - if (supportsInjectedFeatures()) { - injectFeatures(args) - } // Fire off updates that came in faster than the init while (updates.length) { const update = updates.pop() diff --git a/src/features.js b/src/features.js index a33435496d..ea494d8b6e 100644 --- a/src/features.js +++ b/src/features.js @@ -1,5 +1,4 @@ const baseFeatures = /** @type {const} */([ - 'runtimeChecks', 'fingerprintingAudio', 'fingerprintingBattery', 'fingerprintingCanvas', @@ -74,10 +73,3 @@ export const platformSupport = { ...otherFeatures ] } - -// Certain features are injected into the page in Firefox -// This is because Firefox does not support proxies for custom elements, it's advised you don't use this without a good reason -/** @type {FeatureName[]} */ -export const runtimeInjected = [ - 'runtimeChecks' -] diff --git a/src/features/navigator-interface.js b/src/features/navigator-interface.js index 065a894670..72038fd38d 100644 --- a/src/features/navigator-interface.js +++ b/src/features/navigator-interface.js @@ -26,9 +26,7 @@ export default class NavigatorInterface extends ContentFeature { platform: args.platform.name, isDuckDuckGo () { return DDGPromise.resolve(true) - }, - taints: new Set(), - taintedOrigins: new Set() + } }, enumerable: true, configurable: false, diff --git a/src/features/runtime-checks.js b/src/features/runtime-checks.js deleted file mode 100644 index 172bc9b34e..0000000000 --- a/src/features/runtime-checks.js +++ /dev/null @@ -1,921 +0,0 @@ -/* global TrustedScriptURL, TrustedScript */ - -import ContentFeature from '../content-feature.js' -import { DDGProxy, getStackTraceOrigins, getStack, matchHostname, injectGlobalStyles, createStyleElement, postDebugMessage, taintSymbol, hasTaintedMethod, taintedOrigins, getTabHostname, isBeingFramed } from '../utils.js' -import { wrapFunction } from '../wrapper-utils.js' -import { wrapScriptCodeOverload } from './runtime-checks/script-overload.js' -import { findClosestBreakpoint } from './runtime-checks/helpers.js' -import { Reflect } from '../captured-globals.js' - -let stackDomains = [] -let matchAllStackDomains = false -let taintCheck = false -let initialCreateElement -let tagModifiers = {} -let shadowDomEnabled = false -let scriptOverload = {} -let replaceElement = false -let monitorProperties = true -// Ignore monitoring properties that are only relevant once and already handled -const defaultIgnoreMonitorList = ['onerror', 'onload'] -let ignoreMonitorList = defaultIgnoreMonitorList - -/** - * @param {string} tagName - * @param {'property' | 'attribute' | 'handler' | 'listener'} filterName - * @param {string} key - * @returns {boolean} - */ -function shouldFilterKey (tagName, filterName, key) { - if (filterName === 'attribute') { - key = key.toLowerCase() - } - return tagModifiers?.[tagName]?.filters?.[filterName]?.includes(key) -} - -// use a module-scoped variable to extract some methods from the class https://github.com/duckduckgo/content-scope-scripts/pull/654#discussion_r1277375832 -/** @type {RuntimeChecks} */ -let featureInstance - -let elementRemovalTimeout -const supportedSinks = ['src'] -// Store the original methods so we can call them without any side effects -const defaultElementMethods = { - setAttribute: HTMLElement.prototype.setAttribute, - setAttributeNS: HTMLElement.prototype.setAttributeNS, - getAttribute: HTMLElement.prototype.getAttribute, - getAttributeNS: HTMLElement.prototype.getAttributeNS, - removeAttribute: HTMLElement.prototype.removeAttribute, - remove: HTMLElement.prototype.remove, - removeChild: HTMLElement.prototype.removeChild -} -const supportedTrustedTypes = 'TrustedScriptURL' in window - -const jsMimeTypes = [ - 'text/javascript', - 'text/ecmascript', - 'application/javascript', - 'application/ecmascript', - 'application/x-javascript', - 'application/x-ecmascript', - 'text/javascript1.0', - 'text/javascript1.1', - 'text/javascript1.2', - 'text/javascript1.3', - 'text/javascript1.4', - 'text/javascript1.5', - 'text/jscript', - 'text/livescript', - 'text/x-ecmascript', - 'text/x-javascript' -] - -function getTaintFromScope (scope, args, shouldStackCheck = false) { - try { - scope = args.callee.caller - } catch {} - return hasTaintedMethod(scope, shouldStackCheck) -} - -class DDGRuntimeChecks extends HTMLElement { - #tagName - #el - #listeners - #connected - #sinks - #debug - - constructor () { - super() - this.#tagName = null - this.#el = null - this.#listeners = [] - this.#connected = false - this.#sinks = {} - this.#debug = false - if (shadowDomEnabled) { - const shadow = this.attachShadow({ mode: 'open' }) - const style = createStyleElement(` - :host { - display: none; - } - `) - shadow.appendChild(style) - } - } - - /** - * This method is called once and externally so has to remain public. - **/ - setTagName (tagName, debug = false) { - this.#tagName = tagName - this.#debug = debug - - // Clear the method so it can't be called again - // @ts-expect-error - error TS2790: The operand of a 'delete' operator must be optional. - delete this.setTagName - } - - connectedCallback () { - // Solves re-entrancy issues from React - if (this.#connected) return - this.#connected = true - if (!this._transplantElement) { - // Restore the 'this' object with the DDGRuntimeChecks prototype as sometimes pages will overwrite it. - Object.setPrototypeOf(this, DDGRuntimeChecks.prototype) - } - this._transplantElement() - } - - _monitorProperties (el) { - // Mutation oberver and observedAttributes don't work on property accessors - // So instead we need to monitor all properties on the prototypes and forward them to the real element - let propertyNames = [] - let proto = Object.getPrototypeOf(el) - while (proto && proto !== Object.prototype) { - propertyNames.push(...Object.getOwnPropertyNames(proto)) - proto = Object.getPrototypeOf(proto) - } - const classMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)) - // Filter away the methods we don't want to monitor from our own class - propertyNames = propertyNames.filter(prop => !classMethods.includes(prop)) - propertyNames.forEach(prop => { - if (prop === 'constructor') return - // May throw, but this is best effort monitoring. - try { - Object.defineProperty(this, prop, { - get () { - return el[prop] - }, - set (value) { - if (shouldFilterKey(this.#tagName, 'property', prop)) return - if (ignoreMonitorList.includes(prop)) return - el[prop] = value - } - }) - } catch { } - }) - } - - computeScriptOverload (el) { - // Short circuit if we don't have any script text - if (el.textContent === '') return - // Short circuit if we're in a trusted script environment - // @ts-expect-error TrustedScript is not defined in the TS lib - if (supportedTrustedTypes && el.textContent instanceof TrustedScript) return - - // Short circuit if not a script type - const scriptType = el.type.toLowerCase() - if (!jsMimeTypes.includes(scriptType) && - scriptType !== 'module' && - scriptType !== '') return - - el.textContent = wrapScriptCodeOverload(el.textContent, scriptOverload) - } - - /** - * The element has been moved to the DOM, so we can now reflect all changes to a real element. - * This is to allow us to interrogate the real element before it is moved to the DOM. - */ - _transplantElement () { - // Create the real element - const el = initialCreateElement.call(document, this.#tagName) - if (taintCheck) { - // Add a symbol to the element so we can identify it as a runtime checked element - Object.defineProperty(el, taintSymbol, { value: true, configurable: false, enumerable: false, writable: false }) - // Only show this attribute whilst debugging - if (this.#debug) { - el.setAttribute('data-ddg-runtime-checks', 'true') - } - try { - const origin = this.src && new URL(this.src, window.location.href).hostname - if (origin && taintedOrigins() && getTabHostname() !== origin) { - taintedOrigins()?.add(origin) - } - } catch {} - } - - // Reflect all attrs to the new element - for (const attribute of this.getAttributeNames()) { - if (shouldFilterKey(this.#tagName, 'attribute', attribute)) continue - defaultElementMethods.setAttribute.call(el, attribute, this.getAttribute(attribute)) - } - - // Reflect all props to the new element - const props = Object.keys(this) - - // Nonce isn't enumerable so we need to add it manually - props.push('nonce') - - for (const prop of props) { - if (shouldFilterKey(this.#tagName, 'property', prop)) continue - el[prop] = this[prop] - } - - for (const sink of supportedSinks) { - if (this.#sinks[sink]) { - el[sink] = this.#sinks[sink] - } - } - - // Reflect all listeners to the new element - for (const [...args] of this.#listeners) { - if (shouldFilterKey(this.#tagName, 'listener', args[0])) continue - el.addEventListener(...args) - } - this.#listeners = [] - - // Reflect all 'on' event handlers to the new element - for (const propName in this) { - if (propName.startsWith('on')) { - if (shouldFilterKey(this.#tagName, 'handler', propName)) continue - const prop = this[propName] - if (typeof prop === 'function') { - el[propName] = prop - } - } - } - - // Move all children to the new element - while (this.firstChild) { - el.appendChild(this.firstChild) - } - - if (this.#tagName === 'script') { - this.computeScriptOverload(el) - } - - if (replaceElement) { - this.replaceElement(el) - } else { - this.insertAfterAndRemove(el) - } - - // TODO pollyfill WeakRef - this.#el = new WeakRef(el) - } - - replaceElement (el) { - // This should be called before this.#el is set - // @ts-expect-error - this is wrong node type - super.parentElement?.replaceChild(el, this) - - if (monitorProperties) { - this._monitorProperties(el) - } - } - - insertAfterAndRemove (el) { - // Move the new element to the DOM - try { - this.insertAdjacentElement('afterend', el) - } catch (e) { console.warn(e) } - - if (monitorProperties) { - this._monitorProperties(el) - } - - // Delay removal of the custom element so if the script calls removeChild it will still be in the DOM and not throw. - setTimeout(() => { - try { - super.remove() - } catch {} - }, elementRemovalTimeout) - } - - _getElement () { - return this.#el?.deref() - } - - /** - * Calls a method on the real element if it exists, otherwise calls the method on the DDGRuntimeChecks element. - * @template {keyof defaultElementMethods} E - * @param {E} method - * @param {...Parameters} args - * @return {ReturnType} - */ - _callMethod (method, ...args) { - const el = this._getElement() - if (el) { - return defaultElementMethods[method].call(el, ...args) - } - // @ts-expect-error TS doesn't like the spread operator - return super[method](...args) - } - - _callSetter (prop, value) { - const el = this._getElement() - if (el) { - el[prop] = value - return - } - super[prop] = value - } - - _callGetter (prop) { - const el = this._getElement() - if (el) { - return el[prop] - } - return super[prop] - } - - /* Native DOM element methods we're capturing to supplant values into the constructed node or store data for. */ - - set src (value) { - const el = this._getElement() - if (el) { - el.src = value - return - } - this.#sinks.src = value - } - - get src () { - const el = this._getElement() - if (el) { - return el.src - } - // @ts-expect-error TrustedScriptURL is not defined in the TS lib - if (supportedTrustedTypes && this.#sinks.src instanceof TrustedScriptURL) { - return this.#sinks.src.toString() - } - return this.#sinks.src - } - - getAttribute (name, value) { - if (shouldFilterKey(this.#tagName, 'attribute', name)) return - if (supportedSinks.includes(name)) { - // Use Reflect to avoid infinite recursion - return Reflect.get(DDGRuntimeChecks.prototype, name, this) - } - return this._callMethod('getAttribute', name, value) - } - - getAttributeNS (namespace, name, value) { - if (namespace) { - return this._callMethod('getAttributeNS', namespace, name, value) - } - return Reflect.apply(DDGRuntimeChecks.prototype.getAttribute, this, [name, value]) - } - - setAttribute (name, value) { - if (shouldFilterKey(this.#tagName, 'attribute', name)) return - if (supportedSinks.includes(name)) { - // Use Reflect to avoid infinite recursion - return Reflect.set(DDGRuntimeChecks.prototype, name, value, this) - } - return this._callMethod('setAttribute', name, value) - } - - setAttributeNS (namespace, name, value) { - if (namespace) { - return this._callMethod('setAttributeNS', namespace, name, value) - } - return Reflect.apply(DDGRuntimeChecks.prototype.setAttribute, this, [name, value]) - } - - removeAttribute (name) { - if (shouldFilterKey(this.#tagName, 'attribute', name)) return - if (supportedSinks.includes(name)) { - delete this[name] - return - } - return this._callMethod('removeAttribute', name) - } - - addEventListener (...args) { - if (shouldFilterKey(this.#tagName, 'listener', args[0])) return - const el = this._getElement() - if (el) { - return el.addEventListener(...args) - } - this.#listeners.push([...args]) - } - - removeEventListener (...args) { - if (shouldFilterKey(this.#tagName, 'listener', args[0])) return - const el = this._getElement() - if (el) { - return el.removeEventListener(...args) - } - this.#listeners = this.#listeners.filter((listener) => { - return listener[0] !== args[0] || listener[1] !== args[1] - }) - } - - toString () { - const interfaceName = this.#tagName.charAt(0).toUpperCase() + this.#tagName.slice(1) - return `[object HTML${interfaceName}Element]` - } - - get tagName () { - return this.#tagName.toUpperCase() - } - - get nodeName () { - return this.tagName - } - - remove () { - let returnVal - try { - returnVal = this._callMethod('remove') - super.remove() - } catch {} - return returnVal - } - - // @ts-expect-error TS node return here - removeChild (child) { - return this._callMethod('removeChild', child) - } -} - -/** - * Overrides the instanceof checks to make the custom element interface pass an instanceof check - * @param {Object} elementInterface - */ -function overloadInstanceOfChecks (elementInterface) { - const proxy = new Proxy(elementInterface[Symbol.hasInstance], { - apply (fn, scope, args) { - if (args[0] instanceof DDGRuntimeChecks) { - return true - } - return Reflect.apply(fn, scope, args) - } - }) - // May throw, but we can ignore it - try { - Object.defineProperty(elementInterface, Symbol.hasInstance, { - value: proxy - }) - } catch {} -} - -/** - * Returns true if the tag should be intercepted - * @param {string} tagName - * @returns {boolean} - */ -function shouldInterrogate (tagName) { - const interestingTags = ['script'] - if (!interestingTags.includes(tagName)) { - return false - } - if (matchAllStackDomains) { - isInterrogatingDebugMessage('matchedAllStackDomain') - return true - } - if (taintCheck && document.currentScript?.[taintSymbol]) { - isInterrogatingDebugMessage('taintCheck') - return true - } - const stack = getStack() - const scriptOrigins = [...getStackTraceOrigins(stack)] - const interestingHost = scriptOrigins.find(origin => { - return stackDomains.some(rule => matchHostname(origin, rule.domain)) - }) - const isInterestingHost = !!interestingHost - if (isInterestingHost) { - isInterrogatingDebugMessage('matchedStackDomain', interestingHost, stack, scriptOrigins) - } - return isInterestingHost -} - -function isInterrogatingDebugMessage (matchType, matchedStackDomain, stack, scriptOrigins) { - postDebugMessage('runtimeChecks', { - documentUrl: document.location.href, - matchedStackDomain, - matchType, - scriptOrigins, - stack - }) -} - -function isRuntimeElement (element) { - try { - return element instanceof DDGRuntimeChecks - } catch {} - return false -} - -function overloadGetOwnPropertyDescriptor () { - const capturedDescriptors = { - HTMLScriptElement: Object.getOwnPropertyDescriptors(HTMLScriptElement), - HTMLScriptElementPrototype: Object.getOwnPropertyDescriptors(HTMLScriptElement.prototype) - } - /** - * @param {any} value - * @returns {string | undefined} - */ - function getInterfaceName (value) { - let interfaceName - if (value === HTMLScriptElement) { - interfaceName = 'HTMLScriptElement' - } - if (value === HTMLScriptElement.prototype) { - interfaceName = 'HTMLScriptElementPrototype' - } - return interfaceName - } - // TODO: Consoldiate with wrapProperty code - function getInterfaceDescriptor (interfaceValue, interfaceName, propertyName) { - const capturedInterface = capturedDescriptors[interfaceName] && capturedDescriptors[interfaceName][propertyName] - const capturedInterfaceOut = { ...capturedInterface } - if (capturedInterface.get) { - capturedInterfaceOut.get = wrapFunction(function () { - let method = capturedInterface.get - if (isRuntimeElement(this)) { - method = () => this._callGetter(propertyName) - } - return method.call(this) - }, capturedInterface.get) - } - if (capturedInterface.set) { - capturedInterfaceOut.set = wrapFunction(function (value) { - let method = capturedInterface - if (isRuntimeElement(this)) { - method = (value) => this._callSetter(propertyName, value) - } - return method.call(this, [value]) - }, capturedInterface.set) - } - return capturedInterfaceOut - } - const proxy = new DDGProxy(featureInstance, Object, 'getOwnPropertyDescriptor', { - apply (fn, scope, args) { - const interfaceValue = args[0] - const interfaceName = getInterfaceName(interfaceValue) - const propertyName = args[1] - const capturedInterface = capturedDescriptors[interfaceName] && capturedDescriptors[interfaceName][propertyName] - if (interfaceName && capturedInterface) { - return getInterfaceDescriptor(interfaceValue, interfaceName, propertyName) - } - return Reflect.apply(fn, scope, args) - } - }) - proxy.overload() - const proxy2 = new DDGProxy(featureInstance, Object, 'getOwnPropertyDescriptors', { - apply (fn, scope, args) { - const interfaceValue = args[0] - const interfaceName = getInterfaceName(interfaceValue) - const capturedInterface = capturedDescriptors[interfaceName] - if (interfaceName && capturedInterface) { - const out = {} - for (const propertyName of Object.getOwnPropertyNames(capturedInterface)) { - out[propertyName] = getInterfaceDescriptor(interfaceValue, interfaceName, propertyName) - } - return out - } - return Reflect.apply(fn, scope, args) - } - }) - proxy2.overload() -} - -function overrideCreateElement (debug) { - const proxy = new DDGProxy(featureInstance, Document.prototype, 'createElement', { - apply (fn, scope, args) { - if (args.length >= 1) { - // String() is used to coerce the value to a string (For: ProseMirror/prosemirror-model/src/to_dom.ts) - const initialTagName = String(args[0]).toLowerCase() - if (shouldInterrogate(initialTagName)) { - args[0] = 'ddg-runtime-checks' - const el = Reflect.apply(fn, scope, args) - el.setTagName(initialTagName, debug) - return el - } - } - return Reflect.apply(fn, scope, args) - } - }) - proxy.overload() - initialCreateElement = proxy._native -} - -function overloadRemoveChild () { - const proxy = new DDGProxy(featureInstance, Node.prototype, 'removeChild', { - apply (fn, scope, args) { - const child = args[0] - if (child instanceof DDGRuntimeChecks) { - // Should call the real removeChild method if it's already replaced - const realNode = child._getElement() - if (realNode) { - args[0] = realNode - } - } - return Reflect.apply(fn, scope, args) - } - }) - proxy.overloadDescriptor() -} - -function overloadReplaceChild () { - const proxy = new DDGProxy(featureInstance, Node.prototype, 'replaceChild', { - apply (fn, scope, args) { - const newChild = args[1] - if (newChild instanceof DDGRuntimeChecks) { - const realNode = newChild._getElement() - if (realNode) { - args[1] = realNode - } - } - return Reflect.apply(fn, scope, args) - } - }) - proxy.overloadDescriptor() -} - -export default class RuntimeChecks extends ContentFeature { - load () { - // This shouldn't happen, but if it does we don't want to break the page - try { - // @ts-expect-error TS node return here - globalThis.customElements.define('ddg-runtime-checks', DDGRuntimeChecks) - } catch {} - // eslint-disable-next-line @typescript-eslint/no-this-alias - featureInstance = this - } - - init () { - let enabled = this.getFeatureSettingEnabled('matchAllDomains') - if (!enabled) { - enabled = this.matchDomainFeatureSetting('domains').length > 0 - } - if (!enabled) return - - taintCheck = this.getFeatureSettingEnabled('taintCheck') - matchAllStackDomains = this.getFeatureSettingEnabled('matchAllStackDomains') - stackDomains = this.getFeatureSetting('stackDomains') || [] - elementRemovalTimeout = this.getFeatureSetting('elementRemovalTimeout') || 1000 - tagModifiers = this.getFeatureSetting('tagModifiers') || {} - shadowDomEnabled = this.getFeatureSettingEnabled('shadowDom') || false - scriptOverload = this.getFeatureSetting('scriptOverload') || {} - ignoreMonitorList = this.getFeatureSetting('ignoreMonitorList') || defaultIgnoreMonitorList - replaceElement = this.getFeatureSettingEnabled('replaceElement') || false - monitorProperties = this.getFeatureSettingEnabled('monitorProperties') || true - - overrideCreateElement(this.isDebug) - - if (this.getFeatureSettingEnabled('overloadInstanceOf')) { - overloadInstanceOfChecks(HTMLScriptElement) - } - - if (this.getFeatureSettingEnabled('injectGlobalStyles')) { - injectGlobalStyles(` - ddg-runtime-checks { - display: none; - } - `) - } - - if (this.getFeatureSetting('injectGenericOverloads')) { - this.injectGenericOverloads() - } - if (this.getFeatureSettingEnabled('overloadRemoveChild')) { - overloadRemoveChild() - } - if (this.getFeatureSettingEnabled('overloadReplaceChild')) { - overloadReplaceChild() - } - if (this.getFeatureSettingEnabled('overloadGetOwnPropertyDescriptor')) { - overloadGetOwnPropertyDescriptor() - } - } - - injectGenericOverloads () { - const genericOverloads = this.getFeatureSetting('injectGenericOverloads') - if ('Date' in genericOverloads) { - this.overloadDate(genericOverloads.Date) - } - if ('Date.prototype.getTimezoneOffset' in genericOverloads) { - this.overloadDateGetTimezoneOffset(genericOverloads['Date.prototype.getTimezoneOffset']) - } - if ('NavigatorUAData.prototype.getHighEntropyValues' in genericOverloads) { - this.overloadHighEntropyValues(genericOverloads['NavigatorUAData.prototype.getHighEntropyValues']) - } - ['localStorage', 'sessionStorage'].forEach(storageType => { - if (storageType in genericOverloads) { - const storageConfig = genericOverloads[storageType] - if (storageConfig.scheme === 'memory') { - this.overloadStorageWithMemory(storageConfig, storageType) - } else if (storageConfig.scheme === 'session') { - this.overloadStorageWithSession(storageConfig, storageType) - } - } - }) - const breakpoints = this.getFeatureSetting('breakpoints') - const screenSize = { height: screen.height, width: screen.width }; - ['innerHeight', 'innerWidth', 'outerHeight', 'outerWidth', 'Screen.prototype.height', 'Screen.prototype.width'].forEach(sizing => { - if (sizing in genericOverloads) { - const sizingConfig = genericOverloads[sizing] - if (isBeingFramed() && !sizingConfig.applyToFrames) return - this.overloadScreenSizes(sizingConfig, breakpoints, screenSize, sizing, sizingConfig.offset || 0) - } - }) - } - - overloadDate (config) { - const offset = (new Date()).getTimezoneOffset() - globalThis.Date = new Proxy(globalThis.Date, { - construct (target, args) { - const constructed = Reflect.construct(target, args) - if (getTaintFromScope(this, arguments, config.stackCheck)) { - // Falible in that the page could brute force the offset to match. We should fix this. - if (constructed.getTimezoneOffset() === offset) { - return constructed.getUTCDate() - } - } - return constructed - } - }) - } - - overloadDateGetTimezoneOffset (config) { - const offset = (new Date()).getTimezoneOffset() - this.defineProperty(globalThis.Date.prototype, 'getTimezoneOffset', { - configurable: true, - enumerable: true, - writable: true, - value () { - if (getTaintFromScope(this, arguments, config.stackCheck)) { - return 0 - } - return offset - } - }) - } - - overloadHighEntropyValues (config) { - if (!('NavigatorUAData' in globalThis)) { - return - } - - const originalGetHighEntropyValues = globalThis.NavigatorUAData.prototype.getHighEntropyValues - this.defineProperty(globalThis.NavigatorUAData.prototype, 'getHighEntropyValues', { - configurable: true, - enumerable: true, - writable: true, - value (hints) { - let hintsOut = hints - if (getTaintFromScope(this, arguments, config.stackCheck)) { - // If tainted override with default values (using empty array) - hintsOut = [] - } - return Reflect.apply(originalGetHighEntropyValues, this, [hintsOut]) - } - }) - } - - overloadStorageWithMemory (config, key) { - /** - * @implements {Storage} - */ - class MemoryStorage { - #data = {} - - /** - * @param {Parameters[0]} id - * @param {Parameters[1]} val - * @returns {ReturnType} - */ - setItem (id, val) { - if (arguments.length < 2) throw new TypeError(`Failed to execute 'setItem' on 'Storage': 2 arguments required, but only ${arguments.length} present.`) - this.#data[id] = String(val) - } - - /** - * @param {Parameters[0]} id - * @returns {ReturnType} - */ - getItem (id) { - return Object.prototype.hasOwnProperty.call(this.#data, id) ? this.#data[id] : null - } - - /** - * @param {Parameters[0]} id - * @returns {ReturnType} - */ - removeItem (id) { - delete this.#data[id] - } - - /** - * @returns {ReturnType} - */ - clear () { - this.#data = {} - } - - /** - * @param {Parameters[0]} n - * @returns {ReturnType} - */ - key (n) { - const keys = Object.keys(this.#data) - return keys[n] - } - - get length () { - return Object.keys(this.#data).length - } - } - /** @satisfies {Storage} */ - const instance = new MemoryStorage() - const storage = new Proxy(instance, { - set (target, prop, value) { - Reflect.apply(target.setItem, target, [prop, value]) - return true - }, - get (target, prop) { - if (typeof target[prop] === 'function') { - return target[prop].bind(instance) - } - return Reflect.get(target, prop, instance) - } - }) - this.overrideStorage(config, key, storage) - } - - overloadStorageWithSession (config, key) { - const storage = globalThis.sessionStorage - this.overrideStorage(config, key, storage) - } - - overrideStorage (config, key, storage) { - const originalStorage = globalThis[key] - this.defineProperty(globalThis, key, { - get () { - if (getTaintFromScope(this, arguments, config.stackCheck)) { - return storage - } - return originalStorage - }, - enumerable: true, - configurable: true - }) - } - - /** - * @typedef {import('./runtime-checks/helpers.js').Sizing} Sizing - */ - - /** - * Overloads the provided key with the closest breakpoint size - * @param {Sizing[]} breakpoints - * @param {Sizing} screenSize - * @param {string} key - * @param {number} [offset] - */ - overloadScreenSizes (config, breakpoints, screenSize, key, offset = 0) { - const closest = findClosestBreakpoint(breakpoints, screenSize) - if (!closest) { - return - } - let returnVal = null - /** @type {object} */ - let scope = globalThis - let overrideKey = key - let receiver - switch (key) { - case 'innerHeight': - case 'outerHeight': - returnVal = closest.height - offset - break - case 'innerWidth': - case 'outerWidth': - returnVal = closest.width - offset - break - case 'Screen.prototype.height': - scope = Screen.prototype - overrideKey = 'height' - returnVal = closest.height - offset - receiver = globalThis.screen - break - case 'Screen.prototype.width': - scope = Screen.prototype - overrideKey = 'width' - returnVal = closest.width - offset - receiver = globalThis.screen - break - } - const defaultGetter = Object.getOwnPropertyDescriptor(scope, overrideKey)?.get - // Should never happen - if (!defaultGetter) { - return - } - // TODO: inner* and outer* should have a setter too - this.defineProperty(scope, overrideKey, { - get () { - const defaultVal = Reflect.apply(defaultGetter, receiver, []) - if (getTaintFromScope(this, arguments, config.stackCheck)) { - return returnVal - } - return defaultVal - }, - enumerable: true, - configurable: true - }) - } -} diff --git a/src/features/runtime-checks/helpers.js b/src/features/runtime-checks/helpers.js deleted file mode 100644 index 62739eefd4..0000000000 --- a/src/features/runtime-checks/helpers.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @typedef {object} Sizing - * @property {number} height - * @property {number} width - */ - -/** - * @param {Sizing[]} breakpoints - * @param {Sizing} screenSize - * @returns { Sizing | null} - */ -export function findClosestBreakpoint (breakpoints, screenSize) { - let closestBreakpoint = null - let closestDistance = Infinity - - for (let i = 0; i < breakpoints.length; i++) { - const breakpoint = breakpoints[i] - const distance = Math.sqrt(Math.pow(breakpoint.height - screenSize.height, 2) + Math.pow(breakpoint.width - screenSize.width, 2)) - - if (distance < closestDistance) { - closestBreakpoint = breakpoint - closestDistance = distance - } - } - - return closestBreakpoint -} diff --git a/src/features/runtime-checks/script-overload.js b/src/features/runtime-checks/script-overload.js deleted file mode 100644 index 8549536ebf..0000000000 --- a/src/features/runtime-checks/script-overload.js +++ /dev/null @@ -1,269 +0,0 @@ -import { processAttr, getContextId } from '../../utils.js' - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const globalStates = new Set() - -function generateUniqueID () { - const debug = false - if (debug) { - // Easier to debug - return Symbol(globalThis?.crypto?.randomUUID()) - } - return Symbol(undefined) -} - -function addTaint () { - const contextID = generateUniqueID() - if ('duckduckgo' in navigator && - navigator.duckduckgo && - typeof navigator.duckduckgo === 'object' && - 'taints' in navigator.duckduckgo && - navigator.duckduckgo.taints instanceof Set) { - if (document.currentScript) { - // @ts-expect-error - contextID is undefined on currentScript - document.currentScript.contextID = contextID - } - navigator?.duckduckgo?.taints.add(contextID) - } - return contextID -} - -function createContextAwareFunction (fn) { - return function (...args) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let scope = this - // Save the previous contextID and set the new one - const prevContextID = this?.contextID - // @ts-expect-error - contextID is undefined on window - // eslint-disable-next-line no-undef - const changeToContextID = getContextId(this) || contextID - if (typeof args[0] === 'function') { - args[0].contextID = changeToContextID - } - // @ts-expect-error - scope doesn't match window - if (scope && scope !== globalThis) { - scope.contextID = changeToContextID - } else if (!scope) { - scope = new Proxy(scope, { - get (target, prop) { - if (prop === 'contextID') { - return changeToContextID - } - return Reflect.get(target, prop) - } - }) - } - // Run the original function with the new contextID - const result = Reflect.apply(fn, scope, args) - - // Restore the previous contextID - scope.contextID = prevContextID - - return result - } -} - -/** - * Indent a code block using braces - * @param {string} string - * @returns {string} - */ -function removeIndent (string) { - const lines = string.split('\n') - const indentSize = 2 - let currentIndent = 0 - const indentedLines = lines.map((line) => { - if (line.trim().startsWith('}')) { - currentIndent -= indentSize - } - const indentedLine = ' '.repeat(currentIndent) + line.trim() - if (line.trim().endsWith('{')) { - currentIndent += indentSize - } - - return indentedLine - }) - return indentedLines.filter(a => a.trim()).join('\n') -} - -const lookup = {} -function getOrGenerateIdentifier (path) { - if (!(path in lookup)) { - lookup[path] = generateAlphaIdentifier(Object.keys(lookup).length + 1) - } - return lookup[path] -} - -function generateAlphaIdentifier (num) { - if (num < 1) { - throw new Error('Input must be a positive integer') - } - const charCodeOffset = 97 - let identifier = '' - while (num > 0) { - num-- - const remainder = num % 26 - const charCode = remainder + charCodeOffset - identifier = String.fromCharCode(charCode) + identifier - num = Math.floor(num / 26) - } - return '_ddg_' + identifier -} - -/** - * @param {*} scope - * @param {Record} outputs - * @returns {Proxy} - */ -function constructProxy (scope, outputs) { - const taintString = '__ddg_taint__' - // @ts-expect-error - Expected 2 arguments, but got 1 - if (Object.is(scope)) { - // Should not happen, but just in case fail safely - console.error('Runtime checks: Scope must be an object', scope, outputs) - return scope - } - return new Proxy(scope, { - get (target, property) { - const targetObj = target[property] - let targetOut = target - if (typeof property === 'string' && property in outputs) { - targetOut = outputs - } - // Reflects functions with the correct 'this' scope - if (typeof targetObj === 'function') { - return (...args) => { - return Reflect.apply(targetOut[property], target, args) - } - } else { - return Reflect.get(targetOut, property, scope) - } - }, - getOwnPropertyDescriptor (target, property) { - if (typeof property === 'string' && property === taintString) { - return { configurable: true, enumerable: false, value: true } - } - return Reflect.getOwnPropertyDescriptor(target, property) - } - }) -} - -function valToString (val) { - if (typeof val === 'function') { - return val.toString() - } - return JSON.stringify(val) -} - -/** - * Output scope variable definitions to arbitrary depth - */ -function stringifyScope (scope, scopePath) { - let output = '' - for (const [key, value] of scope) { - const varOutName = getOrGenerateIdentifier([...scopePath, key]) - if (value instanceof Map) { - const proxyName = getOrGenerateIdentifier(['_proxyFor_', varOutName]) - output += ` - let ${proxyName} = ${scopePath.join('?.')}?.${key} ? ${scopePath.join('.')}.${key} : Object.bind(null); - ` - const keys = Array.from(value.keys()) - output += stringifyScope(value, [...scopePath, key]) - const proxyOut = keys.map((keyName) => `${keyName}: ${getOrGenerateIdentifier([...scopePath, key, keyName])}`) - output += ` - let ${varOutName} = constructProxy(${proxyName}, { - ${proxyOut.join(',\n')} - }); - ` - // If we're at the top level, we need to add the window and globalThis variables (Eg: let navigator = parentScope_navigator) - if (scopePath.length === 1) { - output += ` - let ${key} = ${varOutName}; - ` - } - } else { - output += ` - let ${varOutName} = ${valToString(value)}; - ` - } - } - return output -} - -/** - * Code generates wrapping variables for code that is injected into the page - * @param {*} code - * @param {*} config - * @returns {string} - */ -export function wrapScriptCodeOverload (code, config) { - const processedConfig = {} - for (const [key, value] of Object.entries(config)) { - processedConfig[key] = processAttr(value) - } - // Don't do anything if the config is empty - if (Object.keys(processedConfig).length === 0) return code - - let prepend = '' - const aggregatedLookup = new Map() - let currentScope = null - /* Convert the config into a map of scopePath -> { key: value } */ - for (const [key, value] of Object.entries(processedConfig)) { - const path = key.split('.') - - currentScope = aggregatedLookup - const pathOut = path[path.length - 1] - // Traverse the path and create the nested objects - path.slice(0, -1).forEach((pathPart) => { - if (!currentScope.has(pathPart)) { - currentScope.set(pathPart, new Map()) - } - currentScope = currentScope.get(pathPart) - }) - currentScope.set(pathOut, value) - } - - prepend += stringifyScope(aggregatedLookup, ['parentScope']) - // Stringify top level keys - const keysOut = [...aggregatedLookup.keys()].map((keyName) => `${keyName}: ${getOrGenerateIdentifier(['parentScope', keyName])}`).join(',\n') - prepend += ` - const window = constructProxy(parentScope, { - ${keysOut} - }); - // Ensure globalThis === window - const globalThis = window - ` - return removeIndent(`(function (parentScope) { - /** - * DuckDuckGo Runtime Checks injected code. - * If you're reading this, you're probably trying to debug a site that is breaking due to our runtime checks. - * Please raise an issues on our GitHub repo: https://github.com/duckduckgo/content-scope-scripts/ - */ - ${constructProxy.toString()} - ${prepend} - - ${getContextId.toString()} - ${generateUniqueID.toString()} - ${createContextAwareFunction.toString()} - ${addTaint.toString()} - const contextID = addTaint() - - const originalSetTimeout = setTimeout - setTimeout = createContextAwareFunction(originalSetTimeout) - - const originalSetInterval = setInterval - setInterval = createContextAwareFunction(originalSetInterval) - - const originalPromiseThen = Promise.prototype.then - Promise.prototype.then = createContextAwareFunction(originalPromiseThen) - - const originalPromiseCatch = Promise.prototype.catch - Promise.prototype.catch = createContextAwareFunction(originalPromiseCatch) - - const originalPromiseFinally = Promise.prototype.finally - Promise.prototype.finally = createContextAwareFunction(originalPromiseFinally) - - ${code} - })(globalThis) - `) -} diff --git a/src/globals.d.ts b/src/globals.d.ts index 901ef73a11..735fe5f6bc 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -42,8 +42,3 @@ declare module 'ddg:platformFeatures' { const output: Record import('./content-feature').default> export default output } - -declare module 'ddg:runtimeInjects' { - const output: Record - export default output -} diff --git a/src/utils.js b/src/utils.js index 67152e2750..c8bce53e1b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -7,8 +7,6 @@ let globalObj = typeof window === 'undefined' ? globalThis : window let Error = globalObj.Error let messageSecret -export const taintSymbol = Symbol('taint') - // save a reference to original CustomEvent amd dispatchEvent so they can't be overriden to forge messages export const OriginalCustomEvent = typeof CustomEvent === 'undefined' ? null : CustomEvent export const originalWindowDispatchEvent = typeof window === 'undefined' ? null : window.dispatchEvent.bind(window) @@ -350,57 +348,6 @@ export function getContextId (scope) { } } -/** - * Returns a set of origins that are tainted - * @returns {Set | null} - */ -export function taintedOrigins () { - return getGlobalObject('taintedOrigins') -} - -/** - * Returns a set of taints - * @returns {Set | null} - */ -export function taints () { - return getGlobalObject('taints') -} - -/** - * @param {string} name - * @returns {any | null} - */ -function getGlobalObject (name) { - if ('duckduckgo' in navigator && - typeof navigator.duckduckgo === 'object' && - navigator.duckduckgo && - name in navigator.duckduckgo && - navigator.duckduckgo[name]) { - return navigator.duckduckgo[name] - } - return null -} - -export function hasTaintedMethod (scope, shouldStackCheck = false) { - if (document?.currentScript?.[taintSymbol]) return true - if ('__ddg_taint__' in window) return true - if (getContextId(scope)) return true - if (!shouldStackCheck || !taintedOrigins()) { - return false - } - const currentTaintedOrigins = taintedOrigins() - if (!currentTaintedOrigins || currentTaintedOrigins.size === 0) { - return false - } - const stackOrigins = getStackTraceOrigins(getStack()) - for (const stackOrigin of stackOrigins) { - if (currentTaintedOrigins.has(stackOrigin)) { - return true - } - } - return false -} - /** * @param {*[]} argsArray * @returns {string} @@ -438,7 +385,7 @@ export class DDGProxy { * @param {string} property * @param {ProxyObject

} proxyObject */ - constructor (feature, objectScope, property, proxyObject, taintCheck = false) { + constructor (feature, objectScope, property, proxyObject) { this.objectScope = objectScope this.property = property this.feature = feature @@ -446,19 +393,7 @@ export class DDGProxy { this.camelFeatureName = camelcase(this.featureName) const outputHandler = (...args) => { this.feature.addDebugFlag() - let isExempt = shouldExemptMethod(this.camelFeatureName) - // If taint checking is enabled for this proxy then we should verify that the method is not tainted and exempt if it isn't - if (!isExempt && taintCheck) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let scope = this - try { - // @ts-expect-error - Caller doesn't match this - // eslint-disable-next-line no-caller - scope = arguments.callee.caller - } catch {} - const isTainted = hasTaintedMethod(scope) - isExempt = !isTainted - } + const isExempt = shouldExemptMethod(this.camelFeatureName) // Keep this here as getStack() is expensive if (debug) { postDebugMessage(this.camelFeatureName, { diff --git a/src/wrapper-utils.js b/src/wrapper-utils.js index e077b4e763..dec969641a 100644 --- a/src/wrapper-utils.js +++ b/src/wrapper-utils.js @@ -107,7 +107,6 @@ export function toStringGetTrap (targetFn, mockValue) { /** * Wrap functions to fix toString but also behave as closely to their real function as possible like .name and .length etc. - * TODO: validate with firefox non runtimeChecks context and also consolidate with wrapToString * @param {*} functionValue * @param {*} realTarget * @returns {Proxy} a proxy for the function diff --git a/tsconfig.json b/tsconfig.json index 5dbb88356f..1ea7addfab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,11 +24,9 @@ "inject", "packages", "playwright.config.js", - "script-overload-snapshots", "./src/features/duckplayer/duckplayer-settings.ts" ], "exclude": [ - "snapshots/script-overload-snapshots", "integration-test/pages", "integration-test/test-pages", "integration-test/extension", diff --git a/unit-test/features.js b/unit-test/features.js index 8a1d79d3e2..34c881850b 100644 --- a/unit-test/features.js +++ b/unit-test/features.js @@ -5,7 +5,6 @@ describe('Features definition', () => { // ensuring this order doesn't change, as it recently caused breakage expect(platformSupport.apple).toEqual([ 'webCompat', - 'runtimeChecks', 'fingerprintingAudio', 'fingerprintingBattery', 'fingerprintingCanvas', diff --git a/unit-test/platform-features.js b/unit-test/platform-features.js deleted file mode 100644 index ca5826bad7..0000000000 --- a/unit-test/platform-features.js +++ /dev/null @@ -1,24 +0,0 @@ -import { join } from 'node:path' -import { rollupScript } from '../scripts/utils/build.js' -import { cwd } from '../scripts/script-utils.js' - -const CWD = join(cwd(import.meta.url), '..') - -describe('Code generation test', () => { - it('Given a list of features, only bundle the provided ones', async () => { - // Uses the snapshots generated in `npm run generate-snapshots` to ensure we don't break the output. - const actual = await rollupScript({ - scriptPath: join(CWD, 'unit-test/fixtures/feature-includes.js'), - name: 'lol', - platform: 'apple', - featureNames: ['navigatorInterface', 'gpc'] - }) - const expected = ` - var platformFeatures = { - ddg_feature_navigatorInterface: NavigatorInterface, - ddg_feature_gpc: GlobalPrivacyControl - };` - - expect(actual.includes(expected)).toBeTruthy() - }) -}) diff --git a/unit-test/runtime-checks.js b/unit-test/runtime-checks.js deleted file mode 100644 index 34ba5dabcf..0000000000 --- a/unit-test/runtime-checks.js +++ /dev/null @@ -1,67 +0,0 @@ -import { findClosestBreakpoint } from '../src/features/runtime-checks/helpers.js' - -describe('Runtime checks', () => { - describe('findClosestBreakpoint', () => { - it('no breakpoints returns null', () => { - const closest = findClosestBreakpoint([], { height: 1, width: 1 }) - expect(closest).toBeNull() - }) - - it('picks closest when exact match', () => { - const closest = findClosestBreakpoint([ - { height: 1, width: 1 }, - { height: 1024, width: 768 } - ], { height: 1024, width: 768 }) - expect(closest).toEqual({ height: 1024, width: 768 }) - }) - - it('picks closest when close matches', () => { - const closest = findClosestBreakpoint([ - { height: 1, width: 1 }, - { height: 1000, width: 700 } - ], { height: 1024, width: 768 }) - expect(closest).toEqual({ height: 1000, width: 700 }) - - const closest2 = findClosestBreakpoint([ - { height: 1, width: 1 }, - { height: 1000, width: 700 }, - { height: 2000, width: 800 } - ], { height: 1024, width: 768 }) - expect(closest2).toEqual({ height: 1000, width: 700 }) - - // Picks the first one if there's a tie - const closest3 = findClosestBreakpoint([ - { height: 1, width: 1 }, - { height: 1023, width: 768 }, - { height: 1025, width: 768 } - ], { height: 1024, width: 768 }) - expect(closest3).toEqual({ height: 1023, width: 768 }) - - const closest4 = findClosestBreakpoint([ - { height: 1, width: 1 }, - { height: 1023, width: 767 }, - { height: 1023, width: 769 }, - { height: 1025, width: 767 }, - { height: 1025, width: 769 } - ], { height: 1024, width: 768 }) - expect(closest4).toEqual({ height: 1023, width: 767 }) - }) - - it('picks closest when clear match', () => { - const breakpoints = [ - { height: 500, width: 600 }, - { height: 1024, width: 768 }, - { height: 2000, width: 1000 }, - { height: 20000, width: 8000 } - ] - const closest = findClosestBreakpoint(breakpoints, { height: 1024, width: 768 }) - expect(closest).toEqual({ height: 1024, width: 768 }) - - const closest2 = findClosestBreakpoint(breakpoints, { height: 800, width: 600 }) - expect(closest2).toEqual({ height: 1024, width: 768 }) - - const closest3 = findClosestBreakpoint(breakpoints, { height: 550, width: 600 }) - expect(closest3).toEqual({ height: 500, width: 600 }) - }) - }) -}) diff --git a/unit-test/script-overload.js b/unit-test/script-overload.js deleted file mode 100644 index abf4c0c556..0000000000 --- a/unit-test/script-overload.js +++ /dev/null @@ -1,25 +0,0 @@ -import { wrapScriptCodeOverload } from '../src/features/runtime-checks/script-overload.js' - -import { join } from 'node:path' -import { readFileSync, readdirSync } from 'node:fs' -import { cwd } from '../scripts/script-utils.js' -const ROOT = join(cwd(import.meta.url)) -const configPath = join(ROOT, '../snapshots/script-overload-snapshots/config') - -function replaceWindowsLineEndings (text) { - return text.replace(/\r/gm, '') -} - -describe('Output validation', () => { - it('Given the correct config we should generate expected code output', () => { - // Uses the snapshots generated in `npm run generate-snapshots` to ensure we don't break the output. - const files = readdirSync(configPath) - for (const fileName of files) { - const config = readFileSync(join(configPath, fileName)).toString() - const out = wrapScriptCodeOverload('console.log(1)', JSON.parse(config)) - const outName = fileName.replace(/.json$/, '.js') - const expectedOut = readFileSync(join(ROOT, '../snapshots/script-overload-snapshots/out/', outName)).toString() - expect(replaceWindowsLineEndings(out)).withContext(`wrapScriptCodeOverload with ${fileName} matches ${outName}`).toEqual(replaceWindowsLineEndings(expectedOut)) - } - }) -})