From f8014e2e9e44e1fb558d0203104779980ca85556 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:22:50 +0400 Subject: [PATCH 01/41] fix: mv2 firefox csp header --- app/scripts/background.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/scripts/background.js b/app/scripts/background.js index 4fbbee449160..ce55d4716bb4 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -327,6 +327,24 @@ function maybeDetectPhishing(theController) { ); } +/** + * Overrides the Content-Security-Policy Header, acting as a workaround for an MV2 Firefox Bug. + */ +function overrideContentSecurityPolicyHeader() { + browser.webRequest.onHeadersReceived.addListener( + ({ responseHeaders }) => { + for (const header of responseHeaders) { + if (header.name.toLowerCase() === 'content-security-policy') { + header.value = ''; + } + } + return { responseHeaders }; + }, + { urls: ['http://*/*', 'https://*/*'] }, + ['blocking', 'responseHeaders'], + ); +} + // These are set after initialization let connectRemote; let connectExternalExtension; @@ -473,6 +491,10 @@ async function initialize() { if (!isManifestV3) { await loadPhishingWarningPage(); + const { name } = await browser.runtime.getBrowserInfo(); + if (name === PLATFORM_FIREFOX) { + overrideContentSecurityPolicyHeader(); + } } await sendReadyMessageToTabs(); log.info('MetaMask initialization complete.'); From c6bd1bd10ef11795e48d1504fac4ec58dc0bbee5 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 15 Oct 2024 00:45:21 +0400 Subject: [PATCH 02/41] feat: nonce --- app/scripts/background.js | 5 ++++- development/build/utils.js | 2 +- development/webpack/utils/plugins/SelfInjectPlugin/index.ts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index ce55d4716bb4..ba33571a8c4c 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -335,7 +335,10 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - header.value = ''; + header.value = header.value.replace( + /script-src([^;]*)/u, + (match) => `${match} 'nonce-inpage'`, + ); } } return { responseHeaders }; diff --git a/development/build/utils.js b/development/build/utils.js index 525815d2520a..0263419c9e75 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -293,7 +293,7 @@ function getBuildName({ function makeSelfInjecting(filePath) { const fileContents = readFileSync(filePath, 'utf8'); const textContent = JSON.stringify(fileContents); - const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};d.documentElement.appendChild(s).remove();}`; + const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce='inpage';d.documentElement.appendChild(s).remove();}`; writeFileSync(filePath, js, 'utf8'); } diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index b80f6102ab75..3b8c2c7f1084 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -142,6 +142,7 @@ export class SelfInjectPlugin { `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, ); newSource.add(`;`); + newSource.add(`s.nonce='inpage';`); // add and immediately remove the script to avoid modifying the DOM. newSource.add(`d.documentElement.appendChild(s).remove()`); newSource.add(`}`); From 190524739c2acb4b900877f0b8bfd1eaa7fc91d7 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:29:30 +0400 Subject: [PATCH 03/41] fix: nonce --- app/scripts/background.js | 2 +- development/build/utils.js | 2 +- development/webpack/utils/plugins/SelfInjectPlugin/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index ba33571a8c4c..9c7872115110 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -337,7 +337,7 @@ function overrideContentSecurityPolicyHeader() { if (header.name.toLowerCase() === 'content-security-policy') { header.value = header.value.replace( /script-src([^;]*)/u, - (match) => `${match} 'nonce-inpage'`, + (match) => `${match} 'nonce-${btoa(browser.runtime.id)}'`, ); } } diff --git a/development/build/utils.js b/development/build/utils.js index 0263419c9e75..3e21ef550128 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -293,7 +293,7 @@ function getBuildName({ function makeSelfInjecting(filePath) { const fileContents = readFileSync(filePath, 'utf8'); const textContent = JSON.stringify(fileContents); - const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce='inpage';d.documentElement.appendChild(s).remove();}`; + const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa(browser.runtime.id);d.documentElement.appendChild(s).remove();}`; writeFileSync(filePath, js, 'utf8'); } diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index 3b8c2c7f1084..98bdf5b5f28e 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -142,7 +142,7 @@ export class SelfInjectPlugin { `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, ); newSource.add(`;`); - newSource.add(`s.nonce='inpage';`); + newSource.add(`s.nonce=btoa(browser.runtime.id);`); // add and immediately remove the script to avoid modifying the DOM. newSource.add(`d.documentElement.appendChild(s).remove()`); newSource.add(`}`); From cd551fd85635fabf9ae2b8df09948fcb9041c7a0 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:26:07 +0400 Subject: [PATCH 04/41] fix: random url --- app/scripts/background.js | 2 +- development/build/utils.js | 2 +- development/webpack/utils/plugins/SelfInjectPlugin/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 9c7872115110..11a211fdfb30 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -337,7 +337,7 @@ function overrideContentSecurityPolicyHeader() { if (header.name.toLowerCase() === 'content-security-policy') { header.value = header.value.replace( /script-src([^;]*)/u, - (match) => `${match} 'nonce-${btoa(browser.runtime.id)}'`, + (match) => `${match} 'nonce-${btoa(browser.runtime.getURL('/'))}'`, ); } } diff --git a/development/build/utils.js b/development/build/utils.js index 3e21ef550128..bfc1e6417ecb 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -293,7 +293,7 @@ function getBuildName({ function makeSelfInjecting(filePath) { const fileContents = readFileSync(filePath, 'utf8'); const textContent = JSON.stringify(fileContents); - const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa(browser.runtime.id);d.documentElement.appendChild(s).remove();}`; + const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa(browser.runtime.getURL('/'));d.documentElement.appendChild(s).remove();}`; writeFileSync(filePath, js, 'utf8'); } diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index 98bdf5b5f28e..6dc0abd98829 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -142,7 +142,7 @@ export class SelfInjectPlugin { `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, ); newSource.add(`;`); - newSource.add(`s.nonce=btoa(browser.runtime.id);`); + newSource.add(`s.nonce=btoa(browser.runtime.getURL('/'));`); // add and immediately remove the script to avoid modifying the DOM. newSource.add(`d.documentElement.appendChild(s).remove()`); newSource.add(`}`); From d1a84c24832a6bcf2aa7fd365df96798a3f58a8a Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:53:50 +0400 Subject: [PATCH 05/41] fix: webpack tests --- development/webpack/test/plugins.SelfInjectPlugin.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/development/webpack/test/plugins.SelfInjectPlugin.test.ts b/development/webpack/test/plugins.SelfInjectPlugin.test.ts index 3a3ef729eacf..c94f93df7234 100644 --- a/development/webpack/test/plugins.SelfInjectPlugin.test.ts +++ b/development/webpack/test/plugins.SelfInjectPlugin.test.ts @@ -55,7 +55,7 @@ describe('SelfInjectPlugin', () => { // reference the `sourceMappingURL` assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa(browser.runtime.getURL('/'));d.documentElement.appendChild(s).remove()}`, ); } else { // the new source should NOT reference the new sourcemap, since it's @@ -66,7 +66,7 @@ describe('SelfInjectPlugin', () => { // console. assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa(browser.runtime.getURL('/'));d.documentElement.appendChild(s).remove()}`, ); } From 885c3207159a87adabc1f8ec1dfc0855e0176f06 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:33:32 +0400 Subject: [PATCH 06/41] fix: regex --- app/scripts/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 6c28d2b4082b..69478fc19e14 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -336,7 +336,7 @@ function overrideContentSecurityPolicyHeader() { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { header.value = header.value.replace( - /script-src([^;]*)/u, + /(^|;\s*)script-src([^;]*)/u, (match) => `${match} 'nonce-${btoa(browser.runtime.getURL('/'))}'`, ); } From ba383e4b8bc835aced0c5412b58043b343c0eacc Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:57:07 +0400 Subject: [PATCH 07/41] fix: regex whitespace --- app/scripts/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 69478fc19e14..a2fe9a20c030 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -336,7 +336,7 @@ function overrideContentSecurityPolicyHeader() { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { header.value = header.value.replace( - /(^|;\s*)script-src([^;]*)/u, + /(^|;[\t\n\f\r ]*)script-src([^;]*)/u, (match) => `${match} 'nonce-${btoa(browser.runtime.getURL('/'))}'`, ); } From c048d41d1c3460702447a65b441f31ecbe1638c8 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:38:49 +0400 Subject: [PATCH 08/41] feat: addNonceToCsp with tests --- app/scripts/background.js | 7 ++- shared/modules/add-nonce-to-csp.test.ts | 77 +++++++++++++++++++++++++ shared/modules/add-nonce-to-csp.ts | 13 +++++ 3 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 shared/modules/add-nonce-to-csp.test.ts create mode 100644 shared/modules/add-nonce-to-csp.ts diff --git a/app/scripts/background.js b/app/scripts/background.js index a2fe9a20c030..67641859cd1b 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,6 +56,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; +import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -335,9 +336,9 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - header.value = header.value.replace( - /(^|;[\t\n\f\r ]*)script-src([^;]*)/u, - (match) => `${match} 'nonce-${btoa(browser.runtime.getURL('/'))}'`, + header.value = addNonceToCsp( + header.value, + btoa(browser.runtime.getURL('/')), ); } } diff --git a/shared/modules/add-nonce-to-csp.test.ts b/shared/modules/add-nonce-to-csp.test.ts new file mode 100644 index 000000000000..d461a7973a8a --- /dev/null +++ b/shared/modules/add-nonce-to-csp.test.ts @@ -0,0 +1,77 @@ +import { addNonceToCsp } from './add-nonce-to-csp'; + +describe('addNonceToCsp', () => { + it('empty string', () => { + const input = ''; + const expected = ''; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one empty directive', () => { + const input = 'script-src'; + const expected = `script-src 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, one value', () => { + const input = 'script-src default.example'; + const expected = `script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, two values', () => { + const input = "script-src 'self' default.example"; + const expected = `script-src 'self' default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('multiple directives', () => { + const input = + "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example"; + const expected = `default-src 'self'; script-src 'unsafe-eval' scripts.example 'nonce-test'; object-src; style-src styles.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('non-ASCII directives', () => { + const input = 'script-src default.example;\u0080;style-src style.example'; + const expected = `script-src default.example 'nonce-test';\u0080;style-src style.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('uppercase directive names', () => { + const input = 'SCRIPT-SRC DEFAULT.EXAMPLE'; + const expected = `SCRIPT-SRC DEFAULT.EXAMPLE 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('duplicate directive names', () => { + const input = + 'default-src default.example; script-src script.example; script-src script.example'; + const expected = `default-src default.example; script-src script.example 'nonce-test'; script-src script.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('nonce value contains script-src', () => { + const input = + "default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com"; + const expected = `default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('url value contains script-src', () => { + const input = + "default-src 'self' https://script-src.com; script-src 'self' https://example.com"; + const expected = `default-src 'self' https://script-src.com; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); +}); diff --git a/shared/modules/add-nonce-to-csp.ts b/shared/modules/add-nonce-to-csp.ts new file mode 100644 index 000000000000..8949bc9811f8 --- /dev/null +++ b/shared/modules/add-nonce-to-csp.ts @@ -0,0 +1,13 @@ +/** + * Adds a nonce value to the script-src directive of the Content-Security-Policy Header. + * + * @param header - the Content-Security-Policy Header + * @param nonce - the nonce value to add + * @returns the Content-Security-Policy Header with the nonce value added + */ +export const addNonceToCsp = (header: string, nonce: string) => { + return header.replace( + /(^|;[\t\n\f\r ]*)script-src([^;]*)/iu, + (match) => `${match} 'nonce-${nonce}'`, + ); +}; From 95b3ad85622c3adbeb5c591dca0bcf67c28de6c8 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:49:00 +0400 Subject: [PATCH 09/41] fix: limit types --- app/scripts/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 317efc99a255..a8143ae8c2c0 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -348,7 +348,7 @@ function overrideContentSecurityPolicyHeader() { } return { responseHeaders }; }, - { urls: ['http://*/*', 'https://*/*'] }, + { types: ['main_frame', 'sub_frame'], urls: ['http://*/*', 'https://*/*'] }, ['blocking', 'responseHeaders'], ); } From ab2896d09632d5e7c76974e2c56b0c7b72330228 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 17 Oct 2024 22:52:56 +0400 Subject: [PATCH 10/41] fix: getPlatform --- app/scripts/background.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index a8143ae8c2c0..5448a19fdec1 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -499,8 +499,8 @@ async function initialize() { if (!isManifestV3) { await loadPhishingWarningPage(); - const { name } = await browser.runtime.getBrowserInfo(); - if (name === PLATFORM_FIREFOX) { + const platform = getPlatform(); + if (platform === PLATFORM_FIREFOX) { overrideContentSecurityPolicyHeader(); } } From 9a7cd358064517c3fa3591fa8a65cd1180ce3805 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Fri, 18 Oct 2024 00:17:54 +0400 Subject: [PATCH 11/41] feat: CSP --- app/scripts/background.js | 15 +++-- shared/modules/add-nonce-to-csp.test.ts | 77 ----------------------- shared/modules/add-nonce-to-csp.ts | 13 ---- shared/modules/content-security-policy.ts | 65 +++++++++++++++++++ 4 files changed, 76 insertions(+), 94 deletions(-) delete mode 100644 shared/modules/add-nonce-to-csp.test.ts delete mode 100644 shared/modules/add-nonce-to-csp.ts create mode 100644 shared/modules/content-security-policy.ts diff --git a/app/scripts/background.js b/app/scripts/background.js index 5448a19fdec1..98a4710a72d7 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,7 +56,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; -import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp'; +import { CSP } from '../../shared/modules/content-security-policy'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -340,10 +340,17 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - header.value = addNonceToCsp( - header.value, - btoa(browser.runtime.getURL('/')), + const contentSecurityPolicy = CSP.parse(header.value); + const nonce = `'nonce-${btoa(browser.runtime.getURL('/'))}'`; + const scriptSrc = Object.keys(contentSecurityPolicy).find( + (directive) => directive.toLowerCase() === 'script-src', ); + if (scriptSrc) { + contentSecurityPolicy[scriptSrc].push(nonce); + } else { + contentSecurityPolicy['script-src'] = [nonce]; + } + header.value = CSP.stringify(contentSecurityPolicy); } } return { responseHeaders }; diff --git a/shared/modules/add-nonce-to-csp.test.ts b/shared/modules/add-nonce-to-csp.test.ts deleted file mode 100644 index d461a7973a8a..000000000000 --- a/shared/modules/add-nonce-to-csp.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { addNonceToCsp } from './add-nonce-to-csp'; - -describe('addNonceToCsp', () => { - it('empty string', () => { - const input = ''; - const expected = ''; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('one empty directive', () => { - const input = 'script-src'; - const expected = `script-src 'nonce-test'`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('one directive, one value', () => { - const input = 'script-src default.example'; - const expected = `script-src default.example 'nonce-test'`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('one directive, two values', () => { - const input = "script-src 'self' default.example"; - const expected = `script-src 'self' default.example 'nonce-test'`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('multiple directives', () => { - const input = - "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example"; - const expected = `default-src 'self'; script-src 'unsafe-eval' scripts.example 'nonce-test'; object-src; style-src styles.example`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('non-ASCII directives', () => { - const input = 'script-src default.example;\u0080;style-src style.example'; - const expected = `script-src default.example 'nonce-test';\u0080;style-src style.example`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('uppercase directive names', () => { - const input = 'SCRIPT-SRC DEFAULT.EXAMPLE'; - const expected = `SCRIPT-SRC DEFAULT.EXAMPLE 'nonce-test'`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('duplicate directive names', () => { - const input = - 'default-src default.example; script-src script.example; script-src script.example'; - const expected = `default-src default.example; script-src script.example 'nonce-test'; script-src script.example`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('nonce value contains script-src', () => { - const input = - "default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com"; - const expected = `default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com 'nonce-test'`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); - - it('url value contains script-src', () => { - const input = - "default-src 'self' https://script-src.com; script-src 'self' https://example.com"; - const expected = `default-src 'self' https://script-src.com; script-src 'self' https://example.com 'nonce-test'`; - const output = addNonceToCsp(input, 'test'); - expect(output).toBe(expected); - }); -}); diff --git a/shared/modules/add-nonce-to-csp.ts b/shared/modules/add-nonce-to-csp.ts deleted file mode 100644 index 8949bc9811f8..000000000000 --- a/shared/modules/add-nonce-to-csp.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Adds a nonce value to the script-src directive of the Content-Security-Policy Header. - * - * @param header - the Content-Security-Policy Header - * @param nonce - the nonce value to add - * @returns the Content-Security-Policy Header with the nonce value added - */ -export const addNonceToCsp = (header: string, nonce: string) => { - return header.replace( - /(^|;[\t\n\f\r ]*)script-src([^;]*)/iu, - (match) => `${match} 'nonce-${nonce}'`, - ); -}; diff --git a/shared/modules/content-security-policy.ts b/shared/modules/content-security-policy.ts new file mode 100644 index 000000000000..e44371a24a7a --- /dev/null +++ b/shared/modules/content-security-policy.ts @@ -0,0 +1,65 @@ +// ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. +// See . +const ASCII_WHITESPACE_CHARS = '\t\n\f\r '; +const ASCII_WHITESPACE = RegExp(`[${ASCII_WHITESPACE_CHARS}]+`); +const ASCII_WHITESPACE_AT_START = RegExp(`^[${ASCII_WHITESPACE_CHARS}]+`); +const ASCII_WHITESPACE_AT_END = RegExp(`[${ASCII_WHITESPACE_CHARS}]+$`); + +// An ASCII code point is a code point in the range U+0000 NULL to U+007F DELETE, inclusive. +// See . +const ASCII = /^[\x00-\x7f]*$/; + +export type ContentSecurityPolicy = Record; + +/** + * An intrinsic object that provides functions to handle the Content Security Policy (CSP) format. + */ +export const CSP = { + /** + * Converts a Content Security Policy (CSP) string into an object according to [the spec][0]. + * + * [0]: https://w3c.github.io/webappsec-csp/#parse-serialized-policy + * + * @param text The Content Security Policy (CSP) string to parse. + * @returns A Content Security Policy (CSP) object. + */ + parse: (text: string) => { + const contentSecurityPolicy: ContentSecurityPolicy = {}; + + // For each token returned by strictly splitting serialized on the + // U+003B SEMICOLON character (;): + const tokens = text.split(';'); + + for (const token of tokens) { + // Strip leading and trailing ASCII whitespace from token. + const strippedToken = token + .replace(ASCII_WHITESPACE_AT_START, '') + .replace(ASCII_WHITESPACE_AT_END, ''); + + // If strippedToken is an empty string, or if strippedToken is not an ASCII string, continue. + if (!strippedToken || !ASCII.test(strippedToken)) continue; + + // Directive name is the result of collecting a sequence of code points from token which are not ASCII whitespace. + // Directive values are the result of splitting token on ASCII whitespace. + const [name, ...values] = strippedToken.split(ASCII_WHITESPACE); + + contentSecurityPolicy[name] = values; + } + + return contentSecurityPolicy; + }, + /** + * Converts a Content Security Policy (CSP) object into a string. + * + * @param contentSecurityPolicy The Content Security Policy (CSP) object to stringify. + * @returns A Content Security Policy (CSP) string. + */ + stringify: (contentSecurityPolicy: ContentSecurityPolicy) => { + return Object.entries(contentSecurityPolicy) + .map(([name, values]) => { + const value = values.length ? ` ${values.join(' ')}` : ''; + return `${name}${value}`; + }) + .join('; '); + }, +}; From d1e0bcece6f1c9d55ffce180917b20d592cfb0c2 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:40:20 +0400 Subject: [PATCH 12/41] fix: new RegExp --- shared/modules/content-security-policy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared/modules/content-security-policy.ts b/shared/modules/content-security-policy.ts index e44371a24a7a..b307c275b8ff 100644 --- a/shared/modules/content-security-policy.ts +++ b/shared/modules/content-security-policy.ts @@ -1,9 +1,9 @@ // ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. // See . const ASCII_WHITESPACE_CHARS = '\t\n\f\r '; -const ASCII_WHITESPACE = RegExp(`[${ASCII_WHITESPACE_CHARS}]+`); -const ASCII_WHITESPACE_AT_START = RegExp(`^[${ASCII_WHITESPACE_CHARS}]+`); -const ASCII_WHITESPACE_AT_END = RegExp(`[${ASCII_WHITESPACE_CHARS}]+$`); +const ASCII_WHITESPACE = new RegExp(`[${ASCII_WHITESPACE_CHARS}]+`); +const ASCII_WHITESPACE_AT_START = new RegExp(`^[${ASCII_WHITESPACE_CHARS}]+`); +const ASCII_WHITESPACE_AT_END = new RegExp(`[${ASCII_WHITESPACE_CHARS}]+$`); // An ASCII code point is a code point in the range U+0000 NULL to U+007F DELETE, inclusive. // See . From 267a35c8e1be7d49b7a35420a3b099b3d85f2e5a Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Fri, 18 Oct 2024 20:48:13 +0400 Subject: [PATCH 13/41] feat: comments --- app/scripts/background.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 98a4710a72d7..9d4a80c9966f 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -333,7 +333,9 @@ function maybeDetectPhishing(theController) { } /** - * Overrides the Content-Security-Policy Header, acting as a workaround for an MV2 Firefox Bug. + * Overrides the Content-Security-Policy (CSP) header by adding a nonce to the `script-src` directive. + * This is a workaround for [Bug #1446231](https://bugzilla.mozilla.org/show_bug.cgi?id=1446231), + * which involves overriding the page CSP for inline script nodes injected by extension content scripts. */ function overrideContentSecurityPolicyHeader() { browser.webRequest.onHeadersReceived.addListener( @@ -506,6 +508,8 @@ async function initialize() { if (!isManifestV3) { await loadPhishingWarningPage(); + // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts + // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 const platform = getPlatform(); if (platform === PLATFORM_FIREFOX) { overrideContentSecurityPolicyHeader(); From 52053ab5c299881c6dbddd7dfb2d97a85b17228d Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:59:06 +0400 Subject: [PATCH 14/41] feat: array of directives --- app/scripts/background.js | 17 +++++++++------ shared/modules/content-security-policy.ts | 25 +++++++++++++---------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 9d4a80c9966f..36244408b916 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -342,17 +342,22 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - const contentSecurityPolicy = CSP.parse(header.value); + const directives = CSP.parse(header.value); const nonce = `'nonce-${btoa(browser.runtime.getURL('/'))}'`; - const scriptSrc = Object.keys(contentSecurityPolicy).find( - (directive) => directive.toLowerCase() === 'script-src', + const scriptSrc = directives.find( + (directive) => directive.name.toLowerCase() === 'script-src', ); if (scriptSrc) { - contentSecurityPolicy[scriptSrc].push(nonce); + scriptSrc.values.push(nonce); } else { - contentSecurityPolicy['script-src'] = [nonce]; + const defaultSrc = directives.find( + (directive) => directive.name.toLowerCase() === 'default-src', + ); + if (defaultSrc) { + defaultSrc.values.push(nonce); + } } - header.value = CSP.stringify(contentSecurityPolicy); + header.value = CSP.stringify(directives); } } return { responseHeaders }; diff --git a/shared/modules/content-security-policy.ts b/shared/modules/content-security-policy.ts index b307c275b8ff..76361c594946 100644 --- a/shared/modules/content-security-policy.ts +++ b/shared/modules/content-security-policy.ts @@ -9,22 +9,25 @@ const ASCII_WHITESPACE_AT_END = new RegExp(`[${ASCII_WHITESPACE_CHARS}]+$`); // See . const ASCII = /^[\x00-\x7f]*$/; -export type ContentSecurityPolicy = Record; +export interface ContentSecurityPolicyDirective { + name: string; + values: string[]; +} /** * An intrinsic object that provides functions to handle the Content Security Policy (CSP) format. */ export const CSP = { /** - * Converts a Content Security Policy (CSP) string into an object according to [the spec][0]. + * Converts a Content Security Policy (CSP) string into an array of directives according to [the spec][0]. * * [0]: https://w3c.github.io/webappsec-csp/#parse-serialized-policy * * @param text The Content Security Policy (CSP) string to parse. - * @returns A Content Security Policy (CSP) object. + * @returns An array of Content Security Policy (CSP) directives. */ parse: (text: string) => { - const contentSecurityPolicy: ContentSecurityPolicy = {}; + const directives: ContentSecurityPolicyDirective[] = []; // For each token returned by strictly splitting serialized on the // U+003B SEMICOLON character (;): @@ -43,20 +46,20 @@ export const CSP = { // Directive values are the result of splitting token on ASCII whitespace. const [name, ...values] = strippedToken.split(ASCII_WHITESPACE); - contentSecurityPolicy[name] = values; + directives.push({ name, values }); } - return contentSecurityPolicy; + return directives; }, /** - * Converts a Content Security Policy (CSP) object into a string. + * Converts an array of Content Security Policy (CSP) directives into a string. * - * @param contentSecurityPolicy The Content Security Policy (CSP) object to stringify. + * @param directives An array of Content Security Policy (CSP) directives to stringify. * @returns A Content Security Policy (CSP) string. */ - stringify: (contentSecurityPolicy: ContentSecurityPolicy) => { - return Object.entries(contentSecurityPolicy) - .map(([name, values]) => { + stringify: (directives: ContentSecurityPolicyDirective[]) => { + return directives + .map(({ name, values }) => { const value = values.length ? ` ${values.join(' ')}` : ''; return `${name}${value}`; }) From 8f68b2a46c78caf59b778664c39de89e0c3fde09 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:54:34 +0400 Subject: [PATCH 15/41] fix: lint --- app/scripts/background.js | 4 ++-- shared/modules/content-security-policy.ts | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 36244408b916..3553326ffabd 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -515,8 +515,8 @@ async function initialize() { await loadPhishingWarningPage(); // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 - const platform = getPlatform(); - if (platform === PLATFORM_FIREFOX) { + const platformName = getPlatform(); + if (platformName === PLATFORM_FIREFOX) { overrideContentSecurityPolicyHeader(); } } diff --git a/shared/modules/content-security-policy.ts b/shared/modules/content-security-policy.ts index 76361c594946..3fc6ffd29a5d 100644 --- a/shared/modules/content-security-policy.ts +++ b/shared/modules/content-security-policy.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-control-regex, require-unicode-regexp */ // ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. // See . const ASCII_WHITESPACE_CHARS = '\t\n\f\r '; @@ -9,10 +10,10 @@ const ASCII_WHITESPACE_AT_END = new RegExp(`[${ASCII_WHITESPACE_CHARS}]+$`); // See . const ASCII = /^[\x00-\x7f]*$/; -export interface ContentSecurityPolicyDirective { +export type ContentSecurityPolicyDirective = { name: string; values: string[]; -} +}; /** * An intrinsic object that provides functions to handle the Content Security Policy (CSP) format. @@ -23,7 +24,7 @@ export const CSP = { * * [0]: https://w3c.github.io/webappsec-csp/#parse-serialized-policy * - * @param text The Content Security Policy (CSP) string to parse. + * @param text - The Content Security Policy (CSP) string to parse. * @returns An array of Content Security Policy (CSP) directives. */ parse: (text: string) => { @@ -40,7 +41,9 @@ export const CSP = { .replace(ASCII_WHITESPACE_AT_END, ''); // If strippedToken is an empty string, or if strippedToken is not an ASCII string, continue. - if (!strippedToken || !ASCII.test(strippedToken)) continue; + if (!strippedToken || !ASCII.test(strippedToken)) { + continue; + } // Directive name is the result of collecting a sequence of code points from token which are not ASCII whitespace. // Directive values are the result of splitting token on ASCII whitespace. @@ -54,7 +57,7 @@ export const CSP = { /** * Converts an array of Content Security Policy (CSP) directives into a string. * - * @param directives An array of Content Security Policy (CSP) directives to stringify. + * @param directives - An array of Content Security Policy (CSP) directives to stringify. * @returns A Content Security Policy (CSP) string. */ stringify: (directives: ContentSecurityPolicyDirective[]) => { From 9baaa8624081ad8610cfc8ba8e4613fd850eaabe Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:09:04 +0400 Subject: [PATCH 16/41] feat: respect whitespace --- app/scripts/background.js | 18 +--- shared/modules/content-security-policy.ts | 108 ++++++++++++++++------ 2 files changed, 83 insertions(+), 43 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 3553326ffabd..52237d7d1878 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -342,22 +342,10 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - const directives = CSP.parse(header.value); - const nonce = `'nonce-${btoa(browser.runtime.getURL('/'))}'`; - const scriptSrc = directives.find( - (directive) => directive.name.toLowerCase() === 'script-src', + header.value = CSP.addNonce( + header.value, + btoa(browser.runtime.getURL('/')), ); - if (scriptSrc) { - scriptSrc.values.push(nonce); - } else { - const defaultSrc = directives.find( - (directive) => directive.name.toLowerCase() === 'default-src', - ); - if (defaultSrc) { - defaultSrc.values.push(nonce); - } - } - header.value = CSP.stringify(directives); } } return { responseHeaders }; diff --git a/shared/modules/content-security-policy.ts b/shared/modules/content-security-policy.ts index 3fc6ffd29a5d..f4ade9b63a8f 100644 --- a/shared/modules/content-security-policy.ts +++ b/shared/modules/content-security-policy.ts @@ -1,18 +1,56 @@ -/* eslint-disable no-control-regex, require-unicode-regexp */ // ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. // See . -const ASCII_WHITESPACE_CHARS = '\t\n\f\r '; -const ASCII_WHITESPACE = new RegExp(`[${ASCII_WHITESPACE_CHARS}]+`); -const ASCII_WHITESPACE_AT_START = new RegExp(`^[${ASCII_WHITESPACE_CHARS}]+`); -const ASCII_WHITESPACE_AT_END = new RegExp(`[${ASCII_WHITESPACE_CHARS}]+$`); +const ASCII_WHITESPACE_CHARS = ['\t', '\n', '\f', '\r', ' '].join(''); -// An ASCII code point is a code point in the range U+0000 NULL to U+007F DELETE, inclusive. -// See . -const ASCII = /^[\x00-\x7f]*$/; +// A Content Security Policy is described using a series of policy directives +// each of which describes the policy for a certain resource type or policy area. +// See . +const CSP_DIRECTIVES = [ + // Fetch directives + 'child-src', + 'connect-src', + 'default-src', + 'fenced-frame-src', + 'font-src', + 'frame-src', + 'img-src', + 'manifest-src', + 'media-src', + 'object-src', + 'prefetch-src', + 'script-src', + 'script-src-elem', + 'script-src-attr', + 'style-src', + 'style-src-elem', + 'style-src-attr', + 'worker-src', + // Document directives + 'base-uri', + 'sandbox', + // Navigation directives + 'form-action', + 'frame-ancestors', + // Reporting directives + 'report-to', + // Other directives + 'require-trusted-types-for', + 'trusted-types', + 'upgrade-insecure-requests', + // Deprecated directives + 'block-all-mixed-content', + 'report-uri', +].join('|'); + +/* eslint-disable require-unicode-regexp */ +const MATCH_CSP_DIRECTIVES = new RegExp( + `^([${ASCII_WHITESPACE_CHARS}]*(${CSP_DIRECTIVES}))`, // Match any directive and leading ASCII whitespace + 'is', // Case-insensitive, including newlines +); export type ContentSecurityPolicyDirective = { name: string; - values: string[]; + values: string; }; /** @@ -33,22 +71,16 @@ export const CSP = { // For each token returned by strictly splitting serialized on the // U+003B SEMICOLON character (;): const tokens = text.split(';'); - for (const token of tokens) { - // Strip leading and trailing ASCII whitespace from token. - const strippedToken = token - .replace(ASCII_WHITESPACE_AT_START, '') - .replace(ASCII_WHITESPACE_AT_END, ''); - - // If strippedToken is an empty string, or if strippedToken is not an ASCII string, continue. - if (!strippedToken || !ASCII.test(strippedToken)) { + const result = token.match(MATCH_CSP_DIRECTIVES); + if (!result) { continue; } - - // Directive name is the result of collecting a sequence of code points from token which are not ASCII whitespace. - // Directive values are the result of splitting token on ASCII whitespace. - const [name, ...values] = strippedToken.split(ASCII_WHITESPACE); - + const name = result[0]; + if (!name) { + continue; + } + const values = token.slice(name.length); directives.push({ name, values }); } @@ -61,11 +93,31 @@ export const CSP = { * @returns A Content Security Policy (CSP) string. */ stringify: (directives: ContentSecurityPolicyDirective[]) => { - return directives - .map(({ name, values }) => { - const value = values.length ? ` ${values.join(' ')}` : ''; - return `${name}${value}`; - }) - .join('; '); + return directives.map(({ name, values }) => `${name}${values}`).join(';'); + }, + /** + * Adds a nonce to a Content Security Policy (CSP) string. + * + * @param text - The Content Security Policy (CSP) string to add the nonce to. + * @param nonce - The nonce to add to the Content Security Policy (CSP) string. + * @returns The updated Content Security Policy (CSP) string. + */ + addNonce: (text: string, nonce: string) => { + const directives = CSP.parse(text); + const formattedNonce = ` 'nonce-${nonce}'`; + const scriptSrc = directives.find((directive) => + directive.name.toLowerCase().includes('script-src'), + ); + if (scriptSrc) { + scriptSrc.values += formattedNonce; + } else { + const defaultSrc = directives.find((directive) => + directive.name.toLowerCase().includes('default-src'), + ); + if (defaultSrc) { + defaultSrc.values += formattedNonce; + } + } + return CSP.stringify(directives); }, }; From fe9188ec43e44a5894f785e374c0b4ba3850b1d7 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:15:04 +0400 Subject: [PATCH 17/41] feat: make it simpler --- app/scripts/background.js | 4 +- shared/modules/content-security-policy.ts | 141 +++++----------------- 2 files changed, 30 insertions(+), 115 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 52237d7d1878..915d8d82c2b9 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,7 +56,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; -import { CSP } from '../../shared/modules/content-security-policy'; +import { addNonceToCSP } from '../../shared/modules/content-security-policy'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -342,7 +342,7 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - header.value = CSP.addNonce( + header.value = addNonceToCSP( header.value, btoa(browser.runtime.getURL('/')), ); diff --git a/shared/modules/content-security-policy.ts b/shared/modules/content-security-policy.ts index f4ade9b63a8f..e26f2201f197 100644 --- a/shared/modules/content-security-policy.ts +++ b/shared/modules/content-security-policy.ts @@ -2,122 +2,37 @@ // See . const ASCII_WHITESPACE_CHARS = ['\t', '\n', '\f', '\r', ' '].join(''); -// A Content Security Policy is described using a series of policy directives -// each of which describes the policy for a certain resource type or policy area. -// See . -const CSP_DIRECTIVES = [ - // Fetch directives - 'child-src', - 'connect-src', - 'default-src', - 'fenced-frame-src', - 'font-src', - 'frame-src', - 'img-src', - 'manifest-src', - 'media-src', - 'object-src', - 'prefetch-src', - 'script-src', - 'script-src-elem', - 'script-src-attr', - 'style-src', - 'style-src-elem', - 'style-src-attr', - 'worker-src', - // Document directives - 'base-uri', - 'sandbox', - // Navigation directives - 'form-action', - 'frame-ancestors', - // Reporting directives - 'report-to', - // Other directives - 'require-trusted-types-for', - 'trusted-types', - 'upgrade-insecure-requests', - // Deprecated directives - 'block-all-mixed-content', - 'report-uri', -].join('|'); - -/* eslint-disable require-unicode-regexp */ -const MATCH_CSP_DIRECTIVES = new RegExp( - `^([${ASCII_WHITESPACE_CHARS}]*(${CSP_DIRECTIVES}))`, // Match any directive and leading ASCII whitespace - 'is', // Case-insensitive, including newlines -); - -export type ContentSecurityPolicyDirective = { - name: string; - values: string; -}; +const matchDirective = (directive: string) => + /* eslint-disable require-unicode-regexp */ + new RegExp( + `^([${ASCII_WHITESPACE_CHARS}]*${directive}[${ASCII_WHITESPACE_CHARS}]*)`, // Match the directive and surrounding ASCII whitespace + 'is', // Case-insensitive, including newlines + ); +const matchScript = matchDirective('script-src'); +const matchDefault = matchDirective('default-src'); /** - * An intrinsic object that provides functions to handle the Content Security Policy (CSP) format. + * Adds a nonce to a Content Security Policy (CSP) string. + * + * @param text - The Content Security Policy (CSP) string to add the nonce to. + * @param nonce - The nonce to add to the Content Security Policy (CSP) string. + * @returns The updated Content Security Policy (CSP) string. */ -export const CSP = { - /** - * Converts a Content Security Policy (CSP) string into an array of directives according to [the spec][0]. - * - * [0]: https://w3c.github.io/webappsec-csp/#parse-serialized-policy - * - * @param text - The Content Security Policy (CSP) string to parse. - * @returns An array of Content Security Policy (CSP) directives. - */ - parse: (text: string) => { - const directives: ContentSecurityPolicyDirective[] = []; - - // For each token returned by strictly splitting serialized on the - // U+003B SEMICOLON character (;): - const tokens = text.split(';'); - for (const token of tokens) { - const result = token.match(MATCH_CSP_DIRECTIVES); - if (!result) { - continue; - } - const name = result[0]; - if (!name) { - continue; - } - const values = token.slice(name.length); - directives.push({ name, values }); - } - - return directives; - }, - /** - * Converts an array of Content Security Policy (CSP) directives into a string. - * - * @param directives - An array of Content Security Policy (CSP) directives to stringify. - * @returns A Content Security Policy (CSP) string. - */ - stringify: (directives: ContentSecurityPolicyDirective[]) => { - return directives.map(({ name, values }) => `${name}${values}`).join(';'); - }, - /** - * Adds a nonce to a Content Security Policy (CSP) string. - * - * @param text - The Content Security Policy (CSP) string to add the nonce to. - * @param nonce - The nonce to add to the Content Security Policy (CSP) string. - * @returns The updated Content Security Policy (CSP) string. - */ - addNonce: (text: string, nonce: string) => { - const directives = CSP.parse(text); - const formattedNonce = ` 'nonce-${nonce}'`; - const scriptSrc = directives.find((directive) => - directive.name.toLowerCase().includes('script-src'), +export const addNonceToCSP = (text: string, nonce: string) => { + const formattedNonce = ` 'nonce-${nonce}'`; + const directives = text.split(';'); + const scriptIndex = directives.findIndex((directive) => + matchScript.test(directive), + ); + if (scriptIndex >= 0) { + directives[scriptIndex] += formattedNonce; + } else { + const defaultIndex = directives.findIndex((directive) => + matchDefault.test(directive), ); - if (scriptSrc) { - scriptSrc.values += formattedNonce; - } else { - const defaultSrc = directives.find((directive) => - directive.name.toLowerCase().includes('default-src'), - ); - if (defaultSrc) { - defaultSrc.values += formattedNonce; - } + if (defaultIndex >= 0) { + directives[defaultIndex] += formattedNonce; } - return CSP.stringify(directives); - }, + } + return directives.join(';'); }; From f32f598ca0f019e9a9357444ffef7172dcea7bdd Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:16:37 +0400 Subject: [PATCH 18/41] fix: filename --- app/scripts/background.js | 2 +- .../modules/{content-security-policy.ts => add-nonce-to-csp.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename shared/modules/{content-security-policy.ts => add-nonce-to-csp.ts} (100%) diff --git a/app/scripts/background.js b/app/scripts/background.js index 915d8d82c2b9..ac879408beac 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,7 +56,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; -import { addNonceToCSP } from '../../shared/modules/content-security-policy'; +import { addNonceToCSP } from '../../shared/modules/add-nonce-to-csp'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; diff --git a/shared/modules/content-security-policy.ts b/shared/modules/add-nonce-to-csp.ts similarity index 100% rename from shared/modules/content-security-policy.ts rename to shared/modules/add-nonce-to-csp.ts From f72aa1d5f742e4dc476bd8d5e7cdc2a0c59412b8 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 22 Oct 2024 22:26:52 +0400 Subject: [PATCH 19/41] fix: casing --- app/scripts/background.js | 4 ++-- shared/modules/add-nonce-to-csp.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index ac879408beac..8ce6ac6d551b 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,7 +56,7 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; -import { addNonceToCSP } from '../../shared/modules/add-nonce-to-csp'; +import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -342,7 +342,7 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - header.value = addNonceToCSP( + header.value = addNonceToCsp( header.value, btoa(browser.runtime.getURL('/')), ); diff --git a/shared/modules/add-nonce-to-csp.ts b/shared/modules/add-nonce-to-csp.ts index e26f2201f197..a8b7fe333089 100644 --- a/shared/modules/add-nonce-to-csp.ts +++ b/shared/modules/add-nonce-to-csp.ts @@ -18,7 +18,7 @@ const matchDefault = matchDirective('default-src'); * @param nonce - The nonce to add to the Content Security Policy (CSP) string. * @returns The updated Content Security Policy (CSP) string. */ -export const addNonceToCSP = (text: string, nonce: string) => { +export const addNonceToCsp = (text: string, nonce: string) => { const formattedNonce = ` 'nonce-${nonce}'`; const directives = text.split(';'); const scriptIndex = directives.findIndex((directive) => From a939b234b292a1bdd0710ffbb32392d2397fb64f Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 22 Oct 2024 22:43:38 +0400 Subject: [PATCH 20/41] feat: tests --- shared/modules/add-nonce-to-csp.test.ts | 98 +++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 shared/modules/add-nonce-to-csp.test.ts diff --git a/shared/modules/add-nonce-to-csp.test.ts b/shared/modules/add-nonce-to-csp.test.ts new file mode 100644 index 000000000000..dc80bfd9a92a --- /dev/null +++ b/shared/modules/add-nonce-to-csp.test.ts @@ -0,0 +1,98 @@ +import { addNonceToCsp } from './add-nonce-to-csp'; + +describe('addNonceToCsp', () => { + it('empty string', () => { + const input = ''; + const expected = ''; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one empty directive', () => { + const input = 'script-src'; + const expected = `script-src 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, one value', () => { + const input = 'script-src default.example'; + const expected = `script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, two values', () => { + const input = "script-src 'self' default.example"; + const expected = `script-src 'self' default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('multiple directives', () => { + const input = + "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example"; + const expected = `default-src 'self'; script-src 'unsafe-eval' scripts.example 'nonce-test'; object-src; style-src styles.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('no applicable directive', () => { + const input = 'img-src https://example.com'; + const expected = `img-src https://example.com`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('non-ASCII directives', () => { + const input = 'script-src default.example;\u0080;style-src style.example'; + const expected = `script-src default.example 'nonce-test';\u0080;style-src style.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('uppercase directive names', () => { + const input = 'SCRIPT-SRC DEFAULT.EXAMPLE'; + const expected = `SCRIPT-SRC DEFAULT.EXAMPLE 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('duplicate directive names', () => { + const input = + 'default-src default.example; script-src script.example; script-src script.example'; + const expected = `default-src default.example; script-src script.example 'nonce-test'; script-src script.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('nonce value contains script-src', () => { + const input = + "default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com"; + const expected = `default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('url value contains script-src', () => { + const input = + "default-src 'self' https://script-src.com; script-src 'self' https://example.com"; + const expected = `default-src 'self' https://script-src.com; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('fallback to default-src', () => { + const input = `default-src 'none'`; + const expected = `default-src 'none' 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('keep ascii whitespace characters', () => { + const input = ' script-src default.example '; + const expected = ` script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); +}); From 8e654684814494d8ec331158812126cdeff9f6c7 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:18:38 +0400 Subject: [PATCH 21/41] feat: nonce explanation --- app/scripts/background.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index a280f830715c..23adb44c411a 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -340,14 +340,13 @@ function maybeDetectPhishing(theController) { * which involves overriding the page CSP for inline script nodes injected by extension content scripts. */ function overrideContentSecurityPolicyHeader() { + // The extension url is unique per install on Firefox, so we can safely add it as a nonce to the CSP header + const nonce = btoa(browser.runtime.getURL('/')); browser.webRequest.onHeadersReceived.addListener( ({ responseHeaders }) => { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { - header.value = addNonceToCsp( - header.value, - btoa(browser.runtime.getURL('/')), - ); + header.value = addNonceToCsp(header.value, nonce); } } return { responseHeaders }; From ce4709f4f89fc71cb79502259d5b3614fa0af203 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:33:35 +0400 Subject: [PATCH 22/41] feat: nonceExpression --- development/build/utils.js | 2 +- .../webpack/test/plugins.SelfInjectPlugin.test.ts | 4 ++-- development/webpack/utils/helpers.ts | 13 +++++++++++++ .../utils/plugins/SelfInjectPlugin/index.ts | 10 +++++++--- .../utils/plugins/SelfInjectPlugin/types.ts | 15 +++++++++++++-- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/development/build/utils.js b/development/build/utils.js index bfc1e6417ecb..626aacd588c7 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -293,7 +293,7 @@ function getBuildName({ function makeSelfInjecting(filePath) { const fileContents = readFileSync(filePath, 'utf8'); const textContent = JSON.stringify(fileContents); - const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa(browser.runtime.getURL('/'));d.documentElement.appendChild(s).remove();}`; + const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa((globalThis.browser||chrome).runtime.getURL('/'));d.documentElement.appendChild(s).remove();}`; writeFileSync(filePath, js, 'utf8'); } diff --git a/development/webpack/test/plugins.SelfInjectPlugin.test.ts b/development/webpack/test/plugins.SelfInjectPlugin.test.ts index c94f93df7234..b6390654a4bb 100644 --- a/development/webpack/test/plugins.SelfInjectPlugin.test.ts +++ b/development/webpack/test/plugins.SelfInjectPlugin.test.ts @@ -55,7 +55,7 @@ describe('SelfInjectPlugin', () => { // reference the `sourceMappingURL` assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa(browser.runtime.getURL('/'));d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } else { // the new source should NOT reference the new sourcemap, since it's @@ -66,7 +66,7 @@ describe('SelfInjectPlugin', () => { // console. assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa(browser.runtime.getURL('/'));d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } diff --git a/development/webpack/utils/helpers.ts b/development/webpack/utils/helpers.ts index 2e7dc25b6da3..4ddcaae687a8 100644 --- a/development/webpack/utils/helpers.ts +++ b/development/webpack/utils/helpers.ts @@ -232,3 +232,16 @@ export function logStats(err?: Error | null, stats?: Stats) { * @returns a new array with duplicate values removed and sorted */ export const uniqueSort = (array: string[]) => [...new Set(array)].sort(); + +/** + * Generates a runtime URL expression for a given path. + * + * This function constructs a URL string using the `runtime.getURL` method + * from either the `globalThis.browser` or `chrome` object, depending on + * which one is available in the global scope. + * + * @param path - The path of the runtime URL. + * @returns The constructed runtime URL string. + */ +export const getRuntimeURLExpression = (path: string) => + `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index 6dc0abd98829..96b123829ba9 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -1,6 +1,7 @@ import { dirname, relative } from 'node:path'; import { ModuleFilenameHelpers, Compilation, sources } from 'webpack'; import { validate } from 'schema-utils'; +import { getRuntimeURLExpression } from '../../helpers'; import { schema } from './schema'; import type { SelfInjectPluginOptions, Source, Compiler } from './types'; @@ -13,8 +14,11 @@ const defaultOptions = { // The default `sourceUrlExpression` is configured for browser extensions. // It generates the absolute url of the given file as an extension url. // e.g., `chrome-extension:///scripts/inpage.js` - sourceUrlExpression: (filename: string) => - `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(filename)})`, + sourceUrlExpression: getRuntimeURLExpression, + // The default `nonceExpression` is configured for browser extensions. + // It generates the absolute url of a path as an extension url in base64. + // e.g., `Y2hyb21lLWV4dGVuc2lvbjovLzxleHRlbnNpb24taWQ+Lw==` + nonceExpression: (path: string) => `btoa(${getRuntimeURLExpression(path)})`, } satisfies SelfInjectPluginOptions; /** @@ -142,7 +146,7 @@ export class SelfInjectPlugin { `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, ); newSource.add(`;`); - newSource.add(`s.nonce=btoa(browser.runtime.getURL('/'));`); + newSource.add(`s.nonce=${this.options.nonceExpression('/')};`); // add and immediately remove the script to avoid modifying the DOM. newSource.add(`d.documentElement.appendChild(s).remove()`); newSource.add(`}`); diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts index 2240227bd76a..266d8306f097 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts @@ -23,7 +23,7 @@ export type SelfInjectPluginOptions = { * will be injected into matched file to provide a sourceURL for the self * injected script. * - * Defaults to `(filename: string) => (globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * Defaults to `(filename: string) => (globalThis.browser||chrome).runtime.getURL("${filename}")` * * @example Custom * ```js @@ -39,11 +39,22 @@ export type SelfInjectPluginOptions = { * * ```js * { - * sourceUrlExpression: (filename) => `(globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * sourceUrlExpression: (filename) => `(globalThis.browser||chrome).runtime.getURL("${filename}")` * } * ``` * @param filename - the chunk's relative filename as it will exist in the output directory * @returns */ sourceUrlExpression?: (filename: string) => string; + /** + * A function that returns a JavaScript expression escaped as a string which + * will be injected into matched file to provide a nonceExpression for the self + * injected script. + * + * Defaults to `(path: string) => btoa((globalThis.browser||chrome).runtime.getURL("${path}"))` + * + * @param path - the path to be encoded as a nonce + * @returns + */ + nonceExpression?: (path: string) => string; }; From 5483bd8ae7de109111a9dabb2bedad267d722346 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Sat, 26 Oct 2024 00:40:51 +0400 Subject: [PATCH 23/41] feat: checkURLForProviderInjection --- app/scripts/background.js | 13 ++-- shared/modules/provider-injection.js | 94 +++++++++++++++++----------- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 23adb44c411a..cbe5fe32daca 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -57,6 +57,7 @@ import { // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp'; +import { checkURLForProviderInjection } from '../../shared/modules/provider-injection'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -343,10 +344,14 @@ function overrideContentSecurityPolicyHeader() { // The extension url is unique per install on Firefox, so we can safely add it as a nonce to the CSP header const nonce = btoa(browser.runtime.getURL('/')); browser.webRequest.onHeadersReceived.addListener( - ({ responseHeaders }) => { - for (const header of responseHeaders) { - if (header.name.toLowerCase() === 'content-security-policy') { - header.value = addNonceToCsp(header.value, nonce); + ({ responseHeaders, url }) => { + // Check whether inpage.js is going to be injected into the page or not. + // There is no reason to modify the headers if we are not injecting inpage.js. + if (checkURLForProviderInjection(new URL(url))) { + for (const header of responseHeaders) { + if (header.name.toLowerCase() === 'content-security-policy') { + header.value = addNonceToCsp(header.value, nonce); + } } } return { responseHeaders }; diff --git a/shared/modules/provider-injection.js b/shared/modules/provider-injection.js index 25a316e93440..b96df88c29e2 100644 --- a/shared/modules/provider-injection.js +++ b/shared/modules/provider-injection.js @@ -5,40 +5,36 @@ */ export default function shouldInjectProvider() { return ( - doctypeCheck() && - suffixCheck() && - documentElementCheck() && - !blockedDomainCheck() + checkURLForProviderInjection(new URL(window.location)) && + checkDocumentForProviderInjection() ); } /** - * Checks the doctype of the current document if it exists + * Checks if a given URL is eligible for provider injection. * - * @returns {boolean} {@code true} if the doctype is html or if none exists + * This function determines if a URL passes the suffix check and is not part of the blocked domains. + * + * @param {URL} url - The URL to be checked for injection. + * @returns {boolean} Returns `true` if the URL passes the suffix check and is not blocked, otherwise `false`. */ -function doctypeCheck() { - const { doctype } = window.document; - if (doctype) { - return doctype.name === 'html'; - } - return true; +export function checkURLForProviderInjection(url) { + return suffixCheck(url) && !blockedDomainCheck(url); } /** - * Returns whether or not the extension (suffix) of the current document is prohibited + * Returns whether or not the extension (suffix) of the given URL's pathname is prohibited * - * This checks {@code window.location.pathname} against a set of file extensions - * that we should not inject the provider into. This check is indifferent of - * query parameters in the location. + * This checks the provided URL's pathname against a set of file extensions + * that we should not inject the provider into. * - * @returns {boolean} whether or not the extension of the current document is prohibited + * @param {URL} url - The URL to check + * @returns {boolean} whether or not the extension of the given URL's pathname is prohibited */ -function suffixCheck() { +function suffixCheck({ pathname }) { const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]; - const currentUrl = window.location.pathname; for (let i = 0; i < prohibitedTypes.length; i++) { - if (prohibitedTypes[i].test(currentUrl)) { + if (prohibitedTypes[i].test(pathname)) { return false; } } @@ -46,24 +42,12 @@ function suffixCheck() { } /** - * Checks the documentElement of the current document + * Checks if the given domain is blocked * - * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + * @param {URL} url - The URL to check + * @returns {boolean} {@code true} if the given domain is blocked */ -function documentElementCheck() { - const documentElement = document.documentElement.nodeName; - if (documentElement) { - return documentElement.toLowerCase() === 'html'; - } - return true; -} - -/** - * Checks if the current domain is blocked - * - * @returns {boolean} {@code true} if the current domain is blocked - */ -function blockedDomainCheck() { +function blockedDomainCheck(url) { // If making any changes, please also update the same list found in the MetaMask-Mobile & SDK repositories const blockedDomains = [ 'execution.consensys.io', @@ -85,8 +69,7 @@ function blockedDomainCheck() { 'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html', ]; - const { hostname: currentHostname, pathname: currentPathname } = - window.location; + const { hostname: currentHostname, pathname: currentPathname } = url; const trimTrailingSlash = (str) => str.endsWith('/') ? str.slice(0, -1) : str; @@ -104,3 +87,38 @@ function blockedDomainCheck() { ) ); } + +/** + * Checks if the document is suitable for provider injection by verifying the doctype and document element. + * + * @returns {boolean} `true` if the document passes both the doctype and document element checks, otherwise `false`. + */ +export function checkDocumentForProviderInjection() { + return doctypeCheck() && documentElementCheck(); +} + +/** + * Checks the doctype of the current document if it exists + * + * @returns {boolean} {@code true} if the doctype is html or if none exists + */ +function doctypeCheck() { + const { doctype } = window.document; + if (doctype) { + return doctype.name === 'html'; + } + return true; +} + +/** + * Checks the documentElement of the current document + * + * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + */ +function documentElementCheck() { + const documentElement = document.documentElement.nodeName; + if (documentElement) { + return documentElement.toLowerCase() === 'html'; + } + return true; +} From 83f39086530c5c4099f0f77fc906dcf4f1e426cb Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:47:36 +0400 Subject: [PATCH 24/41] feat: e2e test --- development/create-static-server.js | 24 +++++++++-- development/static-server.js | 2 +- test/e2e/helpers.js | 5 ++- test/e2e/phishing-warning-page-server.js | 2 +- .../index.html | 9 +++++ .../content-security-policy.spec.ts | 40 +++++++++++++++++++ .../synchronous-injection.spec.js | 2 +- 7 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html create mode 100644 test/e2e/tests/content-security-policy/content-security-policy.spec.ts diff --git a/development/create-static-server.js b/development/create-static-server.js index a8d5e28b0088..36a9842d5d4c 100755 --- a/development/create-static-server.js +++ b/development/create-static-server.js @@ -4,10 +4,28 @@ const path = require('path'); const serveHandler = require('serve-handler'); -const createStaticServer = (rootDirectory) => { +/** + * Creates an HTTP server that serves static files from a directory using serve-handler. + * If a request URL starts with `/node_modules/`, it rewrites the URL and serves files from the `node_modules` directory. + * + * @param {object} options - Configuration options for serve-handler. Documentation can be found here: https://github.com/vercel/serve-handler + * @param {string} [options.public] - A custom directory to be served relative to the current working directory. + * @param {boolean|string[]} [options.cleanUrls] - Disable `.html` extension stripping, or restrict it to specific paths. + * @param {{source: string, destination: string}[]} [options.rewrites] - Array of rewrite rules to map certain paths to different ones. + * @param {{source: string, destination: string}[]} [options.redirects] - Array of redirect rules to forward paths to different paths or external URLs. + * @param {{source: string, headers: {key: string, value: string}[]}[]} [options.headers] - Array of custom headers to set for specific paths. + * @param {boolean|string[]} [options.directoryListing] - Disable directory listing, or restrict it to specific paths. + * @param {string[]} [options.unlisted] - List of paths to exclude from the directory listing. + * @param {boolean} [options.trailingSlash] - Whether to enforce trailing slashes on paths. + * @param {boolean} [options.renderSingle] - If a directory contains only one file, render it. + * @param {boolean} [options.symlinks] - Whether to resolve symlinks instead of serving a 404 error. + * @param {boolean} [options.etag] - Whether to calculate and use an ETag response header instead of Last-Modified. + * @returns {http.Server} An instance of an HTTP server configured with the specified options. + */ +const createStaticServer = (options) => { return http.createServer((request, response) => { if (request.url.startsWith('/node_modules/')) { - request.url = request.url.substr(14); + request.url = request.url.slice(14); return serveHandler(request, response, { directoryListing: false, public: path.resolve('./node_modules'), @@ -15,7 +33,7 @@ const createStaticServer = (rootDirectory) => { } return serveHandler(request, response, { directoryListing: false, - public: rootDirectory, + ...options, }); }); }; diff --git a/development/static-server.js b/development/static-server.js index bb15133d6fdd..ec3a51a512f0 100755 --- a/development/static-server.js +++ b/development/static-server.js @@ -31,7 +31,7 @@ const onRequest = (request, response) => { }; const startServer = ({ port, rootDirectory }) => { - const server = createStaticServer(rootDirectory); + const server = createStaticServer({ public: rootDirectory }); server.on('request', onRequest); diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 53f1c9deaeda..8adf81498cf1 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -65,6 +65,7 @@ async function withFixtures(options, testSuite) { smartContract, driverOptions, dappOptions, + staticServerOptions, title, ignoredConsoleErrors = [], dappPath = undefined, @@ -159,7 +160,9 @@ async function withFixtures(options, testSuite) { 'dist', ); } - dappServer.push(createStaticServer(dappDirectory)); + dappServer.push( + createStaticServer({ public: dappDirectory, ...staticServerOptions }), + ); dappServer[i].listen(`${dappBasePort + i}`); await new Promise((resolve, reject) => { dappServer[i].on('listening', resolve); diff --git a/test/e2e/phishing-warning-page-server.js b/test/e2e/phishing-warning-page-server.js index 2f6099e1a3d0..a66c9211ed83 100644 --- a/test/e2e/phishing-warning-page-server.js +++ b/test/e2e/phishing-warning-page-server.js @@ -13,7 +13,7 @@ const phishingWarningDirectory = path.resolve( class PhishingWarningPageServer { constructor() { - this._server = createStaticServer(phishingWarningDirectory); + this._server = createStaticServer({ public: phishingWarningDirectory }); } async start({ port = 9999 } = {}) { diff --git a/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html new file mode 100644 index 000000000000..dae42d094890 --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html @@ -0,0 +1,9 @@ + + + + Mock CSP header testing + + +
Mock Page for Content-Security-Policy header testing
+ + diff --git a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts new file mode 100644 index 000000000000..110de39363d6 --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts @@ -0,0 +1,40 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { defaultGanacheOptions, openDapp, withFixtures } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; + +describe('Content-Security-Policy', function (this: Suite) { + it('opening a restricted website should still load the extension', async function () { + await withFixtures( + { + dapp: true, + dappPaths: [ + './tests/content-security-policy/content-security-policy-mock-page', + ], + staticServerOptions: { + headers: [ + { + source: 'index.html', + headers: [ + { + key: 'Content-Security-Policy', + value: `default-src: 'none'`, + }, + ], + }, + ], + }, + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await openDapp(driver); + const isExtensionLoaded: boolean = await driver.executeScript( + 'return typeof window.ethereum !== "undefined"', + ); + assert.equal(isExtensionLoaded, true); + }, + ); + }); +}); diff --git a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js index 917e289f320f..8e4254fb8b34 100644 --- a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js +++ b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js @@ -7,7 +7,7 @@ const dappPort = 8080; describe('The provider', function () { it('can be injected synchronously and successfully used by a dapp', async function () { - const dappServer = createStaticServer(__dirname); + const dappServer = createStaticServer({ public: __dirname }); dappServer.listen(dappPort); await new Promise((resolve, reject) => { dappServer.on('listening', resolve); From a0147420bae414296dc64f1c453ae57296d82aed Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:11:44 +0400 Subject: [PATCH 25/41] fix: getRuntimeURLExpression placement --- development/webpack/utils/helpers.ts | 13 ------------- .../utils/plugins/SelfInjectPlugin/helpers.ts | 12 ++++++++++++ .../webpack/utils/plugins/SelfInjectPlugin/index.ts | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts diff --git a/development/webpack/utils/helpers.ts b/development/webpack/utils/helpers.ts index 4ddcaae687a8..2e7dc25b6da3 100644 --- a/development/webpack/utils/helpers.ts +++ b/development/webpack/utils/helpers.ts @@ -232,16 +232,3 @@ export function logStats(err?: Error | null, stats?: Stats) { * @returns a new array with duplicate values removed and sorted */ export const uniqueSort = (array: string[]) => [...new Set(array)].sort(); - -/** - * Generates a runtime URL expression for a given path. - * - * This function constructs a URL string using the `runtime.getURL` method - * from either the `globalThis.browser` or `chrome` object, depending on - * which one is available in the global scope. - * - * @param path - The path of the runtime URL. - * @returns The constructed runtime URL string. - */ -export const getRuntimeURLExpression = (path: string) => - `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts b/development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts new file mode 100644 index 000000000000..bdf43fb8580c --- /dev/null +++ b/development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts @@ -0,0 +1,12 @@ +/** + * Generates a runtime URL expression for a given path. + * + * This function constructs a URL string using the `runtime.getURL` method + * from either the `globalThis.browser` or `chrome` object, depending on + * which one is available in the global scope. + * + * @param path - The path of the runtime URL. + * @returns The constructed runtime URL string. + */ +export const getRuntimeURLExpression = (path: string) => + `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index 96b123829ba9..915eefd7bb17 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -1,7 +1,7 @@ import { dirname, relative } from 'node:path'; import { ModuleFilenameHelpers, Compilation, sources } from 'webpack'; import { validate } from 'schema-utils'; -import { getRuntimeURLExpression } from '../../helpers'; +import { getRuntimeURLExpression } from './helpers'; import { schema } from './schema'; import type { SelfInjectPluginOptions, Source, Compiler } from './types'; From 851cc5e56aa6b3e338172f6f7bd72b74aaafe353 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:34:48 +0400 Subject: [PATCH 26/41] fix: location mock --- shared/modules/provider-injection.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shared/modules/provider-injection.test.ts b/shared/modules/provider-injection.test.ts index 1742e31b4db6..c7da24b46642 100644 --- a/shared/modules/provider-injection.test.ts +++ b/shared/modules/provider-injection.test.ts @@ -8,11 +8,7 @@ describe('shouldInjectProvider', () => { const urlObj = new URL(urlString); mockedWindow.mockImplementation(() => ({ - location: { - hostname: urlObj.hostname, - origin: urlObj.origin, - pathname: urlObj.pathname, - }, + location: urlObj, document: { doctype: { name: 'html', From 73a22d3b48d26d3c3978570c1cc2fd10733765e0 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:37:46 +0400 Subject: [PATCH 27/41] fix: placement --- .../utils/plugins/SelfInjectPlugin/helpers.ts | 12 ------------ .../utils/plugins/SelfInjectPlugin/index.ts | 14 +++++++++++++- 2 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts b/development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts deleted file mode 100644 index bdf43fb8580c..000000000000 --- a/development/webpack/utils/plugins/SelfInjectPlugin/helpers.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generates a runtime URL expression for a given path. - * - * This function constructs a URL string using the `runtime.getURL` method - * from either the `globalThis.browser` or `chrome` object, depending on - * which one is available in the global scope. - * - * @param path - The path of the runtime URL. - * @returns The constructed runtime URL string. - */ -export const getRuntimeURLExpression = (path: string) => - `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index 915eefd7bb17..18d3624310ae 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -1,12 +1,24 @@ import { dirname, relative } from 'node:path'; import { ModuleFilenameHelpers, Compilation, sources } from 'webpack'; import { validate } from 'schema-utils'; -import { getRuntimeURLExpression } from './helpers'; import { schema } from './schema'; import type { SelfInjectPluginOptions, Source, Compiler } from './types'; export { type SelfInjectPluginOptions } from './types'; +/** + * Generates a runtime URL expression for a given path. + * + * This function constructs a URL string using the `runtime.getURL` method + * from either the `globalThis.browser` or `chrome` object, depending on + * which one is available in the global scope. + * + * @param path - The path of the runtime URL. + * @returns The constructed runtime URL string. + */ +const getRuntimeURLExpression = (path: string) => + `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; + /** * Default options for the SelfInjectPlugin. */ From 031f530a60a35f31188a70dbb8ee11e3ec2d675a Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 31 Oct 2024 19:50:52 +0400 Subject: [PATCH 28/41] Update development/webpack/utils/plugins/SelfInjectPlugin/types.ts Co-authored-by: David Murdoch <187813+davidmurdoch@users.noreply.github.com> --- development/webpack/utils/plugins/SelfInjectPlugin/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts index 266d8306f097..e70467cd270b 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts @@ -48,7 +48,7 @@ export type SelfInjectPluginOptions = { sourceUrlExpression?: (filename: string) => string; /** * A function that returns a JavaScript expression escaped as a string which - * will be injected into matched file to provide a nonceExpression for the self + * will be injected into matched file to set a nonce for the self * injected script. * * Defaults to `(path: string) => btoa((globalThis.browser||chrome).runtime.getURL("${path}"))` From dab24be12c242148d2bc0ba40cd5dbdbfd7ee564 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 31 Oct 2024 19:54:44 +0400 Subject: [PATCH 29/41] feat: @types/serve-handler --- development/create-static-server.js | 13 +------------ package.json | 1 + yarn.lock | 10 ++++++++++ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/development/create-static-server.js b/development/create-static-server.js index 36a9842d5d4c..8e55fa54ca13 100755 --- a/development/create-static-server.js +++ b/development/create-static-server.js @@ -8,18 +8,7 @@ const serveHandler = require('serve-handler'); * Creates an HTTP server that serves static files from a directory using serve-handler. * If a request URL starts with `/node_modules/`, it rewrites the URL and serves files from the `node_modules` directory. * - * @param {object} options - Configuration options for serve-handler. Documentation can be found here: https://github.com/vercel/serve-handler - * @param {string} [options.public] - A custom directory to be served relative to the current working directory. - * @param {boolean|string[]} [options.cleanUrls] - Disable `.html` extension stripping, or restrict it to specific paths. - * @param {{source: string, destination: string}[]} [options.rewrites] - Array of rewrite rules to map certain paths to different ones. - * @param {{source: string, destination: string}[]} [options.redirects] - Array of redirect rules to forward paths to different paths or external URLs. - * @param {{source: string, headers: {key: string, value: string}[]}[]} [options.headers] - Array of custom headers to set for specific paths. - * @param {boolean|string[]} [options.directoryListing] - Disable directory listing, or restrict it to specific paths. - * @param {string[]} [options.unlisted] - List of paths to exclude from the directory listing. - * @param {boolean} [options.trailingSlash] - Whether to enforce trailing slashes on paths. - * @param {boolean} [options.renderSingle] - If a directory contains only one file, render it. - * @param {boolean} [options.symlinks] - Whether to resolve symlinks instead of serving a 404 error. - * @param {boolean} [options.etag] - Whether to calculate and use an ETag response header instead of Last-Modified. + * @param { NonNullable[2]> } options - Configuration options for serve-handler. Documentation can be found here: https://github.com/vercel/serve-handler * @returns {http.Server} An instance of an HTTP server configured with the specified options. */ const createStaticServer = (options) => { diff --git a/package.json b/package.json index 9191330a0c39..b94d46e5f0f2 100644 --- a/package.json +++ b/package.json @@ -526,6 +526,7 @@ "@types/redux-mock-store": "1.0.6", "@types/remote-redux-devtools": "^0.5.5", "@types/selenium-webdriver": "^4.1.19", + "@types/serve-handler": "^6.1.4", "@types/sinon": "^10.0.13", "@types/sprintf-js": "^1", "@types/w3c-web-hid": "^1.0.3", diff --git a/yarn.lock b/yarn.lock index 3b1dc9246bbb..8c561acdcedf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10878,6 +10878,15 @@ __metadata: languageName: node linkType: hard +"@types/serve-handler@npm:^6.1.4": + version: 6.1.4 + resolution: "@types/serve-handler@npm:6.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/c92ae204605659b37202af97cfcc7690be43b9290692c1d6c3c93805b399044fd67573af4eb2e7b1fd975451db6d0d5c6cd2f09b20997209fa3341f345f661e4 + languageName: node + linkType: hard + "@types/serve-index@npm:^1.9.4": version: 1.9.4 resolution: "@types/serve-index@npm:1.9.4" @@ -26079,6 +26088,7 @@ __metadata: "@types/redux-mock-store": "npm:1.0.6" "@types/remote-redux-devtools": "npm:^0.5.5" "@types/selenium-webdriver": "npm:^4.1.19" + "@types/serve-handler": "npm:^6.1.4" "@types/sinon": "npm:^10.0.13" "@types/sprintf-js": "npm:^1" "@types/w3c-web-hid": "npm:^1.0.3" From 41498b0203da3de02017081a53a5c21916f0e72f Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:22:31 +0400 Subject: [PATCH 30/41] fix: csp header in e2e --- .../content-security-policy/content-security-policy.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts index 110de39363d6..f67f03479130 100644 --- a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts +++ b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts @@ -18,7 +18,7 @@ describe('Content-Security-Policy', function (this: Suite) { headers: [ { key: 'Content-Security-Policy', - value: `default-src: 'none'`, + value: `default-src 'none'`, }, ], }, From b6e2e2541380d1ffd6f1103ebf58413017c37b5c Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:14:15 +0400 Subject: [PATCH 31/41] Update content-security-policy.spec.ts --- .../content-security-policy.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts index f67f03479130..996c64343c5b 100644 --- a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts +++ b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts @@ -1,6 +1,11 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; -import { defaultGanacheOptions, openDapp, withFixtures } from '../../helpers'; +import { + defaultGanacheOptions, + openDapp, + unlockWallet, + withFixtures, +} from '../../helpers'; import FixtureBuilder from '../../fixture-builder'; describe('Content-Security-Policy', function (this: Suite) { @@ -29,6 +34,7 @@ describe('Content-Security-Policy', function (this: Suite) { title: this.test?.fullTitle(), }, async ({ driver }) => { + await unlockWallet(driver); await openDapp(driver); const isExtensionLoaded: boolean = await driver.executeScript( 'return typeof window.ethereum !== "undefined"', From 75e16be64f6e8bc5ee54217b789f43e994c7314a Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:11:36 +0400 Subject: [PATCH 32/41] feat: ui toggle --- app/_locales/en/messages.json | 6 +++ app/scripts/background.js | 6 ++- app/scripts/constants/sentry-state.ts | 1 + .../preferences-controller.test.ts | 17 ++++++++ .../controllers/preferences-controller.ts | 20 ++++++++++ app/scripts/lib/backup.test.js | 1 + app/scripts/metamask-controller.js | 4 ++ ui/helpers/constants/settings.js | 9 +++++ ui/helpers/utils/settings-search.test.js | 4 ++ .../advanced-tab/advanced-tab.component.js | 39 +++++++++++++++++++ .../advanced-tab/advanced-tab.container.js | 6 +++ .../advanced-tab/advanced-tab.stories.js | 18 +++++++++ ui/store/actions.ts | 12 ++++++ 13 files changed, 142 insertions(+), 1 deletion(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 512eb3b0ee36..216781b454ce 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3764,6 +3764,12 @@ "outdatedBrowserNotification": { "message": "Your browser is out of date. If you don't update your browser, you won't be able to get security patches and new features from MetaMask." }, + "overrideContentSecurityPolicyHeader": { + "message": "Override Content-Security-Policy header" + }, + "overrideContentSecurityPolicyHeaderDescription": { + "message": "This option is a workaround for a known issue in Firefox, where the Content-Security-Policy header may not be bypassed, causing the extension to fail to load properly. Disabling this option is not recommended unless required for specific web page compatibility." + }, "padlock": { "message": "Padlock" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index cbe5fe32daca..97c30521d6ec 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -510,7 +510,11 @@ async function initialize() { // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 const platformName = getPlatform(); - if (platformName === PLATFORM_FIREFOX) { + if ( + platformName === PLATFORM_FIREFOX && + controller.preferencesController.state + .overrideContentSecurityPolicyHeader + ) { overrideContentSecurityPolicyHeader(); } } diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 3125016ea0b5..655851590441 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -223,6 +223,7 @@ export const SENTRY_BACKGROUND_STATE = { advancedGasFee: true, currentLocale: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: true, forgottenPassword: true, identities: false, diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 74daf39e17ad..09ddd31d4c82 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -837,6 +837,23 @@ describe('preferences controller', () => { }); }); + describe('overrideContentSecurityPolicyHeader', () => { + it('defaults overrideContentSecurityPolicyHeader to true', () => { + const { controller } = setupController({}); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(true); + }); + + it('set overrideContentSecurityPolicyHeader to false', () => { + const { controller } = setupController({}); + controller.setOverrideContentSecurityPolicyHeader(false); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(false); + }); + }); + describe('snapsAddSnapAccountModalDismissed', () => { it('defaults snapsAddSnapAccountModalDismissed to false', () => { const { controller } = setupController({}); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index f6537952d651..ef6d8891d527 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -133,6 +133,7 @@ export type PreferencesControllerState = Omit< useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; + overrideContentSecurityPolicyHeader: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; use4ByteResolution: boolean; @@ -172,6 +173,7 @@ export const getDefaultPreferencesControllerState = useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useMultiAccountBalanceChecker: true, useSafeChainsListValidation: true, // set to true means the dynamic list from the API is being used @@ -300,6 +302,10 @@ const controllerMetadata = { persist: true, anonymous: true, }, + overrideContentSecurityPolicyHeader: { + persist: true, + anonymous: true, + }, useMultiAccountBalanceChecker: { persist: true, anonymous: true, @@ -985,6 +991,20 @@ export class PreferencesController extends BaseController< }); } + /** + * A setter for the user preference to override the Content-Security-Policy header + * + * @param overrideContentSecurityPolicyHeader - User preference for overriding the Content-Security-Policy header. + */ + setOverrideContentSecurityPolicyHeader( + overrideContentSecurityPolicyHeader: boolean, + ): void { + this.update((state) => { + state.overrideContentSecurityPolicyHeader = + overrideContentSecurityPolicyHeader; + }); + } + /** * A setter for the incomingTransactions in preference to be updated * diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 7a322148c847..cea7e9a7ba6f 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -150,6 +150,7 @@ const jsonData = JSON.stringify({ useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useTokenDetection: false, useCollectibleDetection: false, openSeaEnabled: false, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 55f5e881de4a..1e9b5a04f990 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3485,6 +3485,10 @@ export default class MetamaskController extends EventEmitter { preferencesController.setDismissSeedBackUpReminder.bind( preferencesController, ), + setOverrideContentSecurityPolicyHeader: + preferencesController.setOverrideContentSecurityPolicyHeader.bind( + preferencesController, + ), setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind( preferencesController, ), diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index c22b0cbcf183..336e694cc55c 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -154,6 +154,15 @@ const SETTINGS_CONSTANTS = [ route: `${ADVANCED_ROUTE}#export-data`, icon: 'fas fa-download', }, + // advanced settingsRefs[11] + { + tabMessage: (t) => t('advanced'), + sectionMessage: (t) => t('overrideContentSecurityPolicyHeader'), + descriptionMessage: (t) => + t('overrideContentSecurityPolicyHeaderDescription'), + route: `${ADVANCED_ROUTE}#override-content-security-policy-header`, + icon: 'fas fa-sliders-h', + }, { tabMessage: (t) => t('contacts'), sectionMessage: (t) => t('contacts'), diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index cc7b875d8c5e..f5ef109a5684 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -68,6 +68,10 @@ const t = (key) => { return 'Dismiss Secret Recovery Phrase backup reminder'; case 'dismissReminderDescriptionField': return 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds'; + case 'overrideContentSecurityPolicyHeader': + return 'Override Content-Security-Policy header'; + case 'overrideContentSecurityPolicyHeaderDescription': + return 'This option is a workaround for a known issue in Firefox, where the Content-Security-Policy header may not be bypassed, causing the extension to fail to load properly. Disabling this option is not recommended unless required for specific web page compatibility.'; case 'Contacts': return 'Contacts'; case 'securityAndPrivacy': diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index 50aea4e0dc60..86ffe6d1c1a9 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js @@ -57,6 +57,8 @@ export default class AdvancedTab extends PureComponent { backupUserData: PropTypes.func.isRequired, showExtensionInFullSizeView: PropTypes.bool, setShowExtensionInFullSizeView: PropTypes.func.isRequired, + overrideContentSecurityPolicyHeader: PropTypes.bool, + setOverrideContentSecurityPolicyHeader: PropTypes.func.isRequired, }; state = { @@ -575,6 +577,42 @@ export default class AdvancedTab extends PureComponent { ); } + renderOverrideContentSecurityPolicyHeader() { + const { t } = this.context; + const { + overrideContentSecurityPolicyHeader, + setOverrideContentSecurityPolicyHeader, + } = this.props; + + return ( + +
+ {t('overrideContentSecurityPolicyHeader')} +
+ {t('overrideContentSecurityPolicyHeaderDescription')} +
+
+ +
+ setOverrideContentSecurityPolicyHeader(!value)} + offLabel={t('off')} + onLabel={t('on')} + /> +
+
+ ); + } + render() { const { warning } = this.props; // When adding/removing/editing the order of renders, double-check the order of the settingsRefs. This affects settings-search.js @@ -592,6 +630,7 @@ export default class AdvancedTab extends PureComponent { {this.renderAutoLockTimeLimit()} {this.renderUserDataBackup()} {this.renderDismissSeedBackupReminderControl()} + {this.renderOverrideContentSecurityPolicyHeader()} ); } diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js index f2ad894d1e8b..68f6cb05d1f0 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js @@ -8,6 +8,7 @@ import { displayWarning, setAutoLockTimeLimit, setDismissSeedBackUpReminder, + setOverrideContentSecurityPolicyHeader, setFeatureFlag, setShowExtensionInFullSizeView, setShowFiatConversionOnTestnetsPreference, @@ -28,6 +29,7 @@ export const mapStateToProps = (state) => { featureFlags: { sendHexData } = {}, useNonceField, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, } = metamask; const { showFiatInTestnets, @@ -46,6 +48,7 @@ export const mapStateToProps = (state) => { autoLockTimeLimit, useNonceField, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, }; }; @@ -76,6 +79,9 @@ export const mapDispatchToProps = (dispatch) => { setDismissSeedBackUpReminder: (value) => { return dispatch(setDismissSeedBackUpReminder(value)); }, + setOverrideContentSecurityPolicyHeader: (value) => { + return dispatch(setOverrideContentSecurityPolicyHeader(value)); + }, }; }; diff --git a/ui/pages/settings/advanced-tab/advanced-tab.stories.js b/ui/pages/settings/advanced-tab/advanced-tab.stories.js index 46e7ee978f43..b54161606d30 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.stories.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.stories.js @@ -12,6 +12,7 @@ export default { showFiatInTestnets: { control: 'boolean' }, useLedgerLive: { control: 'boolean' }, dismissSeedBackUpReminder: { control: 'boolean' }, + overrideContentSecurityPolicyHeader: { control: 'boolean' }, setAutoLockTimeLimit: { action: 'setAutoLockTimeLimit' }, setShowFiatConversionOnTestnetsPreference: { action: 'setShowFiatConversionOnTestnetsPreference', @@ -20,6 +21,9 @@ export default { setIpfsGateway: { action: 'setIpfsGateway' }, setIsIpfsGatewayEnabled: { action: 'setIsIpfsGatewayEnabled' }, setDismissSeedBackUpReminder: { action: 'setDismissSeedBackUpReminder' }, + setOverrideContentSecurityPolicyHeader: { + action: 'setOverrideContentSecurityPolicyHeader', + }, setUseNonceField: { action: 'setUseNonceField' }, setHexDataFeatureFlag: { action: 'setHexDataFeatureFlag' }, displayWarning: { action: 'displayWarning' }, @@ -37,6 +41,7 @@ export const DefaultStory = (args) => { sendHexData, showFiatInTestnets, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, }, updateArgs, ] = useArgs(); @@ -64,6 +69,12 @@ export const DefaultStory = (args) => { dismissSeedBackUpReminder: !dismissSeedBackUpReminder, }); }; + + const handleOverrideContentSecurityPolicyHeader = () => { + updateArgs({ + overrideContentSecurityPolicyHeader: !overrideContentSecurityPolicyHeader, + }); + }; return (
{ setShowFiatConversionOnTestnetsPreference={handleShowFiatInTestnets} dismissSeedBackUpReminder={dismissSeedBackUpReminder} setDismissSeedBackUpReminder={handleDismissSeedBackUpReminder} + overrideContentSecurityPolicyHeader={ + overrideContentSecurityPolicyHeader + } + setOverrideContentSecurityPolicyHeader={ + handleOverrideContentSecurityPolicyHeader + } ipfsGateway="ipfs-gateway" />
@@ -90,4 +107,5 @@ DefaultStory.args = { showFiatInTestnets: false, useLedgerLive: false, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, }; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 77189e9683af..7d0043f1a557 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4218,6 +4218,18 @@ export function setDismissSeedBackUpReminder( }; } +export function setOverrideContentSecurityPolicyHeader( + value: boolean, +): ThunkAction { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(showLoadingIndication()); + await submitRequestToBackground('setOverrideContentSecurityPolicyHeader', [ + value, + ]); + dispatch(hideLoadingIndication()); + }; +} + export function getRpcMethodPreferences(): ThunkAction< void, MetaMaskReduxState, From b482cac778350b293c01430a017062c5ac4909f1 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:19:34 +0400 Subject: [PATCH 33/41] Update background.js --- app/scripts/background.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 97c30521d6ec..a2acc4c27323 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -347,13 +347,21 @@ function overrideContentSecurityPolicyHeader() { ({ responseHeaders, url }) => { // Check whether inpage.js is going to be injected into the page or not. // There is no reason to modify the headers if we are not injecting inpage.js. - if (checkURLForProviderInjection(new URL(url))) { + const isInjected = checkURLForProviderInjection(new URL(url)); + + // Check if the user has enabled the overrideContentSecurityPolicyHeader preference + const isEnabled = + controller.preferencesController.state + .overrideContentSecurityPolicyHeader; + + if (isInjected && isEnabled) { for (const header of responseHeaders) { if (header.name.toLowerCase() === 'content-security-policy') { header.value = addNonceToCsp(header.value, nonce); } } } + return { responseHeaders }; }, { types: ['main_frame', 'sub_frame'], urls: ['http://*/*', 'https://*/*'] }, @@ -510,11 +518,7 @@ async function initialize() { // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 const platformName = getPlatform(); - if ( - platformName === PLATFORM_FIREFOX && - controller.preferencesController.state - .overrideContentSecurityPolicyHeader - ) { + if (platformName === PLATFORM_FIREFOX) { overrideContentSecurityPolicyHeader(); } } From 289dc505e7565f6d6e8531f36addde66c00ffa2c Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:29:11 +0400 Subject: [PATCH 34/41] Update settings-search.test.js --- ui/helpers/utils/settings-search.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index f5ef109a5684..648567becdbd 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -164,7 +164,7 @@ describe('Settings Search Utils', () => { }); it('returns "Advanced" section count', () => { - expect(getNumberOfSettingRoutesInTab(t, t('advanced'))).toStrictEqual(11); + expect(getNumberOfSettingRoutesInTab(t, t('advanced'))).toStrictEqual(12); }); it('returns "Contact" section count', () => { From 5163ec9faa1b561c123e0b1010e445c32720f45d Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:20:27 +0400 Subject: [PATCH 35/41] fix: description --- app/_locales/en/messages.json | 2 +- ui/helpers/utils/settings-search.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 216781b454ce..5ca83ac0630e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3768,7 +3768,7 @@ "message": "Override Content-Security-Policy header" }, "overrideContentSecurityPolicyHeaderDescription": { - "message": "This option is a workaround for a known issue in Firefox, where the Content-Security-Policy header may not be bypassed, causing the extension to fail to load properly. Disabling this option is not recommended unless required for specific web page compatibility." + "message": "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility." }, "padlock": { "message": "Padlock" diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index 648567becdbd..323aacd0b8bc 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -71,7 +71,7 @@ const t = (key) => { case 'overrideContentSecurityPolicyHeader': return 'Override Content-Security-Policy header'; case 'overrideContentSecurityPolicyHeaderDescription': - return 'This option is a workaround for a known issue in Firefox, where the Content-Security-Policy header may not be bypassed, causing the extension to fail to load properly. Disabling this option is not recommended unless required for specific web page compatibility.'; + return "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility."; case 'Contacts': return 'Contacts'; case 'securityAndPrivacy': From ab1f3df40b63b50c607a91ae1cb5680853c65bcf Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:05:30 +0400 Subject: [PATCH 36/41] feat: only show on firefox --- app/scripts/background.js | 3 +-- ui/helpers/constants/settings.js | 6 ++++++ ui/helpers/utils/settings-search.js | 6 ++++-- ui/pages/settings/advanced-tab/advanced-tab.component.js | 8 +++++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index a2acc4c27323..90a52b6c0d19 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -517,8 +517,7 @@ async function initialize() { await loadPhishingWarningPage(); // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 - const platformName = getPlatform(); - if (platformName === PLATFORM_FIREFOX) { + if (getPlatform() === PLATFORM_FIREFOX) { overrideContentSecurityPolicyHeader(); } } diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index 336e694cc55c..232bcdae5aff 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -1,4 +1,8 @@ /* eslint-disable @metamask/design-tokens/color-no-hex*/ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getPlatform } from '../../../app/scripts/lib/util'; +import { PLATFORM_FIREFOX } from '../../../shared/constants/app'; import { IconName } from '../../components/component-library'; import { ADVANCED_ROUTE, @@ -19,6 +23,7 @@ import { * # @param {string} route tab route with appended arbitrary, unique anchor tag / hash route * # @param {string} iconName * # @param {string} featureFlag ENV variable name. If the ENV value exists, the route will be searchable; else, route will not be searchable. + * # @param {boolean} hidden If true, the route will not be searchable. */ /** @type {SettingRouteConfig[]} */ @@ -162,6 +167,7 @@ const SETTINGS_CONSTANTS = [ t('overrideContentSecurityPolicyHeaderDescription'), route: `${ADVANCED_ROUTE}#override-content-security-policy-header`, icon: 'fas fa-sliders-h', + hidden: getPlatform() !== PLATFORM_FIREFOX, }, { tabMessage: (t) => t('contacts'), diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js index 07b4501c0208..8c11ad8fad52 100644 --- a/ui/helpers/utils/settings-search.js +++ b/ui/helpers/utils/settings-search.js @@ -8,8 +8,10 @@ export function getSettingsRoutes() { if (settingsRoutes) { return settingsRoutes; } - settingsRoutes = SETTINGS_CONSTANTS.filter((routeObject) => - routeObject.featureFlag ? process.env[routeObject.featureFlag] : true, + settingsRoutes = SETTINGS_CONSTANTS.filter( + (routeObject) => + (routeObject.featureFlag ? process.env[routeObject.featureFlag] : true) && + !routeObject.hidden, ); return settingsRoutes; } diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index 86ffe6d1c1a9..362d5088273b 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js @@ -29,6 +29,10 @@ import { getNumberOfSettingRoutesInTab, handleSettingsRefs, } from '../../../helpers/utils/settings-search'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getPlatform } from '../../../../app/scripts/lib/util'; +import { PLATFORM_FIREFOX } from '../../../../shared/constants/app'; export default class AdvancedTab extends PureComponent { static contextTypes = { @@ -630,7 +634,9 @@ export default class AdvancedTab extends PureComponent { {this.renderAutoLockTimeLimit()} {this.renderUserDataBackup()} {this.renderDismissSeedBackupReminderControl()} - {this.renderOverrideContentSecurityPolicyHeader()} + {getPlatform() === PLATFORM_FIREFOX + ? this.renderOverrideContentSecurityPolicyHeader() + : null} ); } From 33508e7b60b30b24f1edf34ae259d2656895656f Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:14:23 +0400 Subject: [PATCH 37/41] Update settings-search.test.js --- ui/helpers/utils/settings-search.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index 323aacd0b8bc..352e59bde714 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -164,7 +164,7 @@ describe('Settings Search Utils', () => { }); it('returns "Advanced" section count', () => { - expect(getNumberOfSettingRoutesInTab(t, t('advanced'))).toStrictEqual(12); + expect(getNumberOfSettingRoutesInTab(t, t('advanced'))).toStrictEqual(11); }); it('returns "Contact" section count', () => { From 0fb3c3f70056a098d2595a62b8e8ea4a9f782219 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:24:26 +0400 Subject: [PATCH 38/41] Update settings-search.test.js --- ui/helpers/utils/settings-search.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index 352e59bde714..30af3ee6b9da 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -151,9 +151,12 @@ describe('Settings Search Utils', () => { describe('getSettingsRoutes', () => { it('should be an array of settings routes objects', () => { const NUM_OF_ENV_FEATURE_FLAG_SETTINGS = 4; + const NUM_OF_HIDDEN_SETTINGS = 1; expect(getSettingsRoutes()).toHaveLength( - SETTINGS_CONSTANTS.length - NUM_OF_ENV_FEATURE_FLAG_SETTINGS, + SETTINGS_CONSTANTS.length - + NUM_OF_ENV_FEATURE_FLAG_SETTINGS - + NUM_OF_HIDDEN_SETTINGS, ); }); }); From 3bf6b9b571e98cbdc093b4fd783bf567e936db6b Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:33:46 +0400 Subject: [PATCH 39/41] fix --- .../errors-after-init-opt-in-background-state.json | 1 + .../state-snapshots/errors-after-init-opt-in-ui-state.json | 1 + .../errors-before-init-opt-in-background-state.json | 1 + .../state-snapshots/errors-before-init-opt-in-ui-state.json | 1 + 4 files changed, 4 insertions(+) diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 1a871780591f..df634884ad3b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -206,6 +206,7 @@ "useCurrencyRateCheck": true, "useRequestQueue": true, "openSeaEnabled": false, + "overrideContentSecurityPolicyHeader": true, "securityAlertsEnabled": "boolean", "watchEthereumAccountEnabled": "boolean", "bitcoinSupportEnabled": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index e8f8f81a6293..a87ac5b05eea 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -122,6 +122,7 @@ "useCurrencyRateCheck": true, "useRequestQueue": true, "openSeaEnabled": false, + "overrideContentSecurityPolicyHeader": true, "securityAlertsEnabled": "boolean", "watchEthereumAccountEnabled": "boolean", "bitcoinSupportEnabled": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 1a51023a2ca1..9ad84e0835be 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -123,6 +123,7 @@ "ledgerTransportType": "webhid", "lostIdentities": "object", "openSeaEnabled": false, + "overrideContentSecurityPolicyHeader": true, "preferences": { "hideZeroBalanceTokens": false, "showExtensionInFullSizeView": false, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index bf7f87a16134..dd698050a544 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -123,6 +123,7 @@ "ledgerTransportType": "webhid", "lostIdentities": "object", "openSeaEnabled": false, + "overrideContentSecurityPolicyHeader": true, "preferences": { "hideZeroBalanceTokens": false, "showExtensionInFullSizeView": false, From 6e9ed23880b85d9a598506e6ba0828d82a95bbc1 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 7 Nov 2024 01:47:32 +0400 Subject: [PATCH 40/41] fixture --- test/e2e/default-fixture.js | 1 + .../errors-after-init-opt-in-background-state.json | 2 +- .../state-snapshots/errors-after-init-opt-in-ui-state.json | 2 +- .../errors-before-init-opt-in-background-state.json | 2 +- .../state-snapshots/errors-before-init-opt-in-ui-state.json | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 2d3d2999ed43..fd2d5be42891 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -193,6 +193,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { currentLocale: 'en', useExternalServices: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index df634884ad3b..cebacab2b1d9 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -198,6 +198,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": "boolean", "useTokenDetection": true, @@ -206,7 +207,6 @@ "useCurrencyRateCheck": true, "useRequestQueue": true, "openSeaEnabled": false, - "overrideContentSecurityPolicyHeader": true, "securityAlertsEnabled": "boolean", "watchEthereumAccountEnabled": "boolean", "bitcoinSupportEnabled": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index a87ac5b05eea..97399d34c508 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -115,6 +115,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": true, "useTokenDetection": true, @@ -122,7 +123,6 @@ "useCurrencyRateCheck": true, "useRequestQueue": true, "openSeaEnabled": false, - "overrideContentSecurityPolicyHeader": true, "securityAlertsEnabled": "boolean", "watchEthereumAccountEnabled": "boolean", "bitcoinSupportEnabled": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 9ad84e0835be..7622ad15937c 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", @@ -123,7 +124,6 @@ "ledgerTransportType": "webhid", "lostIdentities": "object", "openSeaEnabled": false, - "overrideContentSecurityPolicyHeader": true, "preferences": { "hideZeroBalanceTokens": false, "showExtensionInFullSizeView": false, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index dd698050a544..b3fa8d117beb 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", @@ -123,7 +124,6 @@ "ledgerTransportType": "webhid", "lostIdentities": "object", "openSeaEnabled": false, - "overrideContentSecurityPolicyHeader": true, "preferences": { "hideZeroBalanceTokens": false, "showExtensionInFullSizeView": false, From cec02cbfe1ec05e7120437875fac368ea2b04239 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 7 Nov 2024 01:48:49 +0400 Subject: [PATCH 41/41] Update fixture-builder.js --- test/e2e/fixture-builder.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 334e2f74ceca..bea9e9bad77f 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -63,6 +63,7 @@ function onboardingFixture() { advancedGasFee: {}, currentLocale: 'en', dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: {},