File uploads cross a security boundary. A file supplied by a user may have a misleading name, unexpected content, excessive size, malicious active content, or a structure designed to exploit the software that processes it.
Secure upload handling therefore requires more than checking a filename extension. Treat every uploaded file as untrusted until the application has validated, stored, processed, and served it according to an explicit policy.
Start with a narrow upload policy
Define what the feature actually needs to accept. An avatar service may need only a small set of image formats, while a document workflow may need PDF files and nothing else.
Prefer an allowlist of required formats over a blocklist of dangerous extensions. Blocklists are difficult to maintain because new interpreters, file types, and parsing behaviours can create unexpected execution paths.
The policy should define at least:
- permitted file formats;
- maximum file size;
- maximum number of files per request or account;
- whether archives are allowed;
- whether active content such as scripts or macros is acceptable;
- how long uploaded files are retained.
A smaller accepted surface is easier to validate and monitor.
Do not trust the filename
The original filename is user-controlled metadata. It can contain misleading extensions, unusual Unicode characters, path separators, control characters, or names that collide with existing files.
Do not use it directly as a storage path.
Generate a new storage identifier on the server, for example a cryptographically random value or another collision-resistant identifier. If the original filename is useful to users, store a sanitised version separately as display metadata.
A simple model is:
storage_id = secure_random_identifier()
display_name = sanitise(original_filename)
store(storage_id, file_bytes)
record(storage_id, display_name, owner_id, detected_type)The storage identifier and the display name serve different purposes and should not be confused.
Validate more than the extension
A filename ending in .jpg does not prove that the content is a JPEG image. Likewise, the Content-Type header in a multipart request is supplied by the client and cannot be treated as authoritative.
Use multiple signals appropriate to the file type:
- check the extension against the allowlist;
- inspect the file signature or structure using trusted server-side logic;
- parse the file with a maintained library when deeper validation is required;
- reject content that does not match the expected format.
No single check makes arbitrary files safe. The goal is to verify that the content conforms to the narrow format the application intends to process.
Be careful with formats that can legitimately contain active content. For example, some document and vector formats can include scripts, external references, macros, or embedded objects. If the application does not need those capabilities, reject the format or remove unsupported features with a well-tested sanitisation process.
Enforce size and resource limits early
Large uploads can exhaust memory, disk space, network capacity, parser resources, or downstream processing queues.
Enforce limits as early as practical. A complete design may include limits at the reverse proxy, application server, request handler, storage layer, and asynchronous processing system.
Avoid reading an unbounded upload entirely into memory. Stream data when the platform supports it and stop processing once the configured limit is exceeded.
Compressed archives need special attention. A small archive can expand into a very large amount of data or contain a huge number of entries. If archives are necessary, limit compressed size, expanded size, entry count, nesting depth, and processing time.
Store uploads away from executable application paths
Uploaded files should not become executable merely because they were written to disk.
Keep untrusted uploads outside directories where the web server, application runtime, template engine, or operating system may interpret them as code. Storage permissions should allow only the operations the application requires.
Object storage can reduce some filesystem-specific risks, but it does not make unsafe content harmless. Access policy, metadata, content delivery, and downstream processing still need protection.
If files are private, do not rely on unpredictable URLs as the access-control mechanism. Authorise each retrieval according to the application’s ownership and permission rules.
Serve files with controlled response metadata
How a file is delivered can change its security properties.
Set a deliberate response Content-Type based on trusted server-side classification rather than blindly reflecting user-provided metadata. Where appropriate, use Content-Disposition: attachment so browsers download a file instead of rendering it in the application’s origin.
For content that must render in a browser, understand the capabilities of the accepted format and isolate risky content where practical. Serving attacker-controlled active content from the same origin as authenticated application pages can expose cookies, tokens, or trusted user interactions if browser security boundaries are not designed carefully.
Separate upload from expensive processing
Image resizing, document conversion, metadata extraction, archive inspection, and media transcoding all increase the attack surface because complex parsers process attacker-controlled bytes.
A safer architecture separates the initial upload from downstream processing:
client
-> upload endpoint
-> quarantine or restricted storage
-> validation and scanning
-> controlled processing
-> approved storage
-> authorised deliveryRun processors with minimal privileges. They should not need broad filesystem access, production credentials, or unrestricted network access merely to transform a file.
Apply CPU, memory, file-size, and execution-time limits so malformed input cannot consume resources indefinitely.
Treat malware scanning as one layer
Malware scanning can be valuable when users exchange files or when uploads later reach employee devices, but it should not replace format validation and isolation.
A scanner can miss new or obfuscated threats, and a clean scan does not prove that a parser cannot be exploited by malformed input.
Use scanning as defence in depth alongside narrow file policies, maintained parsing libraries, safe storage, least privilege, and controlled delivery.
If scanning is asynchronous, keep the file unavailable to normal consumers until the required checks have completed successfully.
Protect upload endpoints from abuse
Upload functionality can also be abused without exploiting a file parser. Attackers may use it to consume storage, distribute unwanted content, probe internal workflows, or generate expensive processing jobs.
Apply authentication where the feature requires an account, enforce authorisation for the target resource, and rate-limit operations according to realistic usage patterns.
Quotas can limit storage consumption per user, tenant, or other security boundary. Monitoring should make unusual upload volume, repeated validation failures, and processing errors visible to operators.
Do not put sensitive file contents, authentication tokens, or raw private documents into routine logs. Log useful metadata such as request identifiers, account identifiers, size, detected type, validation result, and processing outcome according to the system’s privacy requirements.
Handle temporary files deliberately
Temporary files are part of the upload lifecycle and need the same care as permanent storage.
Use platform facilities that create temporary files safely. Avoid predictable names and unsafe shared-directory patterns. Ensure temporary content is removed after success, rejection, timeout, or processing failure.
Permissions should prevent unrelated processes or users from reading or replacing temporary uploads.
Keep processing libraries patched
File parsers are complex and regularly receive security fixes. Image codecs, archive libraries, document processors, media tools, and antivirus engines all become part of the application’s attack surface when they inspect untrusted files.
Track these dependencies and patch security issues promptly. Remove unused parsers and converters so the application does not expose processing capabilities it no longer needs.
When a high-risk parser vulnerability is disclosed, temporarily disabling the affected upload format may be safer than accepting files while waiting for a production patch.
Test rejection paths as carefully as successful uploads
Security tests should cover more than a valid file.
Useful cases include:
- allowed extension with invalid content;
- disallowed extension with otherwise valid content;
- conflicting client-declared and detected media types;
- oversized files;
- duplicate filenames;
- filenames containing path separators or unusual characters;
- truncated or malformed files;
- archives that exceed expansion limits;
- unauthorised access to another user’s uploaded file;
- failures during scanning or transformation.
The system should reject unsafe input predictably without leaving partial files, exposing internal paths, or bypassing later validation stages.
Build security around the complete lifecycle
A robust upload design treats the file as untrusted throughout its lifecycle:
restrict accepted formats
-> enforce resource limits
-> generate server-side identity
-> validate content
-> isolate storage
-> scan or transform when required
-> authorise access
-> serve with controlled metadata
-> delete according to retention policyThe key principle is simple: uploading bytes should never implicitly grant those bytes permission to execute, consume unlimited resources, bypass access control, or inherit the trust of the application that received them.