diff --git a/apps/server/src/server/api/routers/crypto.router.ts b/apps/server/src/server/api/routers/crypto.router.ts index 075ec39..dc65289 100644 --- a/apps/server/src/server/api/routers/crypto.router.ts +++ b/apps/server/src/server/api/routers/crypto.router.ts @@ -11,6 +11,7 @@ import { import { ORPCError } from '@orpc/server' import type { JSZipObject } from 'jszip' import JSZip from 'jszip' +import { z } from 'zod' import { getUxConfig } from '@/server/ux-config' import { db } from '../middlewares' import { os } from '../server' @@ -20,6 +21,14 @@ interface ZipFileItem { bytes: Uint8Array } +const summaryPayloadSchema = z + .object({ + taskId: z.string().min(1, 'summary.json must contain a non-empty taskId'), + checkId: z.string().optional(), + inspectionId: z.string().optional(), + }) + .passthrough() + const MAX_RAW_ZIP_BYTES = 50 * 1024 * 1024 const MAX_SINGLE_FILE_BYTES = 20 * 1024 * 1024 const MAX_TOTAL_UNCOMPRESSED_BYTES = 60 * 1024 * 1024 @@ -165,7 +174,7 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy const zipFiles = await listSafeZipFiles(rawZip) - // Extract and parse summary.json from the ZIP + // Extract and validate summary.json from the ZIP const summaryFile = zipFiles.find((f) => f.name === 'summary.json') if (!summaryFile) { throw new ORPCError('BAD_REQUEST', { @@ -173,24 +182,25 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy }) } - let summaryPayload: Record + let rawJson: unknown try { - summaryPayload = JSON.parse(Buffer.from(summaryFile.bytes).toString('utf-8')) + rawJson = JSON.parse(Buffer.from(summaryFile.bytes).toString('utf-8')) } catch { throw new ORPCError('BAD_REQUEST', { message: 'summary.json in the ZIP is not valid JSON', }) } - // Derive signingContext from summary.json fields - const taskId = String(summaryPayload.taskId ?? '') - const checkId = String(summaryPayload.checkId ?? summaryPayload.inspectionId ?? '') - if (!taskId) { + const parsed = summaryPayloadSchema.safeParse(rawJson) + if (!parsed.success) { throw new ORPCError('BAD_REQUEST', { - message: 'summary.json must contain a taskId field', + message: `Invalid summary.json: ${z.prettifyError(parsed.error)}`, }) } - const signingContext = `${taskId}${checkId}` + + const summaryPayload = parsed.data + const checkId = summaryPayload.checkId ?? summaryPayload.inspectionId ?? '' + const signingContext = `${summaryPayload.taskId}${checkId}` // Compute device signature const ikm = config.licence + config.fingerprint