Secure File Uploads in PHP Apps and WordPress: A Practical Guide
Upload forms are a common way into PHP websites. Five habits keep them safe: allow-listed types, safe storage, no script execution, size limits and scanning.
An upload form is one of the easiest ways into a PHP website when it is built carelessly, because an attacker who can upload a .php file and then open it in a browser can run their own code on your server. Secure uploads rest on five habits: accept only the file types you need and check the actual content, rename files and store them outside the web root (or at least where scripts cannot run), block script execution in any upload folder, set size limits on the server, and scan what comes in. This guide covers each one for custom PHP apps and for WordPress, in terms an office manager can use when talking to a developer or host.
Why uploads are risky
Contact forms with attachments, résumé uploads, client document portals and WordPress media libraries all accept files from outside. Things that go wrong:
- Web shells: a PHP file disguised as an image (
invoice.pdf.php, or a real image with PHP code inside) gets saved in a public folder and executed. - Malicious documents: an Office file or PDF that is harmless to the server but dangerous to the employee who opens it.
- Resource abuse: huge or numerous files that fill the disk or memory.
- Path tricks: a file name like
../../config.phpthat tries to write outside the upload folder.
1. Allow only the types you need, and check the content
Decide exactly which formats the form needs (for example PDF, JPG and PNG) and reject everything else. Use an allow-list, not a block-list; OWASP notes that block-lists are easy to bypass.
Don't trust what the browser says. The PHP manual is explicit that $_FILES['…']['type'] "is however not checked on the PHP side," because it is completely controlled by the client. Instead:
- Check the extension against your allow-list.
- Detect the real type from the file's content on the server, for example with PHP's
finfo(FILEINFO_MIME_TYPE), and make sure it matches the extension. - For images, re-processing them (re-saving through GD or Imagick at the same or smaller size) strips most hidden payloads.
OWASP adds that signature checks "should not be used on their own," which is why the next steps matter just as much.
In WordPress
WordPress checks uploads against an allowed list of file types and tries to detect the real type from the content (wp_check_filetype_and_ext). Keep that protection:
- Don't define
ALLOW_UNFILTERED_UPLOADSinwp-config.php. It lets users with theunfiltered_uploadcapability bypass file type checks. - Be cautious with plugins that "allow any file type" or add SVG uploads without sanitizing them.
- Limit who can upload: give staff the lowest role that does the job, and review form plugins that accept attachments from the public.
2. Rename files and store them safely
- Generate the stored name yourself (for example a random ID) and keep the original name only in the database for display. OWASP recommends changing "the filename to something generated by the application."
- Strip directory parts from any name you do use; the PHP manual's example uses
basename()to help prevent path traversal. - Use
move_uploaded_file(), which only moves genuine uploads from PHP's temp folder. - Store files outside the web root when you can, such as
/home/site/private-uploads/, and deliver them through a script that checks the user is allowed to see that file. This is how client portals should serve invoices, contracts and IDs. - Give stored files restrictive permissions (no execute bit).

3. Block script execution in upload folders
Some uploads must be public, like images in a WordPress media library (wp-content/uploads). For those folders, make sure the web server never runs PHP from them, even if a .php file slips through.
Nginx (inside the site's server block, before the general PHP location):
location ~* ^/wp-content/uploads/.*\.(php|phar|phtml)$ {
deny all;
}
Apache (an .htaccess in the upload folder, or better, in the main configuration):
<FilesMatch "\.(php|phar|phtml)$">
Require all denied
</FilesMatch>
OWASP also advises disabling per-directory overrides (such as .htaccess) in upload folders, so an uploaded config file can't change these rules. On Apache, that means AllowOverride None for the upload path in the main configuration.
On the PHP-FPM side, set security.limit_extensions = .php so FPM refuses to execute anything that isn't a .php file, and run each site as its own system user so a successful upload can't reach other sites.
After you apply the rule, test it: place a harmless test .php file in the upload folder, confirm the browser gets "403 Forbidden" rather than running it, then delete the file.
4. Set size limits on the server
Limits in the browser or a form's hidden MAX_FILE_SIZE field are only a convenience. The PHP manual warns that "fooling this setting on the browser side is quite easy," while the server-side limits "cannot be fooled." Configure:
- PHP:
upload_max_filesize,post_max_size(must be larger) andmax_file_uploads. - Web server:
client_max_body_sizein Nginx orLimitRequestBodyin Apache. - Application: a per-form limit that matches the real need (a résumé doesn't need 100 MB), plus a cap on file name length.
- Check
$_FILES['…']['error']and handle size errors with a clear message.
5. Scan and handle files safely
- Scan uploads for malware before anyone opens them. Options include ClamAV on the server, scanning built into some form or storage services, or your endpoint protection on staff computers. OWASP suggests running files "through an antivirus or a sandbox if available."
- Content Disarm & Reconstruct (CDR) tools rebuild Office and PDF files without active content, which is useful for portals that receive many documents.
- Serve downloads safely: send
Content-Disposition: attachmentand the correctContent-Type, plusX-Content-Type-Options: nosniff, so browsers don't try to render a disguised file as a web page. - Require login for anything that isn't meant to be public, and log who uploaded what.
Quick checklist
- Allow-list of file types, checked by extension and real content
- Files renamed on the server; original names never used as paths
- Private files stored outside the web root and served through an access check
- PHP execution blocked in every public upload folder (tested)
-
security.limit_extensions = .phpin PHP-FPM; one system user per site - Size limits in PHP, the web server and the app
- Malware scanning in place; downloads sent with safe headers
- WordPress: no
ALLOW_UNFILTERED_UPLOADS, roles reviewed
Get your upload forms reviewed
If your site takes résumés, client documents or photos, a short review can confirm these protections are in place. Core Florida IT secures websites, WordPress installs and web servers for businesses in Miami-Dade and South Florida, and remotely nationwide. Request a free IT assessment to get started.


