Most WordPress security guides focus on plugins and admin settings. But true hardening happens at the server level — in your PHP configuration, file permissions, server headers, and database setup. These settings protect you even if a plugin is compromised.
File and directory permissions
Incorrect file permissions are one of the most common misconfigurations on WordPress servers.
Recommended permissions
| Path | Permission |
|---|---|
wp-config.php |
400 or 440 |
/wp-content/ |
755 |
/wp-content/uploads/ |
755 |
PHP files in /uploads/ |
Should not exist |
.htaccess |
444 |
| WordPress core files | 644 |
| Directories | 755 |
Prevent PHP execution in the uploads folder
The uploads folder must accept file uploads but should never execute PHP. A single misconfigured permission here lets attackers upload a webshell and execute it directly.
Apache — add this to /wp-content/uploads/.htaccess:
<FilesMatch "\.php$">
Deny from all
</FilesMatch>
Nginx:
location ~* /wp-content/uploads/.*\.php$ {
deny all;
return 403;
}
WO Security Shield checks this configuration on every scan and flags it as a critical misconfiguration if PHP execution is possible in the uploads directory.
PHP configuration hardening
Edit your php.ini (or use a .user.ini in shared hosting):
; Disable dangerous functions
disable_functions = exec, passthru, shell_exec, system, proc_open, popen, curl_multi_exec, parse_ini_file, show_source
; Hide PHP version from headers
expose_php = Off
; Limit upload sizes
upload_max_filesize = 10M
post_max_size = 12M
; Session security
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
HTTP security headers
Security headers are returned with every HTTP response and instruct the browser how to handle your content. They're free, fast to implement, and stop entire categories of attacks.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:
WO Security Shield audits your HTTP headers on every scan and provides a one-click fix for missing or misconfigured headers.
Database user isolation
Your WordPress database user should only have the permissions it actually needs:
-- Create a restricted WordPress DB user
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'strong-password';
-- Grant only what WordPress needs
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX
ON wordpress_db.*
TO 'wp_user'@'localhost';
-- Never grant SUPER, FILE, or PROCESS privileges
FLUSH PRIVILEGES;
If an attacker compromises your WordPress installation, a restricted database user limits the blast radius — they can't read other databases, write files to disk via SELECT INTO OUTFILE, or execute system commands via sys_exec().
wp-config.php hardening
For a comprehensive deep-dive on this topic, see our dedicated guide to hardening wp-config.php. Move wp-config.php one directory above the WordPress root. Requests to it return a 404 rather than exposing the file path.
Also add:
// Prevent direct file editing via WordPress admin
define( 'DISALLOW_FILE_EDIT', true );
// Limit post revisions
define( 'WP_POST_REVISIONS', 5 );
// Force SSL for admin
define( 'FORCE_SSL_ADMIN', true );
// Restrict wp-cron to server-side only
define( 'DISABLE_WP_CRON', true ); // Use a real cron job instead
Server hardening is a one-time investment that pays off continuously. Combined with a properly configured WordPress application firewall and WO Security Shield's ongoing monitoring and file integrity checks, it creates a layered defence that stops most attacks before they can do any damage. For the full picture, work through our WordPress security checklist.
Essential Server-Level Security Configurations
File Permissions Matrix
Incorrect file permissions are one of the most common security misconfigurations we see:
| Path | Recommended permission | Why |
|---|---|---|
WordPress root (/) |
755 | Readable and executable, not writable |
wp-config.php |
440 (or 400) | Read-only, no write access |
.htaccess |
644 | Apache needs to read it |
/wp-content/ |
755 | WordPress needs to write to subdirectories |
/wp-content/uploads/ |
755 | Media uploads need write access |
/wp-content/plugins/ |
755 | Plugin updates need write access |
| PHP files | 644 | Readable, not writable by web server |
Set these in bulk:
# Fix directory permissions
find /var/www/html -type d -exec chmod 755 {} \;
# Fix file permissions
find /var/www/html -type f -exec chmod 644 {} \;
# Lock down wp-config.php
chmod 440 /var/www/html/wp-config.php
# Ensure uploads directory is writable
chown -R www-data:www-data /var/www/html/wp-content/uploads
PHP Configuration Hardening
Edit your php.ini or add these to your virtual host configuration:
; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
; Prevent file inclusion attacks
allow_url_fopen = Off
allow_url_include = Off
; Limit file upload size
upload_max_filesize = 10M
post_max_size = 10M
; Hide PHP version
expose_php = Off
; Session security
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
Important: Some plugins require allow_url_fopen = On (especially those that fetch remote data). Test your site after making this change.
Block PHP Execution in Upload Directories
Attackers frequently upload PHP backdoors disguised as images. Block PHP execution in directories that should only contain media files:
Apache (.htaccess in /wp-content/uploads/):
<Files "*.php">
Deny from all
</Files>
Nginx:
location ~* /wp-content/uploads/.*\.php$ {
deny all;
return 403;
}
MySQL/MariaDB Hardening
Your database is the most valuable target on your server:
-- Create a dedicated WordPress database user with minimal privileges
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'strong_random_password';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP
ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
-- Don't grant FILE, PROCESS, SUPER, or GRANT OPTION
-- WordPress never needs these
Also in your MySQL config (my.cnf):
[mysqld]
# Bind to localhost only — no remote database access
bind-address = 127.0.0.1
# Disable LOCAL INFILE to prevent data exfiltration
local-infile = 0
Automatic Security Updates
Enable unattended security updates for your server OS. On Ubuntu/Debian:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
This ensures critical OS-level security patches are applied automatically — even if you forget to check for weeks.
