From 68e9fddedd93bf1e0bd6e52e2d2e4bd74e9e9d41 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Thu, 17 Aug 2023 01:39:58 +0200 Subject: [PATCH 1/4] Add security headers to dev server --- .proxyrc.js | 17 +++++++++++++++++ README.md | 2 +- internals/getSecurityHeaders.js | 13 +++++++++++++ package.json | 1 + yarn.lock | 7 +++++++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .proxyrc.js diff --git a/.proxyrc.js b/.proxyrc.js new file mode 100644 index 0000000000..f85c4eb6a6 --- /dev/null +++ b/.proxyrc.js @@ -0,0 +1,17 @@ +// @ts-check + +/** + * Add security headers to dev server + * @param {import('connect').Server} app + */ +module.exports = (app) => { + app.use((req, res, next) => { + // Re-generate headers on every request so editing the file is reflected on reload. + delete require.cache[require.resolve('./internals/getSecurityHeaders.js')]; + const { getCsp, getPermissionsPolicy } = require('./internals/getSecurityHeaders.js') + + res.setHeader('Content-Security-Policy', getCsp({ isDev: true, isExtension: false })) + res.setHeader('Permissions-Policy', getPermissionsPolicy()) + next() + }) +} diff --git a/README.md b/README.md index 139c18e071..4732afe89a 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ yarn test yarn start (cd playwright; yarn; npx playwright install --with-deps) (cd playwright; yarn test) -# Or set BASE_URL and EXTENSION_PATH to test production builds and test security headers. +# Or set BASE_URL and EXTENSION_PATH to test production builds. # Or `xvfb-run yarn test` to prevent browser windows opening. # Run cypress tests diff --git a/internals/getSecurityHeaders.js b/internals/getSecurityHeaders.js index 518b23ae9d..0b4a3c8651 100644 --- a/internals/getSecurityHeaders.js +++ b/internals/getSecurityHeaders.js @@ -10,9 +10,19 @@ const dappFrameAncestors = ` http://localhost:* http://127.0.0.1:* ` +const localnet = ` + http://localhost:42280 + http://localhost:9001 +` const hmrWebsocket = ` ws://localhost:2222 ` +const reactErrorOverlay = ` + 'sha256-RV6I4HWPb71LvA27WVD3cEz8GsJrHlfcM/2X2Q5gV00=' +` +const hmrScripts = ` + 'unsafe-eval' +` /** * Keep this synced with deployment headers @@ -24,6 +34,8 @@ const getCsp = ({ isExtension, isDev }) => default-src 'none'; script-src 'self' + ${isDev ? reactErrorOverlay : ''} + ${isDev ? hmrScripts : ''} 'report-sample'; style-src 'self' @@ -37,6 +49,7 @@ const getCsp = ({ isExtension, isDev }) => https://testnet.grpc.oasis.dev https://api.oasisscan.com https://monitor.oasis.dev + ${isDev ? localnet : ''} ${isDev ? hmrWebsocket : ''} ; frame-ancestors diff --git a/package.json b/package.json index 8c9887a473..54718b0061 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@testing-library/react": "13.4.0", "@testing-library/user-event": "14.4.3", "@types/body-scroll-lock": "3.1.0", + "@types/connect": "3.4.35", "@types/jest": "29.5.4", "@types/jest-when": "3.5.2", "@types/lodash": "4.14.197", diff --git a/yarn.lock b/yarn.lock index 1186093ec7..ca74b8a8e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3554,6 +3554,13 @@ resolved "https://registry.yarnpkg.com/@types/body-scroll-lock/-/body-scroll-lock-3.1.0.tgz#435f6abf682bf58640e1c2ee5978320b891970e7" integrity sha512-3owAC4iJub5WPqRhxd8INarF2bWeQq1yQHBgYhN0XLBJMpd5ED10RrJ3aKiAwlTyL5wK7RkBD4SZUQz2AAAMdA== +"@types/connect@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" From a91dce126142eec5930b9a96268e271f6a6b44bc Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 18 Aug 2023 21:07:27 +0200 Subject: [PATCH 2/4] Make E2E tests pass with dev server --- playwright/playwright.config.ts | 8 +++--- playwright/tests/extension.spec.ts | 8 +++++- playwright/tests/fiat.spec.ts | 42 ++++++++++++----------------- playwright/tests/ledger.spec.ts | 3 +-- playwright/tests/validators.spec.ts | 3 +-- 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index 1f56615d68..2b1eca8be1 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -1,6 +1,8 @@ import type { PlaywrightTestConfig } from '@playwright/test' import { devices } from '@playwright/test' +const doubleIfDev = (time: number) => (process.env.BASE_URL ? time : time * 2) + /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -13,13 +15,13 @@ import { devices } from '@playwright/test' const config: PlaywrightTestConfig = { testDir: './tests', /* Maximum time one test can run for. */ - timeout: 30 * 1000, + timeout: doubleIfDev(30 * 1000), expect: { /** * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 8000, + timeout: doubleIfDev(8000), }, /* Run tests in files in parallel */ fullyParallel: true, @@ -34,7 +36,7 @@ const config: PlaywrightTestConfig = { /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 3000, + actionTimeout: doubleIfDev(3000), /* Base URL to use in actions like `await page.goto('/')`. */ /* Test dev server by default, but also allow testing `start:prod`. */ baseURL: process.env.BASE_URL ?? 'http://localhost:3000', diff --git a/playwright/tests/extension.spec.ts b/playwright/tests/extension.spec.ts index d1de603445..17937e1f26 100644 --- a/playwright/tests/extension.spec.ts +++ b/playwright/tests/extension.spec.ts @@ -75,7 +75,13 @@ test.describe('The extension popup should load', () => { }) test('should allow embedded Transak widget', async ({ page, extensionId }) => { - await expectNoErrorsInConsole(page) + await expectNoErrorsInConsole(page, { + ignoreError: msg => { + // Odd errors inside Transak + if (msg.text().includes('responded with a status of 403')) return true + if (msg.text().includes('`sessionKey` is a required property')) return true + }, + }) await page.goto(`chrome-extension://${extensionId}/${popupFile}#/open-wallet/private-key`) await fillPrivateKeyWithoutPassword(page, { privateKey: privateKey, diff --git a/playwright/tests/fiat.spec.ts b/playwright/tests/fiat.spec.ts index 4c003fa44c..a5bd5d7984 100644 --- a/playwright/tests/fiat.spec.ts +++ b/playwright/tests/fiat.spec.ts @@ -22,10 +22,15 @@ async function setup(page: Page) { } test.describe('Fiat on-ramp', () => { - test('Content-Security-Policy should allow embedded Transak widget', async ({ page, baseURL }) => { - expect(baseURL).toBe('http://localhost:5000') + test('Content-Security-Policy should allow embedded Transak widget', async ({ page }) => { expect((await page.request.head('/')).headers()).toHaveProperty('content-security-policy') - await expectNoErrorsInConsole(page) + await expectNoErrorsInConsole(page, { + ignoreError: msg => { + // Odd errors inside Transak + if (msg.text().includes('responded with a status of 403')) return true + if (msg.text().includes('`sessionKey` is a required property')) return true + }, + }) await setup(page) await page .getByText( @@ -37,18 +42,12 @@ test.describe('Fiat on-ramp', () => { await expect(page.frameLocator('iframe')!.getByText('Please Enter Your Email')).toBeVisible() }) - test('Content-Security-Policy should also allow Transak staging iframe', async ({ page, baseURL }) => { - expect(baseURL).toBe('http://localhost:5000') + test('Content-Security-Policy should also allow Transak staging iframe', async ({ page }) => { expect((await page.request.head('/')).headers()).toHaveProperty('content-security-policy') await expectNoErrorsInConsole(page) await setup(page) - await page.route('https://global.transak.com/*', route => - route.fulfill({ - status: 301, - headers: { - Location: 'https://global-stg.transak.com/', - }, - }), + await page.route('https://*.transak.com/?*', route => + route.fulfill({ status: 301, headers: { Location: 'https://global-stg.transak.com/' } }), ) await page @@ -58,19 +57,13 @@ test.describe('Fiat on-ramp', () => { .click() }) - test('Content-Security-Policy should block unknown iframe and fail', async ({ page, baseURL }) => { + test('Content-Security-Policy should block unknown iframe and fail', async ({ page }) => { test.fail() - expect(baseURL).toBe('http://localhost:5000') expect((await page.request.head('/')).headers()).toHaveProperty('content-security-policy') // await expectNoErrorsInConsole(page) // TODO: revert when playwright doesn't skip other tests because of this await setup(page) - await page.route('https://global.transak.com/*', route => - route.fulfill({ - status: 301, - headers: { - Location: 'https://phishing-transak.com/', - }, - }), + await page.route('https://*.transak.com/*', route => + route.fulfill({ status: 301, headers: { Location: 'https://phishing-transak.com/' } }), ) await page.route('https://phishing-transak.com/', route => route.fulfill({ body: `phishing` })) @@ -84,11 +77,11 @@ test.describe('Fiat on-ramp', () => { ]) }) - test('Sandbox should block top-navigation from iframe and fail', async ({ page, baseURL }) => { + test('Sandbox should block top-navigation from iframe and fail', async ({ page }) => { test.fail() // await expectNoErrorsInConsole(page) // TODO: revert when playwright doesn't skip other tests because of this await setup(page) - await page.route('https://global.transak.com/*', route => + await page.route('https://*.transak.com/*', route => route.fulfill({ body: ``, }), @@ -106,8 +99,7 @@ test.describe('Fiat on-ramp', () => { await expect(page).toHaveURL('https://phishing-wallet.com/') }) - test('Permissions-Policy should contain Transak permissions', async ({ page, baseURL }) => { - expect(baseURL).toBe('http://localhost:5000') + test('Permissions-Policy should contain Transak permissions', async ({ page }) => { expect((await page.request.head('/')).headers()).toHaveProperty('permissions-policy') await expectNoErrorsInConsole(page) await setup(page) diff --git a/playwright/tests/ledger.spec.ts b/playwright/tests/ledger.spec.ts index 176dee94e1..ed77b04b44 100644 --- a/playwright/tests/ledger.spec.ts +++ b/playwright/tests/ledger.spec.ts @@ -2,8 +2,7 @@ import { test, expect } from '@playwright/test' import { expectNoErrorsInConsole } from '../utils/expectNoErrorsInConsole' test.describe('Ledger', () => { - test('Permissions-Policy should allow USB', async ({ page, baseURL }) => { - expect(baseURL).toBe('http://localhost:5000') + test('Permissions-Policy should allow USB', async ({ page }) => { expect((await page.request.head('/')).headers()).toHaveProperty('permissions-policy') await expectNoErrorsInConsole(page) diff --git a/playwright/tests/validators.spec.ts b/playwright/tests/validators.spec.ts index f740c643fa..6916d603d7 100644 --- a/playwright/tests/validators.spec.ts +++ b/playwright/tests/validators.spec.ts @@ -28,8 +28,7 @@ test.beforeEach(async ({ page }) => { }) test.describe('Validators', () => { - test('Content-Security-Policy should allow validator icons', async ({ page, baseURL }) => { - expect(baseURL).toBe('http://localhost:5000') + test('Content-Security-Policy should allow validator icons', async ({ page }) => { expect((await page.request.head('/')).headers()).toHaveProperty('content-security-policy') const someValidatorIconPromise = page.waitForResponse( From 94b2d4652becabe06bdabf3558c2ef8bca41aba9 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Thu, 24 Aug 2023 23:22:21 +0200 Subject: [PATCH 3/4] Fix cypress tests --- internals/getSecurityHeaders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/getSecurityHeaders.js b/internals/getSecurityHeaders.js index 0b4a3c8651..e6e109447d 100644 --- a/internals/getSecurityHeaders.js +++ b/internals/getSecurityHeaders.js @@ -78,7 +78,7 @@ const getPermissionsPolicy = () => camera=*, cross-origin-isolated=(), display-capture=(), - document-domain=(), + document-domain=(self), encrypted-media=*, execution-while-not-rendered=(), execution-while-out-of-viewport=(), From c9072205418c57ab66392a98c246ebcc7a2fa623 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 25 Aug 2023 03:13:39 +0200 Subject: [PATCH 4/4] Test react-error-overlay passes dev CSP --- internals/getSecurityHeaders.js | 6 ++---- playwright/tests/csp-react-error-overlay.spec.ts | 12 ++++++++++++ src/app/pages/E2EPage/index.tsx | 11 +++++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 playwright/tests/csp-react-error-overlay.spec.ts diff --git a/internals/getSecurityHeaders.js b/internals/getSecurityHeaders.js index e6e109447d..3f2a74f875 100644 --- a/internals/getSecurityHeaders.js +++ b/internals/getSecurityHeaders.js @@ -17,9 +17,7 @@ const localnet = ` const hmrWebsocket = ` ws://localhost:2222 ` -const reactErrorOverlay = ` - 'sha256-RV6I4HWPb71LvA27WVD3cEz8GsJrHlfcM/2X2Q5gV00=' -` +const reactErrorOverlay = `'sha256-RV6I4HWPb71LvA27WVD3cEz8GsJrHlfcM/2X2Q5gV00='` const hmrScripts = ` 'unsafe-eval' ` @@ -106,4 +104,4 @@ const getPermissionsPolicy = () => .join(' ') .replace(/ ,/g, ',') -module.exports = { getCsp, getPermissionsPolicy } +module.exports = { getCsp, getPermissionsPolicy, reactErrorOverlay } diff --git a/playwright/tests/csp-react-error-overlay.spec.ts b/playwright/tests/csp-react-error-overlay.spec.ts new file mode 100644 index 0000000000..7a340d5518 --- /dev/null +++ b/playwright/tests/csp-react-error-overlay.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test' +import { reactErrorOverlay } from '../../internals/getSecurityHeaders.js' + +test('Dev Content-Security-Policy should allow react-error-overlay', async ({ page, baseURL }) => { + if (baseURL !== 'http://localhost:3000') test.skip() + expect((await page.request.head('/')).headers()).toHaveProperty('content-security-policy') + expect((await page.request.head('/')).headers()['content-security-policy']).toContain(reactErrorOverlay) + await page.goto('/e2e') + await page.getByRole('button', { name: 'Trigger uncaught error' }).click() + await expect(page.locator('iframe')).toBeVisible() + await expect(page.frameLocator('iframe').getByText('ReferenceError')).toBeVisible() +}) diff --git a/src/app/pages/E2EPage/index.tsx b/src/app/pages/E2EPage/index.tsx index 8dd1e2b988..30fd5b1477 100644 --- a/src/app/pages/E2EPage/index.tsx +++ b/src/app/pages/E2EPage/index.tsx @@ -22,7 +22,7 @@ export function E2EPage() {

- +
@@ -118,7 +118,7 @@ function UnsafePasswordField(props: { ) } -function TriggerFatalError() { +function TriggerError() { const dispatch = useDispatch() return (
@@ -128,6 +128,13 @@ function TriggerFatalError() { dispatch(walletActions.openWalletFromPrivateKey({ privateKey: '0xAA', choosePassword: undefined })) }} /> +
) }