Reject at the outer boundary
Limit request size at the proxy, web server, PHP runtime, and application. Each layer fails differently, so the application must recognize when PHP discarded an oversized body. Use the upload error code and verify the temporary file came from the HTTP upload mechanism.
Original filenames are display metadata, not storage paths. Generate a new identifier and store the user-visible name separately after removing control characters.
Inspect content rather than extensions
Determine media type from file content and allow only the types the workflow requires. For images, decode and re-encode with a maintained library when practical. Archives and documents need dedicated inspection because they can contain active content or expand far beyond their compressed size.
Store untrusted uploads outside the executable document root. If public delivery is required, use a separate asset host or an endpoint that sets an explicit content type and attachment policy.
- Allowlist content types and extensions together.
- Set byte, pixel, page, and expansion limits.
- Scan when the risk and workflow justify it.
- Never pass the original filename to a shell command.
Make access a separate decision
Successful upload does not mean public access. Record an ownership and visibility policy, and check it when generating a download. Time-limited signed URLs are useful when a storage service serves private objects, but the application still decides who may receive the URL.
Delete abandoned temporary objects and keep an audit event for rejection reasons without storing the harmful payload longer than necessary.
Upload a valid file, a renamed executable, an oversized body, an image with misleading metadata, and a filename containing path separators; verify each outcome and storage location.