Secure file uploads require more than checking a filename extension. This practical checklist helps you design, audit, and maintain an upload flow with server-side validation, authentication, malware scanning, isolated storage, rate limits, logging, retention controls, and repeatable tests. Use it when onboarding uploads for a new application, preparing for a penetration test, or reviewing an existing implementation after a framework, dependency, or policy change.
Overview
An upload endpoint accepts data supplied by someone else and places that data inside your application’s trust boundary. The risk is not limited to obviously executable files. A malicious or malformed upload may exploit a parser, consume excessive storage or processing capacity, expose sensitive metadata, or be served back to users in an unsafe context.
A defensible design separates the upload process into distinct controls:
- Access control: decide who may upload, where the file belongs, and which users may retrieve it.
- Validation: inspect the actual content and enforce size, type, name, and structural rules on the server.
- Inspection: scan files before making them available to other users or downstream systems.
- Storage isolation: keep untrusted files outside the application’s executable and public document paths.
- Operational controls: apply rate limits, quotas, logging, alerting, retention, and recovery procedures.
Do not treat a browser’s accept attribute, a JavaScript check, or a Content-Type header as a security boundary. These controls improve usability, but a client can alter every one of them. The server must make the final decision.
For the surrounding form and API design, see File Uploads in HTML, JavaScript, and APIs. For a broader validation and scanning overview, see Secure File Uploads: Validation, Malware Scanning, and Storage Best Practices.
What to track
1. Authentication, authorization, and ownership
- Require authentication unless anonymous uploads are an explicit, risk-assessed feature.
- Authorize the upload destination separately from the user’s general account access.
- Associate every upload with an owner, tenant, purpose, and lifecycle state such as
pending,clean,rejected, ordeleted. - Check authorization again when a file is downloaded, previewed, replaced, or deleted.
- Prevent object-reference attacks by using server-side ownership checks rather than trusting an ID supplied by the client.
2. Server-side validation
Validate more than the extension. A useful validation record includes:
- Maximum request and file size, with separate limits for each supported category.
- Allowed media types based on detected content, not only the submitted MIME type.
- Extension rules that match the application’s actual needs.
- Filename length and character restrictions. Generate a storage key rather than using the original name as a path.
- Image dimensions, archive contents, page counts, or other format-specific limits where relevant.
- Protection against archive expansion, nested archives, parser bombs, and excessive processing time.
Reject ambiguous formats unless there is a clear business reason to support them. If a file must be transformed, perform that work in a restricted worker or sandbox and preserve the original as an untrusted object.
3. A small server-side validation pattern
The following Node and Express-style example illustrates the order of operations. The exact APIs depend on your framework and file-handling library, so treat it as an implementation template rather than a drop-in security guarantee.
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'application/pdf']);
const MAX_BYTES = 10 * 1024 * 1024;
async function validateUpload(file) {
if (!file || file.size < 1 || file.size > MAX_BYTES) {
throw new Error('Invalid file size');
}
const detectedType = await detectTypeFromBytes(file.path);
if (!detectedType || !ALLOWED_TYPES.has(detectedType.mime)) {
throw new Error('Unsupported file type');
}
const storageKey = `quarantine/${crypto.randomUUID()}`;
await moveToQuarantine(file.path, storageKey);
return { storageKey, detectedType: detectedType.mime };
}
In Django, apply upload limits at the reverse proxy and application layers, validate with a form or serializer, and generate a storage name rather than interpolating request.FILES into a path. In Rails, use Active Storage or an equivalent abstraction with content-type validation, size limits, authorization checks, and a private service for unscanned objects. In all three frameworks, verify the resolved storage path cannot escape its intended directory.
4. Quarantine, scanning, and sandboxing
Do not publish a file immediately after the upload request succeeds. Store it in quarantine, scan it, and change its state only after the inspection workflow completes. A download endpoint should refuse files that are pending, rejected, or missing a scan result.
| Option | Best fit | Advantages | Questions to verify |
|---|---|---|---|
| Local antivirus daemon | Controlled environments with predictable throughput | Can keep file content within your infrastructure and support a simple queue | How are signatures updated? What happens when the scanner is unavailable? Is isolation adequate? |
| Managed malware-scanning API | Teams that prefer an external service and a defined integration | Less scanner administration and a clear request/result workflow | Data residency, retention, file-size limits, timeout behavior, credentials, and failure handling |
| Detonation sandbox | High-risk files or formats requiring behavioral analysis | Can inspect behavior beyond basic signature matching | Isolation strength, analysis duration, supported formats, privacy controls, and operational cost |
| Layered approach | Systems with higher assurance requirements | Combines type checks, signatures, and deeper analysis | Duplicate processing, queue design, false positives, and a documented fail-closed policy |
Record the scanner result, scanner version or service response identifier, timestamp, and reason for rejection. Decide in advance whether scanner errors block publication. For sensitive applications, failing closed is generally easier to explain and audit than silently publishing an unchecked file.
5. Storage and delivery policy
- Use a private bucket or volume for originals and quarantine objects.
- Disable public listing and avoid predictable object keys.
- Serve approved files through an authorization-checked endpoint or a short-lived signed URL.
- Set a safe download disposition and a content type derived from trusted server-side metadata.
- Keep application credentials scoped to the required bucket, prefix, and actions.
- Separate upload, scan, transform, and delivery permissions where practical.
- Define encryption, backup, deletion, and regional storage requirements with the system owner.
A generic object-storage policy should allow the upload worker to write only to quarantine, allow the scanner to read quarantine and write scan results, and allow the delivery service to read only approved objects. Avoid giving the web application broad administrative access to the entire storage account.
6. Rate limits, logging, and retention
Apply limits by account, IP address, tenant, route, and time window as appropriate. Track request count, bytes uploaded, rejected validations, scan failures, processing duration, and storage consumption. Log identifiers and outcomes rather than sensitive file contents. Make logs resistant to filename injection and ensure access to them is restricted.
Document how long files, quarantine objects, scan records, access logs, and backups are retained. Retention should reflect the application’s purpose and applicable contractual or regulatory obligations; do not assume that a default storage lifecycle is suitable. Provide a deletion process that covers primary storage, derived previews, caches, and backups where applicable.
Cadence and checkpoints
Use a monthly or quarterly review cadence, with additional reviews after material changes. A short recurring checklist is more useful than a one-time security document that no longer matches production.
| Cadence | Checkpoint | Evidence to retain |
|---|---|---|
| Each deployment | Run automated tests for size, type, authorization, path traversal, rejected states, and signed delivery. | CI results and test version |
| Monthly | Review rejected uploads, scan failures, unusual volume, queue delays, and storage growth. | Operational review notes and selected metrics |
| Quarterly | Recheck allowed MIME types, framework configuration, storage permissions, credentials, retention jobs, and incident contacts. | Configuration diff and owner sign-off |
| After a dependency or framework release | Read security advisories, retest parsers and upload middleware, and review compatibility of scanning integrations. | Upgrade record and regression results |
| After a relevant CVE or regulation change | Assess affected components, data flows, retention, access, and notification procedures with the appropriate security or legal owner. | Risk decision and remediation ticket |
Your CI checklist should attempt oversized files, misleading extensions, mismatched MIME declarations, malformed images or documents, nested archives, duplicate names, unauthorized downloads, expired signed URLs, scanner timeouts, and storage-service failures. Also test that a rejected or pending object cannot be reached through a cached URL or alternate endpoint.
How to interpret changes
A rising rejection count does not automatically mean the application is under attack. It may indicate a new client feature, a changed allowed-type list, a stricter parser, or a broken integration. Compare changes with deployments, product announcements, traffic sources, and scanner availability.
Prioritize findings using four questions:
- Can an untrusted file reach a user or parser? A bypass of quarantine or authorization deserves immediate attention.
- Can the issue affect many tenants or accounts? Cross-tenant access and shared storage permissions increase impact.
- Can the issue consume disproportionate resources? Review oversized uploads, decompression, image processing, queues, and storage costs.
- Can you prove what happened? Missing scan records, ownership data, or deletion logs make investigation harder even when the control appears to work.
When a scanner is unavailable, distinguish between an operational outage and a clean result. “Not scanned” is not equivalent to “clean.” Keep the file quarantined, alert the responsible team, and retry according to a bounded policy. When a format must be supported despite a known limitation, document the compensating controls and obtain an explicit risk decision.
Use the upload QA reference at How to Test File Upload Forms to expand functional and negative test coverage. For large or interrupted transfers, review Resumable File Uploads without relaxing the same validation, authorization, and scanning requirements.
When to revisit
Revisit this checklist at least quarterly and whenever a recurring data point changes materially. Schedule an immediate review after a new CVE affects your web framework, upload middleware, image or document parser, archive library, storage SDK, or scanning component. Also review the implementation after a major framework release, a new file type, a change to allowed size limits, a provider policy change, a storage migration, or a penetration-test finding.
Legal and retention guidance should be reassessed when the application begins serving a new region, collecting a new category of personal data, supporting a regulated workflow, or responding to updated GDPR, HIPAA, contractual, or sector-specific requirements. The technical team should not make those determinations alone; involve the relevant security, privacy, compliance, or legal owner.
For a practical next step, create one issue containing the following actions:
- Map the current upload path from browser or API to storage and delivery.
- List every accepted file type, size limit, parser, transformation, and downstream consumer.
- Confirm that validation, quarantine, scanning, authorization, logging, rate limiting, and deletion are implemented and tested.
- Record scanner credentials, storage permissions, framework versions, and responsible owners without placing secrets in the checklist.
- Set the next monthly operational review and quarterly security review on the team calendar.
Keep this document beside the implementation and update it when the system changes. That habit turns a secure file upload checklist from a launch-time formality into a maintained control.