Header Image How to Scan File Uploads for Malware on Vercel

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 }),
  }
);

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.

API Route stage to S3 async + callback AttachmentScanner multi-engine scan callback ok → move to uploads found → delete from S3

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

  1. Sign up and grab your API token
  2. Add ATTACHMENT_SCANNER_URL and ATTACHMENT_SCANNER_API_TOKEN to your Vercel project environment variables
  3. Test with EICAR to confirm scanning works
  4. Set up async scanning with callbacks for production

If you need help with your integration, get in touch — we're always happy to help.

2026-04-18
Profile Image: AttachmentScanner Team AttachmentScanner Team

Other Articles