From 626ef788a51e681d9a8d6a984c32b93aaa43a564 Mon Sep 17 00:00:00 2001 From: Thomas Cazade Date: Fri, 10 Apr 2026 09:27:19 +0200 Subject: [PATCH] fix(server): safely parse body request when creating new folder --- .../server/routes/dev/public/[...path].ts | 5 ++-- .../src/runtime/server/utils/media/request.ts | 30 +++++++++++++++++++ src/module/test/utils/media-request.test.ts | 22 ++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/module/src/runtime/server/utils/media/request.ts create mode 100644 src/module/test/utils/media-request.test.ts diff --git a/src/module/src/runtime/server/routes/dev/public/[...path].ts b/src/module/src/runtime/server/routes/dev/public/[...path].ts index 0a198448..c01991a1 100644 --- a/src/module/src/runtime/server/routes/dev/public/[...path].ts +++ b/src/module/src/runtime/server/routes/dev/public/[...path].ts @@ -5,6 +5,7 @@ import { withLeadingSlash } from 'ufo' // @ts-expect-error useStorage is not defined in .nuxt/imports.d.ts import { useStorage } from '#imports' import { VIRTUAL_MEDIA_COLLECTION_NAME } from '../../../../utils/constants' +import { parseMediaRequestBody } from '../../../utils/media/request' export default eventHandler(async (event) => { @@ -49,9 +50,7 @@ export default eventHandler(async (event) => { } else { const value = await readRawBody(event, 'utf8') - const json = JSON.parse(value || '{}') - const data = json.raw.split(';base64,')[1] - await storage.setItemRaw(key, Buffer.from(data, 'base64')) + await storage.setItemRaw(key, parseMediaRequestBody(value)) } return 'OK' diff --git a/src/module/src/runtime/server/utils/media/request.ts b/src/module/src/runtime/server/utils/media/request.ts new file mode 100644 index 00000000..1a5cf7a3 --- /dev/null +++ b/src/module/src/runtime/server/utils/media/request.ts @@ -0,0 +1,30 @@ +type MediaRequestBody = { + raw?: string +} + +/** + * Parses a media upload request body into the file contents that should be written to storage. + * + * Folder creation sends a `.gitkeep` media item without a `raw` data URL, so that case must + * create an empty placeholder file instead of trying to decode binary content. + * + * @param value Raw JSON request body received by the media route. + * @returns The file contents to write to storage. + * @throws {Error} When a `raw` payload is present but is not a valid base64 data URL. + * @example + * parseMediaRequestBody(JSON.stringify({ raw: 'data:text/plain;base64,aGVsbG8=' })) + */ +export function parseMediaRequestBody(value: string | undefined): Buffer { + const body = JSON.parse(value || '{}') as MediaRequestBody + + if (typeof body.raw !== 'string') { + return Buffer.alloc(0) + } + + const [, data] = body.raw.split(';base64,') + if (!data) { + throw new Error('Invalid media payload: expected a base64 data URL') + } + + return Buffer.from(data, 'base64') +} diff --git a/src/module/test/utils/media-request.test.ts b/src/module/test/utils/media-request.test.ts new file mode 100644 index 00000000..9f912ac6 --- /dev/null +++ b/src/module/test/utils/media-request.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { parseMediaRequestBody } from '../../src/runtime/server/utils/media/request' + +describe('parseMediaRequestBody', () => { + it('returns an empty buffer for folder placeholder files', () => { + const buffer = parseMediaRequestBody(JSON.stringify({ fsPath: 'hello/.gitkeep' })) + + expect(buffer.length).toBe(0) + }) + + it('decodes base64 media payloads', () => { + const buffer = parseMediaRequestBody(JSON.stringify({ raw: 'data:text/plain;base64,aGVsbG8=' })) + + expect(buffer.toString('utf8')).toBe('hello') + }) + + it('throws on malformed raw payloads', () => { + expect(() => parseMediaRequestBody(JSON.stringify({ raw: 'hello' }))).toThrow( + 'Invalid media payload: expected a base64 data URL', + ) + }) +})