Percio Castelo.
PT Home
WordPress hardening: how to change and protect the uploads directory
← Back to blog

WordPress hardening: how to change and protect the uploads directory

6/7/2026 · 3 min · WordPress · 924 views

Hardening uploads: the full checklist#

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.

  1. Open wp-config.php in site root.
  2. 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: UPLOADS must be relative to ABSPATH, 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/' '/public_html/storage_core/data/' \
  --all-tables-with-prefix --precise --report-changed-only --dry-run

Run with --dry-run first to preview changes before applying them.

Important: Avoid blind UPDATE ... REPLACE queries directly on wp_postmeta, as raw SQL string replacement corrupts PHP serialized arrays and objects (such as image metadata and generated intermediate sizes). Always prefer wp search-replace --precise.

4. Server hardening: block script execution#

Moving the path improves obscurity, but does not stop execution attempts if a malicious file is uploaded.

Apache (.htaccess inside new directory)#

Create /public_html/storage_core/data/.htaccess:

# Block all script execution in media directory (Apache 2.4+)
<FilesMatch "(?i)\.(php|phtml|phar|php[0-9])$">
    Require all denied
</FilesMatch>

Nginx (server block)#

location /storage_core/data/ {
    location ~* \.(php|phtml|phar|php[0-9])$ {
        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:

  1. New uploads from admin panel (/wp-admin/upload.php) write into new path.
  2. Existing media in old posts still renders correctly.
  3. 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 OK or 304 Not Modified
  • probe.php: 403 Forbidden or 404 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_basedir blocking new path

Fix:

  1. Revalidate chmod/chown.
  2. Check open_basedir in php.ini or FPM pool.

Likely cause:

  • incomplete search-replace
  • stale cache on plugin/CDN

Fix:

  1. Re-run search-replace with report.
  2. 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:

  1. Enforce local Apache/Nginx blocking.
  2. 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-replace completed 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.

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

924 reads
Creative Commons Attribution-NonCommercial

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments