Skip to content

Commit 6737f49

Browse files
committed
fix: validate upload audit metadata with zod schema
1 parent 3e4c868 commit 6737f49

1 file changed

Lines changed: 27 additions & 7 deletions

File tree

packages/app/src/app/api/upload/route.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { z } from "zod";
12
import { AUDIT_ACTIONS, type AuditActionKey } from "~/modules/audit";
23
import { getCurrentYear } from "~/modules/domain";
34
import { parseSiren } from "~/modules/shared/parseSiren";
@@ -225,11 +226,11 @@ export async function POST(request: Request): Promise<Response> {
225226
userId,
226227
userEmail,
227228
siren,
228-
metadata: {
229+
metadata: uploadAuditMetadataSchema.parse({
229230
flowType,
230231
fileId: result.fileId,
231232
fileName: result.fileName,
232-
},
233+
}),
233234
ipAddress: requestContext.ipAddress,
234235
userAgent: requestContext.userAgent,
235236
durationMs: Date.now() - startedAt,
@@ -323,6 +324,23 @@ function sanitizeUserText(value: string): string {
323324
return out.slice(0, 255);
324325
}
325326

327+
/**
328+
* Audit metadata schema for /api/upload. Fields sourced from user input
329+
* (fileName, virusName) use `sanitizedUserString` so future additions to the
330+
* schema are sanitised by construction — a new untrusted string just has to
331+
* reuse the same transform. Internal literals (flowType, fileId, s3Cleanup)
332+
* are already constrained and do not need sanitisation.
333+
*/
334+
const sanitizedUserString = z.string().transform(sanitizeUserText);
335+
336+
const uploadAuditMetadataSchema = z.object({
337+
flowType: z.enum(["cse_opinion", "joint_evaluation"]),
338+
fileId: z.string().optional(),
339+
fileName: sanitizedUserString.optional(),
340+
virusName: sanitizedUserString.optional(),
341+
s3Cleanup: z.enum(["ok", "failed"]).optional(),
342+
});
343+
326344
type AuditFailureInput = {
327345
action: AuditActionKey;
328346
flowType: FlowType;
@@ -352,11 +370,13 @@ function writeFailure({
352370
virusName = null,
353371
s3Cleanup = null,
354372
}: AuditFailureInput): void {
355-
const metadata: Record<string, unknown> = { flowType };
356-
if (fileName) metadata.fileName = sanitizeUserText(fileName);
357-
if (fileId) metadata.fileId = fileId;
358-
if (virusName) metadata.virusName = sanitizeUserText(virusName);
359-
if (s3Cleanup) metadata.s3Cleanup = s3Cleanup;
373+
const metadata = uploadAuditMetadataSchema.parse({
374+
flowType,
375+
fileName: fileName ?? undefined,
376+
fileId: fileId ?? undefined,
377+
virusName: virusName ?? undefined,
378+
s3Cleanup: s3Cleanup ?? undefined,
379+
});
360380

361381
void logAction({
362382
action,

0 commit comments

Comments
 (0)