Overview
CVE-2024-29409 describes a file upload vulnerability in NestJS v10.3.2 where a remote attacker can trigger arbitrary code execution by manipulating the Content-Type header during file upload. In production environments, a crafted request with a malicious Content-Type header can lead the server to execute attacker-supplied payloads. This undermines the trust boundary between user input and server execution, enabling remote code execution, data exposure, and possible compromise of the host. The vulnerability stems from the server-side logic inadvertently acting on user-provided header data to determine how to process or execute uploaded content. If an application loads, evaluates, or runs code derived from an uploaded file based on Content-Type, an attacker can bypass normal protections and run arbitrary code within the server context. This kind of flaw is classed under Broken Authentication and leads to severe impact when exploited against file upload endpoints. In NestJS, the recommended pattern is to never trust headers for security decisions and to decouple upload handling from any runtime evaluation of user-supplied content.
Affected Versions
NestJS v10.3.2
Code Fix Example
NestJS API Security Remediation
/* VULNERABLE PATTERN */
import { Controller, Post, UploadedFile, Req, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import { Request } from 'express';
@Controller()
export class DemoUploads {
@Post('upload-vuln')
@UseInterceptors(FileInterceptor('file', { storage: memoryStorage() }))
async uploadVuln(@UploadedFile() file: any, @Req() req: Request) {
// Vulnerable: trusts Content-Type header to decide on executing uploaded content
const ct = (req.headers['content-type'] || '').toString();
if (ct.startsWith('application/javascript') && file?.buffer) {
const code = file.buffer.toString('utf8');
// Dangerous: executes user-supplied code
// eslint-disable-next-line no-eval
eval(code);
}
return { name: file?.originalname, size: file?.size };
}
}
/* FIXED PATTERN */
@Controller()
export class DemoUploadsFixed {
@Post('upload-secure')
@UseInterceptors(FileInterceptor('file', {
storage: memoryStorage(),
fileFilter: (req, file, cb) => {
// Do not rely on Content-Type header; validate by extension instead
const ext = (file.originalname || '').toLowerCase().split('.').pop();
if (ext === 'js' || ext === 'ts' || ext === 'json') {
cb(null, true);
} else {
cb(null, false);
}
},
limits: { fileSize: 5 * 1024 * 1024 }
}))
async uploadSafe(@UploadedFile() file: any) {
// Do not execute uploaded content; treat as data payload only
return { name: file?.originalname, size: file?.size, mimetype: file?.mimetype };
}
}