November 2, 2026
How to Scan FastAPI File Uploads for Malware in Python
You picked FastAPI for your project, and your endpoint came together in minutes. Fantastic. You declare an UploadFile, and boom — the file…

By Cloudmersive
2 min read
You picked FastAPI for your project, and your endpoint came together in minutes. Fantastic. You declare an UploadFile, and boom — the file is just there.
But a file extension is still only a label, and an upload content-type header is still little more than a claim. So there's really nothing in place to tell you whether a file is clean or packed with malware.
What you need here is a real virus & malware scan in the request path — something that investigates the file thoroughly before it's saved or passed anywhere. To handle that, you can simply add a security API in the chain and call it from our FastAPI endpoint. In this article, we'll use the Cloudmersive Advanced Virus Scan API.
1. Installing what we need
Let's first get all our resources in order:
pip install fastapi uvicorn python-multipart cloudmersive-virus-api-clientpip install fastapi uvicorn python-multipart cloudmersive-virus-api-client2. Pointing the client at your API key
Next, let's configure our API key (this can be obtained for free by creating a free account on the Cloudmersive website):
import os
import cloudmersive_virus_api_client
configuration = cloudmersive_virus_api_client.Configuration()
configuration.api_key["Apikey"] = os.environ["CLOUDMERSIVE_API_KEY"]
scan_api = cloudmersive_virus_api_client.ScanApi(
cloudmersive_virus_api_client.ApiClient(configuration)
)import os
import cloudmersive_virus_api_client
configuration = cloudmersive_virus_api_client.Configuration()
configuration.api_key["Apikey"] = os.environ["CLOUDMERSIVE_API_KEY"]
scan_api = cloudmersive_virus_api_client.ScanApi(
cloudmersive_virus_api_client.ApiClient(configuration)
)Inspecting an upload
A file arrives as bytes in memory, and the Cloudmersive advanced scan works from a file path. We'll bridge these two ideas inside a single helper by writing the bytes to a temporary file, scanning it, and let the temp file clean itself up on the way out.
import tempfile
def inspect_upload(data: bytes):
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(data)
tmp.flush()
return scan_api.scan_file_advanced(
tmp.name,
allow_executables=False,
allow_scripts=False,
allow_macros=False,
allow_invalid_files=False,
allow_password_protected_files=False,
allow_unsafe_archives=False,
)import tempfile
def inspect_upload(data: bytes):
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(data)
tmp.flush()
return scan_api.scan_file_advanced(
tmp.name,
allow_executables=False,
allow_scripts=False,
allow_macros=False,
allow_invalid_files=False,
allow_password_protected_files=False,
allow_unsafe_archives=False,
)The advanced scan API does more than just match files against known virus and malware signatures. Each flag in the above code lets us reject a whole category of risky content, like executables, scripts, or macros, even when the file isn't a known threat.
Setting one flag to False blocks that category. Any flag we leave out blocks by default, so the six above are just the decisions we're arbitrarily making explicit in this example.
The endpoint
Reading the upload is asynchronous, but the scan call is blocking, so we'll hand it to a threadpool rather than stalling the event loop. A failed scan then ends the request with a 422:
from fastapi import FastAPI, UploadFile, HTTPException
from starlette.concurrency import run_in_threadpool
app = FastAPI()
@app.post("/files")
async def receive_file(upload: UploadFile):
data = await upload.read()
result = await run_in_threadpool(inspect_upload, data)
if result.clean_result is not True:
raise HTTPException(
status_code=422,
detail={
"rejected": upload.filename,
"threats": [v.virus_name for v in (result.found_viruses or [])],
},
)
return {"accepted": upload.filename}from fastapi import FastAPI, UploadFile, HTTPException
from starlette.concurrency import run_in_threadpool
app = FastAPI()
@app.post("/files")
async def receive_file(upload: UploadFile):
data = await upload.read()
result = await run_in_threadpool(inspect_upload, data)
if result.clean_result is not True:
raise HTTPException(
status_code=422,
detail={
"rejected": upload.filename,
"threats": [v.virus_name for v in (result.found_viruses or [])],
},
)
return {"accepted": upload.filename}The way we've set things up, clean files pass through to whatever comes next, while infected ones bounce back with threat names attached, having never touched your file storage destination.
Wrapping up
Keeping this check in our app's request path means the rest of our app gets to treat any accepted file as though it's already safe. If we want to adjust our threat rules or add different handling for rejected files, we only have to make changes to one helper, not to the endpoint around it.