|
| 1 | +import { z } from "zod"; |
1 | 2 | import { AUDIT_ACTIONS, type AuditActionKey } from "~/modules/audit"; |
2 | 3 | import { getCurrentYear } from "~/modules/domain"; |
3 | 4 | import { parseSiren } from "~/modules/shared/parseSiren"; |
@@ -225,11 +226,11 @@ export async function POST(request: Request): Promise<Response> { |
225 | 226 | userId, |
226 | 227 | userEmail, |
227 | 228 | siren, |
228 | | - metadata: { |
| 229 | + metadata: uploadAuditMetadataSchema.parse({ |
229 | 230 | flowType, |
230 | 231 | fileId: result.fileId, |
231 | 232 | fileName: result.fileName, |
232 | | - }, |
| 233 | + }), |
233 | 234 | ipAddress: requestContext.ipAddress, |
234 | 235 | userAgent: requestContext.userAgent, |
235 | 236 | durationMs: Date.now() - startedAt, |
@@ -323,6 +324,23 @@ function sanitizeUserText(value: string): string { |
323 | 324 | return out.slice(0, 255); |
324 | 325 | } |
325 | 326 |
|
| 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 | + |
326 | 344 | type AuditFailureInput = { |
327 | 345 | action: AuditActionKey; |
328 | 346 | flowType: FlowType; |
@@ -352,11 +370,13 @@ function writeFailure({ |
352 | 370 | virusName = null, |
353 | 371 | s3Cleanup = null, |
354 | 372 | }: 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 | + }); |
360 | 380 |
|
361 | 381 | void logAction({ |
362 | 382 | action, |
|
0 commit comments