From 3dcf983e28a2577681ec13ee98c108d0a7284e50 Mon Sep 17 00:00:00 2001 From: kekekuli Date: Tue, 29 Sep 2026 16:30:06 +0800 Subject: [PATCH] fix: validate parsed user metadata shape --- src/http/routes/object/getSignedUploadURL.ts | 2 -- src/storage/uploader.test.ts | 32 ++++++++++++++++++++ src/storage/uploader.ts | 10 ++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/http/routes/object/getSignedUploadURL.ts b/src/http/routes/object/getSignedUploadURL.ts index 4a8df863d..1503bba3e 100644 --- a/src/http/routes/object/getSignedUploadURL.ts +++ b/src/http/routes/object/getSignedUploadURL.ts @@ -78,8 +78,6 @@ export default async function routes(fastify: FastifyInstance) { const customMd = request.headers['x-metadata'] if (typeof customMd === 'string') { - // TODO: parseUserMetadata casts to Record but values could be anything; - // validation should be added in a follow-up userMetadata = parseUserMetadata(customMd) } diff --git a/src/storage/uploader.test.ts b/src/storage/uploader.test.ts index 1c4deebc1..55f5c9792 100644 --- a/src/storage/uploader.test.ts +++ b/src/storage/uploader.test.ts @@ -33,6 +33,7 @@ import { FileStore } from './protocols/tus/file-store' import { UploadId } from './protocols/tus/upload-id' import { AssetRenderer } from './renderer/asset' import { Storage } from './storage' +import { parseUserMetadata } from './uploader' type SavedObject = Parameters[0] & { id: string } @@ -588,3 +589,34 @@ describe('multipart fields after the file', () => { expect(fixture.saved?.metadata?.contentEncoding).toBe(contentEncoding) }) }) + +describe('parseUserMetadata', () => { + const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString('base64') + + test('parses a metadata object with arbitrary JSON values', () => { + const metadata = { + source: 'client', + processed: true, + attempts: 3, + labels: ['invoice', 'paid'], + nested: { system: 'erp' }, + empty: null, + } + + expect(parseUserMetadata(encode(metadata))).toEqual(metadata) + }) + + test.each([ + ['null', null], + ['an array', ['invoice', 'paid']], + ['a string', 'client'], + ['a number', 3], + ['a boolean', true], + ])('rejects %s as top-level metadata', (_description, metadata) => { + expect(parseUserMetadata(encode(metadata))).toBeUndefined() + }) + + test('rejects malformed metadata', () => { + expect(parseUserMetadata(Buffer.from('{invalid-json').toString('base64'))).toBeUndefined() + }) +}) diff --git a/src/storage/uploader.ts b/src/storage/uploader.ts index 7a907264f..414173673 100644 --- a/src/storage/uploader.ts +++ b/src/storage/uploader.ts @@ -565,10 +565,16 @@ export async function fileUploadFromRequest( } } -export function parseUserMetadata(metadata: string) { +export function parseUserMetadata(metadata: string): Record | undefined { try { const json = Buffer.from(metadata, 'base64').toString('utf8') - return JSON.parse(json) as Record + const parsed: unknown = JSON.parse(json) + + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + return undefined + } + + return parsed as Record } catch { // no-op return undefined