How to Scan File Uploads for Malware on Vercel
If your Vercel app accepts file uploads, those files aren't scanned for malware. Vercel handles deployment and edge delivery — but it won't check whether an uploaded file is safe before your app stores it.
Since Vercel is serverless, uploaded files typically pass through an API route or serverless function on their way to cloud storage (S3, R2, Cloudflare, etc.). That function is the right place to scan.
AttachmentScanner adds malware scanning with a single API call. Files are checked against multiple antivirus engines and you get back a clear result: clean, malicious, or suspicious.
This guide covers adding scanning to Vercel functions. For the full picture on scanning strategies, see the complete guide to scanning user uploads.
Set Your Environment Variables
Sign up for an account to get your API token and scanner URL. Add them in your Vercel project settings, or via the CLI:
vercel env add ATTACHMENT_SCANNER_URL
vercel env add ATTACHMENT_SCANNER_API_TOKEN
Your First Scan
Test the connection with the EICAR test file — a standardised test file that every antivirus engine detects:
curl -H "Authorization: Bearer $ATTACHMENT_SCANNER_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.attachmentscanner.com/eicar.com"}' \
-XPOST $ATTACHMENT_SCANNER_URL/v1.0/scans
{
"status": "found",
"filename": "eicar.com",
"matches": ["Eicar-Test-Signature"]
}
That's it — scanning is working. Now let's integrate it into your app.
Scanning in a Next.js API Route
The most common pattern on Vercel: scan the file in your API route before uploading it to storage.
// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get("file") as File;
// Scan the file before storing it
const scanForm = new FormData();
scanForm.append("file", file);
const scanResponse = await fetch(
`${process.env.ATTACHMENT_SCANNER_URL}/v1.0/scans`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ATTACHMENT_SCANNER_API_TOKEN}`,
},
body: scanForm,
}
);
const result = await scanResponse.json();
if (result.status === "found") {
return NextResponse.json(
{ error: "Malicious file detected" },
{ status: 422 }
);
}
// File is clean — upload to your storage
await uploadToStorage(file);
return NextResponse.json({ status: "uploaded" });
}
Scanning by URL
If you upload to storage first (e.g. via a presigned URL from the client), scan by passing the storage URL instead:
const response = await fetch(
`${process.env.ATTACHMENT_SCANNER_URL}/v1.0/scans`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ATTACHMENT_SCANNER_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ url: storageUrl }),
}
);
Going Async: The Recommended Approach
The synchronous examples above work well for smaller files, but for production we recommend async scanning with callbacks. This is especially useful on Vercel where function execution time is limited.
The pattern for serverless: upload the file to a staging location in your storage, kick off an async scan, and handle the result in a callback route.
// app/api/upload/route.ts — kick off async scan
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get("file") as File;
// Stage the file
const stagedUrl = await uploadToStaging(file);
// Scan asynchronously
await fetch(`${process.env.ATTACHMENT_SCANNER_URL}/v1.0/scans`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ATTACHMENT_SCANNER_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: stagedUrl,
async: true,
callback: "https://your-app.vercel.app/api/webhooks/scan-complete",
}),
});
return NextResponse.json({ status: "processing" }, { status: 202 });
}
// app/api/webhooks/scan-complete/route.ts — handle result
export async function POST(req: NextRequest) {
const result = await req.json();
if (result.status === "ok") {
await moveToUploads(result.url);
} else if (result.status === "found") {
await deleteFromStaging(result.url);
}
return NextResponse.json({ received: true });
}
The uploads guide covers the full async pattern
including staging areas and the warning status.
Works Everywhere
AttachmentScanner is cloud-agnostic — the same API works whether you're on Vercel, Heroku, Railway, Fly.io, or your own infrastructure. If you move between platforms, the integration stays the same.
Getting Started
- Sign up and grab your API token
- Add
ATTACHMENT_SCANNER_URLandATTACHMENT_SCANNER_API_TOKENto your Vercel project environment variables - Test with EICAR to confirm scanning works
- Set up async scanning with callbacks for production
If you need help with your integration, get in touch — we're always happy to help.
AttachmentScanner Team
Other Articles
Scan File Uploads on Fly.io
AttachmentScanner Team
Scan File Uploads on Railway
AttachmentScanner Team
Scan File Uploads on Heroku
AttachmentScanner Team