mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Next reads the body of every non-GET request the proxy matches into memory before the route runs. The ceiling was 512 MB so the Apple Health importer could receive whole archives, which let any anonymous POST to any API path make the server hold that much. The ceiling is now 1 MB (proxyClientMaxBodySize). The fifteen routes whose real bodies are larger are left out of the proxy matcher and read their own body after authenticating; apiHandler gives them the proxy's refusals (worker-only, demo mode), security headers and a CSP. safeJson now counts bytes while it reads and stops at its cap instead of reading everything first, the CSV and dose-history imports do the same, and a multipart backup upload is counted while it is read. A test compiles the matcher the way Next does and checks it skips exactly the listed routes and every route with a larger body is listed. Local lab OCR runs its WebAssembly engine in a worker served from /tesseract/. Those responses, and the /labs and /documents pages that start the OCR, now carry 'wasm-unsafe-eval' in script-src; no other route does.
66 lines
1.8 KiB
TypeScript
66 lines
1.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { NextRequest } from "next/server";
|
|
|
|
/**
|
|
* Local lab OCR runs tesseract's WebAssembly engine in a same-origin worker
|
|
* served from `/tesseract/`. Compiling WebAssembly needs `'wasm-unsafe-eval'`
|
|
* in `script-src`, and a worker takes its CSP from its own script's response.
|
|
* The grant is scoped the way the AI-host and Withings carve-outs are: the
|
|
* worker files and the two pages that start OCR, and nothing else.
|
|
*/
|
|
|
|
vi.mock("@/lib/process-type", () => ({
|
|
shouldRunWeb: () => true,
|
|
}));
|
|
|
|
import { proxy } from "../proxy";
|
|
|
|
beforeEach(() => {
|
|
vi.stubEnv("NODE_ENV", "production");
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
function scriptSrc(pathname: string): string {
|
|
const res = proxy(
|
|
new NextRequest(`http://localhost${pathname}`, {
|
|
headers: { cookie: "healthlog_session=sess-1" },
|
|
}),
|
|
);
|
|
const csp = res.headers.get("content-security-policy") ?? "";
|
|
return csp.split(";").find((d) => d.trim().startsWith("script-src")) ?? "";
|
|
}
|
|
|
|
describe("'wasm-unsafe-eval' for local OCR", () => {
|
|
it.each([
|
|
"/tesseract/worker.min.js",
|
|
"/tesseract/tesseract-core-simd-lstm.wasm.js",
|
|
"/labs",
|
|
"/documents",
|
|
])("is granted on %s", (path) => {
|
|
expect(scriptSrc(path)).toContain("'wasm-unsafe-eval'");
|
|
});
|
|
|
|
it.each([
|
|
"/dashboard",
|
|
"/",
|
|
"/labs/some-biomarker",
|
|
"/settings/labs",
|
|
"/settings/ai",
|
|
"/api/labs/ocr/commit",
|
|
"/tesseractish",
|
|
])("is not granted on %s", (path) => {
|
|
const src = scriptSrc(path);
|
|
expect(src).toContain("script-src");
|
|
expect(src).not.toContain("wasm-unsafe-eval");
|
|
});
|
|
|
|
it("never widens script-src to plain eval", () => {
|
|
expect(scriptSrc("/tesseract/worker.min.js")).not.toContain(
|
|
"'unsafe-eval'",
|
|
);
|
|
});
|
|
});
|