From 89cab66898f4edb238eee2321564337034dfa2bc Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 6 May 2025 18:26:33 -0400 Subject: [PATCH] fix multipart/form-data decoding --- packages/backend/src/misc/create-temp.ts | 13 +++++++ .../src/server/ServerUtilityService.ts | 39 ++++++++++++++----- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts index 6cc896046f..fda63c7a9d 100644 --- a/packages/backend/src/misc/create-temp.ts +++ b/packages/backend/src/misc/create-temp.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { pipeline } from 'node:stream/promises'; +import fs from 'node:fs'; import * as tmp from 'tmp'; export function createTemp(): Promise<[string, () => void]> { @@ -27,3 +29,14 @@ export function createTempDir(): Promise<[string, () => void]> { ); }); } + +export async function saveToTempFile(stream: NodeJS.ReadableStream): Promise { + const [filepath, cleanup] = await createTemp(); + try { + await pipeline(stream, fs.createWriteStream(filepath)); + return filepath; + } catch (e) { + cleanup(); + throw e; + } +} diff --git a/packages/backend/src/server/ServerUtilityService.ts b/packages/backend/src/server/ServerUtilityService.ts index f2900fad4f..d90b37ca50 100644 --- a/packages/backend/src/server/ServerUtilityService.ts +++ b/packages/backend/src/server/ServerUtilityService.ts @@ -9,6 +9,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { FastifyInstance } from 'fastify'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { saveToTempFile } from '@/misc/create-temp.js'; @Injectable() export class ServerUtilityService { @@ -29,17 +30,16 @@ export class ServerUtilityService { // Store to temporary file instead, and copy the body fields while we're at it. fastify.addHook<{ Body?: Record }>('onRequest', async request => { if (request.isMultipart()) { - const body = request.body ??= {}; + // We can't use saveRequestFiles() because it erases all the data fields. + // Instead, recreate it manually. + // https://github.com/fastify/fastify-multipart/issues/549 - // Save upload to temp directory. - // These are attached to request.savedRequestFiles - await request.saveRequestFiles(); + for await (const part of request.parts()) { + if (part.type === 'field') { + const k = part.fieldname; + const v = String(part.value); + const body = request.body ??= {}; - // Copy fields to body - const formData = await request.formData(); - formData.forEach((v, k) => { - // This can be string or File, and we handle files above. - if (typeof(v) === 'string') { // This is just progressive conversion from undefined -> string -> string[] if (body[k]) { if (Array.isArray(body[k])) { @@ -50,8 +50,27 @@ export class ServerUtilityService { } else { body[k] = v; } + } else { // Otherwise it's a file + try { + const [filepath] = await saveToTempFile(part.file); + + const tmpUploads = (request.tmpUploads ??= []); + tmpUploads.push(filepath); + + const requestSavedFiles = (request.savedRequestFiles ??= []); + requestSavedFiles.push({ + ...part, + filepath, + }); + } catch (e) { + // Cleanup to avoid file leak in case of errors + await request.cleanRequestFiles(); + request.tmpUploads = null; + request.savedRequestFiles = null; + throw e; + } } - }); + } } }); }