When browsing https://example.com/wp-content/uploads/ returns a list of folders and files, the web server has directory listing enabled — a feature on by default in some older Apache configurations. A correctly hardened WordPress site should return 403 or a blank page. A visible listing is information disclosure that puts the site at risk even if the files themselves seem "not sensitive".
Why this matters
The uploads directory often holds far more than people realize: database backups that a backup plugin failed to push to remote storage and dropped locally, internal PDFs uploaded for a single share, training videos for staff, images deleted from a post but still on disk, and stray .htaccess files written by a misbehaving plugin. An attacker who can browse the index sees all of it. A real-world example: many blogs were exposed globally when UpdraftPlus wrote a full ZIP backup of the DB and files into uploads with a guessable filename — anyone who browsed the directory could download the entire site. Even without backups, exposing the list of installed plugins (via folder names in plugins/) lets an attacker pick a vulnerability that targets each plugin specifically.
How to detect
In a private browser window, visit:
https://example.com/wp-content/uploads/https://example.com/wp-content/uploads/2024/https://example.com/wp-content/plugins/https://example.com/wp-includes/
If you see "Index of /..." or a file listing, directory listing is active. You should be getting 403 or a redirect to the home page.
How to fix
On Apache, add to the root .htaccess:
Options -Indexes
This single directive disables listings for every directory under the document root. To target only uploads:
# wp-content/uploads/.htaccess
Options -Indexes
<FilesMatch "\.(zip|sql|gz|tar|bak)$">
Require all denied
</FilesMatch>
On Nginx, in the server block:
autoindex off;
location ~* \.(zip|sql|gz|tar|bak)$ {
deny all;
return 404;
}
Defense in depth — drop an empty index.php in every folder. If listing is ever accidentally re-enabled, the browser receives the empty file instead of the index:
<?php
// Silence is golden.
WordPress creates this file automatically in the top-level uploads directory but not in every year/month subfolder. Add them manually or via a quick script: find wp-content/uploads -type d -exec touch {}/index.php \;
Common mistakes
Mistake one: blocking /wp-content/uploads/ entirely instead of just the listing. That breaks every legitimate image. Options -Indexes disables the listing only — direct access to specific files still works. Mistake two: relying on random file names. backup-1d8f2k3.zip may be unguessable, but if it shows up in a public index the randomness does not help. Mistake three: fixing it on Apache while a fronting Nginx still serves the listing (or vice versa). When Nginx fronts Apache, listing can be blocked in one and open in the other.
Verifying the fix
Repeat each URL from the detection step. All four should return 403, 404, or a blank page — never a file list. More aggressive verification with curl: curl -s https://example.com/wp-content/uploads/ | head -20 — if the body is empty or 403, the block works. Then sweep for accidentally uploaded files: find wp-content/uploads -name "*.zip" -o -name "*.sql" -o -name "*.bak" and remove them.