diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index b42fe49..9612f6a 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -13,7 +13,7 @@ "withGlobalTauri": true, "windows": [{ "label": "main", "title": "Pipu Music", "width": 1200, "height": 800, "minWidth": 720, "minHeight": 540 }], "security": { - "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:*; font-src 'self' data:; media-src 'self' blob: http://127.0.0.1:*; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:*; object-src 'none'; base-uri 'self'; frame-src 'none'" + "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:*; font-src 'self' data:; media-src 'self' blob: http://127.0.0.1:*; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:*; object-src 'none'; base-uri 'self'; frame-src 'none'" } }, "bundle": { diff --git a/apps/web/e2e/uploads-csp.spec.ts b/apps/web/e2e/uploads-csp.spec.ts new file mode 100644 index 0000000..7a434e3 --- /dev/null +++ b/apps/web/e2e/uploads-csp.spec.ts @@ -0,0 +1,48 @@ +import { readFileSync } from 'node:fs'; +import { expect, test } from '@playwright/test'; + +const nginxHeaders = readFileSync(new URL('../../../deploy/nginx/security-headers.conf', import.meta.url), 'utf8'); +const desktopConfig = JSON.parse(readFileSync(new URL('../../desktop/src-tauri/tauri.conf.json', import.meta.url), 'utf8')); +const policies: Record = { + web: nginxHeaders.match(/add_header Content-Security-Policy "([^"]+)" always;/)![1], + desktop: desktopConfig.app.security.csp, +}; + +for (const [platform, policy] of Object.entries(policies)) { + test(`upload hash worker runs under ${platform} CSP`, async ({ page }) => { + // Apply the deployed policy to both the document and worker responses. + await page.route('**/*', async (route) => { + if (new URL(route.request().url()).pathname === '/csp-upload-test') { + await route.fulfill({ contentType: 'text/html', body: 'Upload CSP', headers: { 'Content-Security-Policy': policy } }); + return; + } + if (new URL(route.request().url()).pathname === '/csp-eval-test.js') { + await route.fulfill({ contentType: 'application/javascript', body: ` + try { new Function('return 1')(); document.documentElement.dataset.evalBlocked = 'false'; } + catch { document.documentElement.dataset.evalBlocked = 'true'; } + ` }); + return; + } + const response = await route.fetch(); + await route.fulfill({ response, headers: { ...response.headers(), 'Content-Security-Policy': policy } }); + }); + await page.goto('/csp-upload-test'); + const result = await page.evaluate(() => new Promise((resolve, reject) => { + const worker = new Worker('/src/features/uploads/hash.worker.ts', { type: 'module' }); + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.onmessage = ({ data }) => { + if (data.type === 'progress') return; + worker.terminate(); + resolve(data); + }; + worker.postMessage({ id: 'csp-test', file: new File(['abc'], 'track.flac') }); + })); + expect(result).toEqual({ + id: 'csp-test', type: 'done', + sha256: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', + }); + // A page script observes CSP; Playwright's evaluate bypasses the eval restriction. + await page.addScriptTag({ url: '/csp-eval-test.js' }); + await expect(page.locator('html')).toHaveAttribute('data-eval-blocked', 'true'); + }); +} diff --git a/deploy/nginx/security-headers.conf b/deploy/nginx/security-headers.conf index ab6baf9..29b10ee 100644 --- a/deploy/nginx/security-headers.conf +++ b/deploy/nginx/security-headers.conf @@ -3,4 +3,5 @@ add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "DENY" always; -add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; +# hash-wasm считает SHA-256 загрузок в Web Worker; разрешаем WASM без JavaScript eval. +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;