fix(uploads): hash files without WebAssembly under strict CSP
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m17s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 3m12s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m42s
ci / ts / ts (packages + web) (push) Failing after 4m37s
ci / module boundaries / boundaries (push) Successful in 5s
ci / desktop / rust (desktop proxy) (push) Successful in 6m12s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m17s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 3m12s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m42s
ci / ts / ts (packages + web) (push) Failing after 4m37s
ci / module boundaries / boundaries (push) Successful in 5s
ci / desktop / rust (desktop proxy) (push) Successful in 6m12s
This commit is contained in:
1 parent
f59d1e8d03
commit
b39e4dc28f
7 files changed
+58
-45
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const nginxHeaders = readFileSync(new URL('../../../deploy/nginx/security-headers.conf', import.meta.url), 'utf8');
|
||||
@@ -20,6 +21,10 @@ for (const [platform, policy] of Object.entries(policies)) {
|
||||
await route.fulfill({ contentType: 'application/javascript', body: `
|
||||
try { new Function('return 1')(); document.documentElement.dataset.evalBlocked = 'false'; }
|
||||
catch { document.documentElement.dataset.evalBlocked = 'true'; }
|
||||
WebAssembly.compile(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])).then(
|
||||
() => { document.documentElement.dataset.wasmBlocked = 'false'; },
|
||||
() => { document.documentElement.dataset.wasmBlocked = 'true'; },
|
||||
);
|
||||
` });
|
||||
return;
|
||||
}
|
||||
@@ -27,22 +32,37 @@ for (const [platform, policy] of Object.entries(policies)) {
|
||||
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 result = await page.evaluate(async () => {
|
||||
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;
|
||||
try {
|
||||
const results = [];
|
||||
// Reuse one worker for empty, known-vector and multi-chunk binary files.
|
||||
const binary = Uint8Array.from({ length: (9 << 20) + 17 }, (_, i) => i % 251);
|
||||
for (const [id, bytes] of [['empty', new Uint8Array()], ['abc', new TextEncoder().encode('abc')], ['binary', binary]] as const) {
|
||||
results.push(await new Promise((resolve, reject) => {
|
||||
const progress: number[] = [];
|
||||
worker.onerror = (event) => reject(new Error(event.message));
|
||||
worker.onmessage = ({ data }) => {
|
||||
if (data.type === 'progress') { progress.push(data.done); return; }
|
||||
resolve({ ...data, progress });
|
||||
};
|
||||
worker.postMessage({ id, file: new File([bytes], `${id}.flac`) });
|
||||
}));
|
||||
}
|
||||
return results;
|
||||
} finally {
|
||||
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',
|
||||
}
|
||||
});
|
||||
const binary = Uint8Array.from({ length: (9 << 20) + 17 }, (_, i) => i % 251);
|
||||
expect(result).toEqual([
|
||||
{ id: 'empty', type: 'done', sha256: createHash('sha256').update('').digest('hex'), progress: [] },
|
||||
{ id: 'abc', type: 'done', sha256: createHash('sha256').update('abc').digest('hex'), progress: [] },
|
||||
{ id: 'binary', type: 'done', sha256: createHash('sha256').update(binary).digest('hex'), progress: expect.arrayContaining([expect.any(Number)]) },
|
||||
]);
|
||||
// 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');
|
||||
await expect(page.locator('html')).toHaveAttribute('data-wasm-blocked', 'true');
|
||||
});
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"@tanstack/react-query": "^5.80.7",
|
||||
"@tanstack/react-router": "^1.120.20",
|
||||
"@tanstack/react-virtual": "^3.14.21",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"js-sha256": "^1.0.0",
|
||||
"music-metadata": "^12.2.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// <reference lib="webworker" />
|
||||
// SHA-256 файла потоком (docs/uploads.md#4): Web Crypto не умеет считать по частям, а файлы бывают по 2 ГБ —
|
||||
// hash-wasm считает чанками в воркере, не держа файл в памяти и не блокируя интерфейс.
|
||||
import { createSHA256 } from 'hash-wasm';
|
||||
// js-sha256 считает чанками в воркере без WebAssembly: работает и со строгим CSP у прокси.
|
||||
import { sha256 } from 'js-sha256';
|
||||
|
||||
export interface HashRequest {
|
||||
id: string;
|
||||
@@ -18,8 +18,7 @@ const ctx = self as unknown as DedicatedWorkerGlobalScope;
|
||||
ctx.onmessage = async (e: MessageEvent<HashRequest>) => {
|
||||
const { id, file } = e.data;
|
||||
try {
|
||||
const h = await createSHA256();
|
||||
h.init();
|
||||
const h = sha256.create();
|
||||
const reader = file.stream().getReader();
|
||||
let done = 0;
|
||||
let lastReport = 0;
|
||||
@@ -33,7 +32,7 @@ ctx.onmessage = async (e: MessageEvent<HashRequest>) => {
|
||||
ctx.postMessage({ id, type: 'progress', done } satisfies HashResponse);
|
||||
}
|
||||
}
|
||||
ctx.postMessage({ id, type: 'done', sha256: h.digest('hex') } satisfies HashResponse);
|
||||
ctx.postMessage({ id, type: 'done', sha256: h.hex() } satisfies HashResponse);
|
||||
} catch (err) {
|
||||
ctx.postMessage({ id, type: 'error', message: err instanceof Error ? err.message : String(err) } satisfies HashResponse);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user