Featured image of post How to Handle File Uploads with Python and FastAPI

How to Handle File Uploads with Python and FastAPI

Handling file uploads securely is a common need for modern applications, but it comes with challenges. From managing large files to ensuring that the content isn't malicious, developers must implement robust solutions. In this guide, we'll walk through how to use Python's FastAPI to handle file uploads. Plus, we'll show you how to integrate Verisys Antivirus API to scan files for malware, protecting your application and users.

NOTE

To follow this tutorial, you will need the following:

Why FastAPI?

FastAPI is a modern web framework for Python that enables you to build APIs quickly. It’s built on standard Python type hints, making it easier to work with and maintain. With built-in support for asynchronous request handling, it’s also ideal for high-performance applications.

Combined with Verisys Antivirus API, FastAPI helps you build secure file upload endpoints, crucial for any web or mobile app handling potentially unsafe files.

Project Scaffold/Setup

The first step is to create a Python environment, ready with the libraries we will use for the FastAPI project.

  1. Open a terminal or command prompt, and navigate to the directory where you want to store the project

  2. Create and activate a virtual environment

  3. Install FastAPI, Uvicorn and python-multipart:

    1
    
    pip install fastapi uvicorn python-multipart
    
  4. For this tutorial, we’ll also use the requests library to send uploaded files to Verisys Antivirus API for malware scanning:

    1
    
    pip install requests
    

Creating the FastAPI App

Let’s start by setting up a simple FastAPI app that will accept file uploads. Create a file main.py with this content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from fastapi import FastAPI, File, UploadFile
import uvicorn
import logging

logger = logging.getLogger('uvicorn.error')
logger.setLevel(logging.DEBUG)

app = FastAPI()

@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
    content = await file.read()

    # Print information about the file to the console
    logger.debug(f"File Name: {file.filename}")
    logger.debug(f"File Size: {len(content)}")
    logger.debug(f"File MIME Type: {file.content_type}")
    
    # Return information about the file to the caller - note that the
    # content_type can easily be spoofed
    return {"filename": file.filename, "file_size": len(content), "file_mime_type": file.content_type}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

Starting the FastAPI App

To run the API using the Uvicorn web server, execute the following from the same folder as main.py:

1
uvicorn main:app --reload

The --reload flag allows the server to automatically reload as we make changes to the code, which is helpful during development.

Once Uvicorn starts, you’ll see output indicating that the app is running, along with the URL where it’s accessible. By default, it will run at:

1
http://127.0.0.1:8000

Testing the API

To test the basic file upload functionality, you can simply use curl, or you could a tool like Postman or Insomnia.

Here’s an example using curl, with a file testfile.png (substitute for any file you want to upload):

1
curl -F "[email protected]" http://127.0.0.1:8000/upload/

You should see a result similar to:

1
2
3
4
5
{
  "filename":"testfile.png",
  "file_size":26728,
  "file_mime_type":"image/png"
}

Integrating Verisys Antivirus API

Uploading files is a start, but ensuring that uploaded files are free of malware is critical, especially when accepting files from end users. Verisys Antivirus API allows us to scan files before processing them further.

NOTE

Verisys Antivirus API is a language-agnostic REST API that allows you to easily add malware scanning to mobile apps, web apps and backend processing.

By scanning user-generated content and file uploads, Verisys Antivirus API can stop dangerous malware at the edge, before it reaches your servers, applications - or end users.

Here’s how to integrate Verisys Antivirus API to scan uploaded files:

  1. Get your Verisys API Key: Before we start, ensure you have your Verisys Antivirus API key. If you don’t have one, visit Verisys Antivirus API to get started or request a trial.

  2. Send the file to Verisys Antivirus API: Once the file has been received, send it to the Verisys API for scanning.

Update main.py with this content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from fastapi import FastAPI, File, UploadFile
import uvicorn
import requests
import logging

VERISYS_API_URL = "https://eu1.api.av.ionxsolutions.com/v1/malware/scan/file"
API_KEY = "your_api_key"

logger = logging.getLogger('uvicorn.error')
logger.setLevel(logging.DEBUG)

