5.9.2. File Validation
Table of Contents
Why File Validation Matters
Whenever a user uploads a file to your backend, you are trusting data that you did not create. This is always risky. A file might be:
- Too large and fill your disk.
- Of an unexpected type and break your application.
- Malicious, such as a script disguised as an image.
File validation is the process of checking that an uploaded file matches your expectations before you store it or process it.
Always validate uploaded files on the server side, even if you have client-side checks. Never trust the client.
File validation is not only about security. It is also about correctness and user experience. If your API expects a PDF, it should reject images instead of failing later in a background job.
What To Validate
When a file arrives at your backend, there are several things you typically want to validate. Common checks include:
| Check type | Examples | Why it matters |
|---|---|---|
| Size | Max 5 MB, min 1 KB | Avoid resource exhaustion, enforce limits |
| Type / MIME type | Only image/jpeg, application/pdf | Ensure correct processing and basic safety |
| Extension | .jpg, .png, .pdf | User feedback, simple filtering |
| Content / magic bytes | File really is an image, not a script | Prevent disguised files |
| Dimensions | Image width and height limits | Control layout and storage cost |
| Page count | PDF with max 10 pages | Business rules |
| Name / path | No path traversal, no strange characters | Prevent directory tricks |
| Count | Max N files per request | Protect from abuse |
You rarely need all of these, but you should consciously choose which ones your API needs.
File Size Validation
Validating file size is usually the first and simplest check.
You can apply limits in three places:
- At the reverse proxy or web server (for example Nginx, Traefik).
- At the application server or framework level.
- In your application code.
The best practice is to enforce limits as early as possible, then verify again in your application logic.
Request-level vs file-level limits
- Request size limit: Maximum total size of the HTTP request body. For example, at most 10 MB in a single request, no matter how many files.
- Per-file size limit: Limit for each individual file, for example each file must be at most 2 MB.
Both are useful:
- Request limit protects your server from very large uploads.
- File limit enforces business rules.
Example: Checking file size in code (Python-like pseudocode)
Imagine an API that accepts a single image file with a 2 MB limit.
MAX_FILE_SIZE = 2 * 1024 * 1024 # 2 MB
def validate_file_size(uploaded_file):
# Many frameworks expose file size directly
size = uploaded_file.size # or len(uploaded_file.read()) in a streaming manner
if size > MAX_FILE_SIZE:
raise ValueError("File too large (max 2 MB).")In a streaming environment you might read chunks and track the total. If the limit is exceeded, you stop reading and return an error.
Never read a huge file fully into memory without limits. Always enforce maximum sizes for uploads.
Validating File Type
Most web clients will send two hints about file type:
- MIME type (also called content type), for example
image/jpeg,application/pdf. - File extension, for example
.jpg,.pdf.
These are only hints from the client. Both can be forged easily.
Using MIME types
Common patterns:
- Check the MIME type matches a whitelist.
- Reject everything that is not in the allowed list.
Example whitelist:
Allowed MIME types:
- image/jpeg
- image/png
- application/pdfIn code you might see:
ALLOWED_MIME_TYPES = {
"image/jpeg",
"image/png",
"application/pdf",
}
def validate_mime_type(uploaded_file):
if uploaded_file.content_type not in ALLOWED_MIME_TYPES:
raise ValueError("Unsupported file type.")This is a first line of defense, not your only check.
Checking file extensions
File extensions are useful for:
- Giving quick feedback to users.
- Organizing storage by type.
However, never rely solely on extensions.
Example check:
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png"}
def validate_extension(filename):
import os
_, ext = os.path.splitext(filename.lower())
if ext not in ALLOWED_EXTENSIONS:
raise ValueError("Unsupported file extension.")Combine extension checks with MIME type and content checks to increase reliability.
Validating File Content (Magic Bytes)
The only way to be reasonably sure what a file really is, is to look at its content, especially the first bytes. These are often called magic bytes or file signatures.
For example:
| File type | Magic bytes (hex) |
|---|---|
| JPEG | FF D8 FF at the start |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
%PDF- (ASCII) | |
| ZIP | 50 4B 03 04 |
Many languages and libraries can guess the type from these bytes.
Example: Checking magic bytes in Python style
def is_jpeg(file_obj):
# Remember current position
pos = file_obj.tell()
header = file_obj.read(3)
# Restore position
file_obj.seek(pos)
return header == b"\xFF\xD8\xFF"This can be generalized or replaced by a library that reads headers and recognizes common formats.
Never trust the MIME type or extension without at least a minimal content check for critical file types, such as user avatars or document uploads.
Image-specific Validation (Dimensions, Format)
Images are a very common upload type. They have special validation needs.
Typical checks:
- Allowed formats, for example JPEG and PNG only.
- Maximum width and height in pixels.
- Minimum width and height.
- Maximum aspect ratio if required, for example no extremely tall images.
Example rules for profile pictures
You might require:
- Formats: JPEG, PNG.
- Size: at most 2 MB.
- Dimensions: between 256Γ256 and 2000Γ2000 pixels.
Example logic (pseudocode):
MAX_SIZE = 2 * 1024 * 1024
MIN_WIDTH, MIN_HEIGHT = 256, 256
MAX_WIDTH, MAX_HEIGHT = 2000, 2000
def validate_profile_image(file):
if file.size > MAX_SIZE:
raise ValueError("Image too large (max 2 MB).")
image = load_image(file) # Use an image library like Pillow
width, height = image.size
if width < MIN_WIDTH or height < MIN_HEIGHT:
raise ValueError("Image too small.")
if width > MAX_WIDTH or height > MAX_HEIGHT:
raise ValueError("Image too big.")Many backends also resize images after validation. Validation and transformation are related but should be considered separate steps.
Validating File Names and Paths
Even if you never store the original filename, you should treat it carefully.
Risks include:
- Path traversal, for example
../../etc/passwd. - System-sensitive characters, such as slashes, backslashes, colons.
- Very long filenames.
Never use a user-provided filename as a filesystem path directly.
Safe handling approach
- Ignore the directory part of the filename.
- Extract only the last component.
- Generate your own internal name, for example a UUID.
Example approach:
import os
import uuid
def sanitize_filename(original_name):
# Get only the base name (strip directories)
base_name = os.path.basename(original_name)
# Replace spaces and risky characters if you intend to keep it
safe_name = base_name.replace(" ", "_")
return safe_name
def generate_storage_name(original_name):
_, ext = os.path.splitext(original_name)
return f"{uuid.uuid4().hex}{ext.lower()}"
Use generate_storage_name to name files on disk or object storage, not the original user filename.
Never trust user-provided file paths or directory names. Always control where files are written.
Validating Number of Files
If your endpoint accepts multiple files, validate:
- How many files are allowed.
- Possibly, limits by type category, for example 1 identity document and up to 5 attachments.
Example rules:
- At most 10 files in a single request.
- Combined size at most 20 MB.
Pseudocode:
MAX_FILES = 10
MAX_TOTAL_SIZE = 20 * 1024 * 1024 # 20 MB
def validate_multiple_files(files):
if len(files) > MAX_FILES:
raise ValueError("Too many files.")
total_size = sum(f.size for f in files)
if total_size > MAX_TOTAL_SIZE:
raise ValueError("Total upload size too large.")This limit helps protect your backend and keeps your storage predictable.
Business Rule Validation
Besides technical checks, you often need business-specific rules. Some examples:
- Only one profile picture per user.
- Only PDF format for invoices.
- Maximum number of documents per account.
- File names must match a pattern, for example
invoice-YYYY-MM-DD.pdf.
Example pattern-based validation:
import re
INVOICE_PATTERN = re.compile(r"^invoice-\d{4}-\d{2}-\d{2}\.pdf$")
def validate_invoice_filename(filename):
if not INVOICE_PATTERN.match(filename):
raise ValueError("Filename must be like invoice-YYYY-MM-DD.pdf")These checks are specific to your project and belong in your application logic.
Error Handling and User Feedback
Good file validation is not only about rejecting bad files. It is also about returning clear, specific error messages.
Bad feedback:
"Invalid file."
Good feedback:
"File too large. Maximum allowed size is 5 MB.""Only PDF files are allowed.""Image too wide. Maximum width is 2000 pixels."
Typically you want to:
- Use appropriate HTTP status codes.
- Return structured error information.
For example, a JSON error response:
{
"error": "validation_error",
"field": "file",
"message": "File too large. Maximum allowed size is 5 MB.",
"limit": 5242880
}This makes it easy for the frontend to show meaningful messages to the user.
Security Considerations in File Validation
File validation is an important part of backend security, but it is not enough on its own. You should combine it with:
- Safe file storage strategy, such as storing files outside the application directory.
- Serving files through a separate domain or object storage.
- Avoiding execution of uploaded files as code.
- Virus scanning for sensitive use cases.
Some specific points related to validation:
- Never execute uploaded files. This includes scripts, binaries, or code in templates.
- Treat all content as untrusted. Even validated images can contain malicious payloads for image parsers.
- Sanitize any data extracted from files before using it elsewhere.
Validation reduces risk but does not eliminate it. Combine validation with safe storage, strict permissions, and defense in depth.
Designing a File Validation Policy
To make file validation consistent in your backend, define a clear policy per use case. For each type of upload, specify:
- Allowed MIME types and extensions.
- Maximum file size.
- Maximum and minimum dimensions or pages if relevant.
- Maximum number of files.
- Whether you will inspect magic bytes and how.
- Any business rules, such as naming conventions.
Put this policy into configuration so it is easy to change without editing core logic. For example, a configuration structure:
profile_image:
max_size: 2097152 # 2 MB
allowed_mime_types:
- image/jpeg
- image/png
min_width: 256
min_height: 256
max_width: 2000
max_height: 2000
invoice_pdf:
max_size: 5242880 # 5 MB
allowed_mime_types:
- application/pdf
max_pages: 20Then implement generic validators that read from this configuration.
By doing so, you keep file validation predictable, testable, and easy to maintain as your backend grows.
Views: 7
KAHIBARO