🛡️ WordPress hardening: how to change and protect the uploads directory
The default WordPress uploads path (/wp-content/uploads/) is one of the first targets in automated attacks. Recon bots scan this predictable location for forgotten configs, plugin backups, and script execution vectors (RCE).
In this guide, I document the practical workflow I used to move the directory to a custom location and apply additional server-layer hardening (Apache/Nginx) so malicious code cannot be executed even if a bad file is uploaded.
The goal is straightforward: reduce attacker predictability, preserve integrity, and keep service availability under pressure.
---
1. Infrastructure planning and preparation
Before touching code, prepare filesystem structure correctly.
📁 Create the new directory
Connect through SSH or FTP and create the target folder. From a security standpoint, avoid names containing "uploads" or "media".
- Example path:
/public_html/storage_core/data/
mkdir -p /public_html/storage_core/data/
🔑 Permission model (Linux)
The web service user (www-data, apache, or cPanel account user) must have write permission.
chmod 755 /public_html/storage_core/data/
If needed, fix ownership:
chown -R accountuser:accountuser /public_html/storage_core/data/
---
2. WordPress core configuration
Update uploads behavior in wp-config.php.
- Open
wp-config.phpin site root. - Insert this line before:
/ That's all, stop editing! Happy publishing. /
// Define new uploads path relative to WordPress root
define('UPLOADS', 'storage_core/data');
Technical note:UPLOADSmust be relative toABSPATH, without leading slash.
After saving, perform a test upload in Media Library to validate write behavior.
---
3. Data migration and database integrity
🚚 Move existing files
For sites with existing media, migrate old files:
- Source:
/wp-content/uploads/* - Destination:
/storage_core/data/
rsync -avh --progress /public_html/wp-content/uploads/ /public_html/storage_core/data/
🗄️ Search and replace (URL integrity fix)
This is the most missed step in real migrations: old media URLs remain stored in database fields (wp_posts, wp_postmeta).
- Find:
wp-content/uploads/ - Replace with:
storage_core/data/
WP-CLI approach:
wp search-replace 'wp-content/uploads/' 'storage_core/data/' --all-tables --precise --report-changed-only
Manual SQL fallback:
UPDATE wp_posts
SET post_content = REPLACE(post_content, 'wp-content/uploads/', 'storage_core/data/');
UPDATE wp_postmeta
SET meta_value = REPLACE(meta_value, 'wp-content/uploads/', 'storage_core/data/');
Production practice: always take a full backup before any global replace.
---
4. Server hardening: block script execution
Moving path improves obscurity, but does not stop .php execution attempts.
Apache (.htaccess inside new directory)
Create /public_html/storage_core/data/.htaccess:
# Block PHP execution in media directory
<Files *.php>
deny from all
</Files>
Nginx (server block)
location /storage_core/data/ {
location ~ \.php$ {
deny all;
}
}
In hybrid stacks (Nginx proxy + Apache backend), enforce blocking in both layers to avoid internal-route bypass.
---
5. Operational post-hardening validation
After migration and hardening rules, I run a strict validation cycle:
- New uploads from admin panel (
/wp-admin/upload.php) write into new path. - Existing media in old posts still renders correctly.
- Script execution attempt is blocked.
curl verification:
curl -I https://domain.tld/storage_core/data/test-image.webp
curl -I https://domain.tld/storage_core/data/probe.php
Expected result:
- image:
200 OKor304 Not Modified probe.php:403 Forbiddenor404 Not Found
I also review error logs to confirm no silent breakage on gallery/CDN plugins.
---
6. Common field failures and fixes
Failure A: new images fail to upload
Likely cause:
- wrong permissions
- wrong ownership
open_basedirblocking new path
Fix:
- Revalidate
chmod/chown. - Check
open_basedirinphp.inior FPM pool.
Failure B: old media links break
Likely cause:
- incomplete
search-replace - stale cache on plugin/CDN
Fix:
- Re-run
search-replacewith report. - Purge app and CDN cache.
Failure C: direct origin IP still exposes uploads
Likely cause:
- WAF/CDN active, but origin still reachable without local controls
Fix:
- Enforce local Apache/Nginx blocking.
- If reverse proxy is used, restrict origin access to trusted proxy ranges.
---
🏁 Strategic conclusion
By changing the default directory and blocking script execution, you remove your WordPress from the common recon path used by generic botnets and reduce damage from automated upload payloads.
Final checklist:
- [ ] Full backup before editing
wp-config.php. - [ ] Correct permissions and ownership on new folder.
- [ ] New uploads writing to custom path.
- [ ]
search-replacecompleted for legacy URLs. - [ ] PHP execution blocked in new media path.
- [ ] HTTP behavior validated with
curl.
This is the operational discipline that separates fragile deployments from professional, resilient infrastructure.
This post is licensed under CC BY-NC.
Comments
Join the discussion below.
Comments are not configured yet. Add Cusdis settings in /assets/json/config/blog-comments-config.json.