app = FastAPI()

class ScanResult:
    def __init__(self, filename="", status="", content_type="", signals=None, metadata=None):
        self.filename = filename
        self.status = status
        self.content_type = content_type
        self.signals = signals if signals is not None else []
        self.metadata = metadata if metadata is not None else []

    def __repr__(self):
        return f"ScanResult(filename={self.filename},status={self.status}, content_type={self.content_type}, signals={self.signals}, metadata={self.metadata})"

def scan_file(file_content, filename):
    files = {'file': (filename, file_content)}
    headers = {'X-API-Key': API_KEY, 'Accept': '*/*'}
    
    response = requests.post(VERISYS_API_URL, headers=headers, files=files)

    # Was the scan successful?    
    if response.status_code == 201:
        result = response.json()

        # Print the full scan result to the console
        logger.debug(f"Scan result: {result}")

        scan_result = ScanResult(
            filename=filename,
            status=result["status"],
            content_type=result["content_type"],
            signals=result["signals"],
            metadata=result["metadata"]
        )

        return scan_result
    else:
        return ScanResult(status="error", metadata=[{"message": "Failed to scan file"}])

@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
    content = await file.read()

    # Print information about the file to the console
    logger.debug(f"File Name: {file.filename}")
    logger.debug(f"File Size: {len(content)}")
    logger.debug(f"File MIME Type: {file.content_type}")
    
    # Scan file with Verisys API
    scan_result = scan_file(content, file.filename)

    # In real-life, you'd now use the scan result to determine what to do 
    # next - but here, we'll just return the scan results to the caller
    
    return scan_result

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

In this modified version, after the file is uploaded, it’s passed to the scan_file function, which sends the file to Verisys Antivirus API for scanning. The response from Verisys is returned as part of the result, indicating whether the file is safe or malicious.

Testing the Completed API

To test the file upload and scanning functionality, as before you can use curl or another tool of your preference.

Here’s an example using curl, with an EICAR Anti-Virus Test File eicar.png (substitute for any file you want to upload):

1
curl -F "[email protected]" http://127.0.0.1:8000/upload/

Depending on the file you upload, you should see the malware scan result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "filename": "eicar.png",
  "status": "threat",
  "content_type": "text/plain",
  "signals": [
    "Virus:EICAR_Test_File"
  ],
  "metadata": {
    "hash_sha1": "3395856ce81f2b7382dee72602f798b642f14140",
    "hash_sha256": "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"
  }
}

Your upload endpoint could use the scan result to determine what to do next - for example, to prevent uploading files that contain malware.

Note that while in this example the file masqueraded as a PNG image file, Verisys Antivirus API detected that it was actually a plain text file!

Handling File Uploads Securely

Here are a couple of additional tips to ensure your file upload system is secure:

  • Limit File Size: Ensure you’re not accepting excessively large files, which could cause performance issues or even DoS attacks. FastAPI allows you to define custom file size limits.

  • Restrict File Types: Only accept specific file types (e.g., PDFs, images) to prevent the upload of executable or malicious files.

NOTE

Checking the file extension and Content-Type header are basic steps towards securing file uploads. However, both of these can easily be spoofed.

By scanning the actual file content, Verisys Antivirus API can identify 50+ different file formats for you, while also scanning files for malware.

Why Choose Verisys Antivirus API?

Verisys Antivirus API is specifically designed to ensure secure file uploads in your applications. Here’s why it’s a great fit for your project:

  • Powerful Malware Detection: Verisys detects a wide range of threats, ensuring that no malicious files slip through.
  • Easy Integration: The API is simple to integrate with any web framework, including FastAPI.
  • Content Type Detection: Alongside malware scanning, the API also scans the file content to determine the real file type.

Conclusion

Handling file uploads is essential for many applications, but steps must be taken to ensure security. By using FastAPI and Verisys Antivirus API, you can create a secure file upload endpoint that scans files for malware before processing them further. Whether you’re building a web app or an API for file storage, this approach ensures that your infrastructure - and your users - remain safe from harmful files.

If you haven’t yet, sign up for Verisys Antivirus API today and start protecting your application against malware!