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 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.
-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 @@ - - - - - -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 @@ - - - - - -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 @@ - - - - - -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 @@ - - - - - -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 @@ - - - - - -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} 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)) - } - }) -})