Secure File Uploads in NestJS with Application-Layer Scanning
Use PompelmiModule, PompelmiService, and memory-backed NestJS upload flows to block or quarantine risky files before storage.
Secure File Uploads in NestJS with Application-Layer Scanning
If you want malware scanning in NestJS, do it at the application layer where your controller, interceptor, and storage decision already live.
That keeps the upload decision inside your own request flow. It also gives you a clean place to reject clearly bad files, quarantine borderline ones, and keep risky bytes out of storage until the application has made a real decision.
Why NestJS is a good fit for this boundary
NestJS already encourages structure around modules, interceptors, and controller methods. File upload security fits that model well:
PompelmiModulecentralizes scanner optionsPompelmiInterceptorgives you automatic request-path scanningPompelmiServicelets you make custom route decisions when a simple block is not enough
That matters for document portals, internal review tools, and mixed enterprise workflows where suspicious should not always mean the same thing as malicious.
Register scanning once
Start by registering the scanner in your application module.
import { Module } from '@nestjs/common';import { PompelmiModule } from '@pompelmi/nestjs';import { CommonHeuristicsScanner, composeScanners, createZipBombGuard,} from 'pompelmi';
const scanner = composeScanners( [ ['zipGuard', createZipBombGuard({ maxEntries: 512, maxCompressionRatio: 12 })], ['heuristics', CommonHeuristicsScanner], ], { stopOn: 'suspicious', tagSourceName: true });
@Module({ imports: [PompelmiModule.forRoot({ scanner })],})export class AppModule {}Use a controller flow that keeps bytes in memory
For uploads, keep the file in memory first. That gives your NestJS controller or interceptor access to the bytes before disk or object storage.
import { Controller, Post, Res, UploadedFile, UseInterceptors,} from '@nestjs/common';import { FileInterceptor } from '@nestjs/platform-express';import { memoryStorage } from 'multer';import { PompelmiService } from '@pompelmi/nestjs';
@Controller('upload')export class UploadController { constructor(private readonly pompelmi: PompelmiService) {}
@Post() @UseInterceptors( FileInterceptor('file', { storage: memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 }, }) ) async upload(@UploadedFile() file: Express.Multer.File, @Res() res: any) { const report = await this.pompelmi.scan(file.buffer);
if (report.verdict === 'malicious') { return res.status(422).json({ ok: false, verdict: report.verdict, findings: report.findings }); }
if (report.verdict === 'suspicious') { return res.status(202).json({ ok: false, action: 'quarantine', findings: report.findings }); }
return res.json({ ok: true, verdict: 'clean', filename: file.originalname }); }}This pattern is useful when you need blocked upload responses that match product behavior instead of a one-size-fits-all middleware response.
Interceptor-first vs service-driven
Both patterns are valid in NestJS, but they solve different problems.
Use PompelmiInterceptor when:
- you want the framework to reject clearly bad uploads automatically
- the route behavior is uniform
- the controller should only run for acceptable files
Use PompelmiService when:
suspiciousshould go to review instead of hard-fail- you need route-specific business logic
- you want different responses for public uploads vs staff uploads
Real risks to account for
A NestJS upload controller still faces the same attack surface as any other Node.js upload path:
- MIME spoofing from client-provided metadata
- extension mismatch on renamed files
- risky archives and ZIP bombs
- suspicious document structures in PDFs or Office files
That is why simple FileInterceptor() usage is not enough on its own. It gives you parsing. It does not give you trust.
Conclusion
Secure file uploads in NestJS work best when scanning lives in the same application-layer flow as your controllers and storage rules. That gives you a clear place to reject, quarantine, or accept before the file becomes durable state.
The shortest production setup is the canonical NestJS guide. If your product needs review paths, combine it with the quarantine workflow docs instead of forcing every verdict into a hard block.