Merge pull request #35899 from overleaf/bg-custom-multer-workaround

add custom multer upload handler to prevent leaked files

GitOrigin-RevId: 7b390d326b4cf81fd82c532c53e9772cb4107a29
This commit is contained in:
Brian Gough
2026-09-17 11:58:42 +00:00
committed by Copybot
parent 44753a2a50
commit 446c4f3f6a
5 changed files with 379 additions and 28 deletions
@@ -10,8 +10,6 @@ import EditorController from '../Editor/EditorController.mjs'
import ProjectLocator from '../Project/ProjectLocator.mjs'
import Settings from '@overleaf/settings'
import { InvalidZipFileError } from './ArchiveErrors.mjs'
import multer from 'multer'
import lodash from 'lodash'
import { expressify } from '@overleaf/promise-utils'
import {
DuplicateNameError,
@@ -22,10 +20,9 @@ import {
import DocumentConversionManager from './DocumentConversionManager.mjs'
import ProjectOptionsHandler from '../Project/ProjectOptionsHandler.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
import { multer, multerUploadHandler } from '../../infrastructure/Multer.mjs'
import { parseReq, z, zz } from '../../infrastructure/Validation.mjs'
const defaultsDeep = lodash.defaultsDeep
const uploadMetaTypeSchema = z.string().optional()
const uploadProjectSchema = z.object({
@@ -72,17 +69,12 @@ const importDocumentSchema = z.object({
file: zz.uploadedFile(),
})
const upload = multer(
defaultsDeep(
{
dest: Settings.path.uploadFolder,
limits: {
fileSize: Settings.maxUploadSize,
},
},
Settings.multerOptions
)
)
const upload = multerUploadHandler({
dest: Settings.path.uploadFolder,
limits: {
fileSize: Settings.maxUploadSize,
},
})
/**
* @param {any} req
@@ -5,6 +5,7 @@ import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
import RateLimiterMiddleware from '../Security/RateLimiterMiddleware.mjs'
import Settings from '@overleaf/settings'
import AsyncLocalStorage from '../../infrastructure/AsyncLocalStorage.mjs'
import { multerErrorHandler } from '../../infrastructure/Multer.mjs'
import { getRawReqInput } from '../../infrastructure/Validation.mjs'
const rateLimiters = {
@@ -25,7 +26,8 @@ export default {
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit(rateLimiters.projectUpload),
ProjectUploadController.multerMiddleware,
ProjectUploadController.uploadProject
ProjectUploadController.uploadProject,
multerErrorHandler
)
if (Settings.enablePandocConversions) {
@@ -34,7 +36,8 @@ export default {
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit(rateLimiters.projectUpload),
ProjectUploadController.multerMiddleware,
ProjectUploadController.importDocument
ProjectUploadController.importDocument,
multerErrorHandler
)
// Keep old route for backwards compatibility with old frontends that haven't reloaded
webRouter.post(
@@ -46,7 +49,8 @@ export default {
getRawReqInput(req).query.type = 'docx'
next()
},
ProjectUploadController.importDocument
ProjectUploadController.importDocument,
multerErrorHandler
)
}
@@ -64,7 +68,8 @@ export default {
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
ProjectUploadController.multerMiddleware,
ProjectUploadController.uploadFile
ProjectUploadController.uploadFile,
multerErrorHandler
)
} else {
webRouter.post(
@@ -74,7 +79,8 @@ export default {
AsyncLocalStorage.middleware,
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
ProjectUploadController.multerMiddleware,
ProjectUploadController.uploadFile
ProjectUploadController.uploadFile,
multerErrorHandler
)
}
},
@@ -0,0 +1,66 @@
import fs from 'node:fs'
import Path from 'node:path'
import crypto from 'node:crypto'
import multer from 'multer'
import lodash from 'lodash'
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
const defaultsDeep = lodash.defaultsDeep
export { multer }
/* multer upload handler with automatic cleanup to work around
* https://github.com/expressjs/multer/issues/259
*/
function filenameWithCleanup(uploadDir) {
return function (req, file, cb) {
const name = crypto.randomUUID()
// add an event handler to clean up files on aborted requests
req.once('close', () => {
if (!req.complete) {
const uploadPath = Path.join(uploadDir, name)
fs.unlink(uploadPath, err => {
if (err && err.code !== 'ENOENT') {
logger.warn(
{ err, uploadPath },
'error deleting uploaded file on multer cleanup'
)
}
})
}
})
cb(null, name)
}
}
export function multerUploadHandler(options = {}) {
const mergedOptions = defaultsDeep({}, options, Settings.multerOptions)
const { dest, ...multerConfig } = mergedOptions
// replace dest parameter with custom storage that cleans up leaked files
if (dest) {
multerConfig.storage = multer.diskStorage({
destination: dest,
filename: filenameWithCleanup(dest),
})
}
return multer(multerConfig)
}
/* remove the uploaded file when an error reaches the end of the route,
* so route handlers don't each have to unlink on every error path
*/
export function multerErrorHandler(err, req, res, next) {
if (req.file?.path) {
fs.unlink(req.file.path, unlinkErr => {
if (unlinkErr && unlinkErr.code !== 'ENOENT') {
logger.warn(
{ err: unlinkErr, uploadPath: req.file.path },
'error deleting uploaded file on multer error cleanup'
)
}
})
}
next(err)
}