WordPress File Permissions and Database Security
By Stu 9 min read
Most WordPress security advice stops at the dashboard. Strong passwords, 2FA, plugin updates. All important, all necessary, and all focused on stopping an attacker getting in.
The two layers below that are what decide how bad it gets if one does. File permissions control what an attacker can change once they have any kind of foothold. Database security controls what they can read, write, or destroy if they reach the data layer. Both are technical, both are boring, and both are the difference between a small incident and a total rebuild.
This post walks through what the correct configuration actually is on a WordPress site, why it matters, and the mistakes that turn a routine hardening exercise into a broken site.
Why file permissions matter more than they look
The web server user runs PHP. PHP loads WordPress. WordPress writes files (uploads, cache, sometimes plugins). That chain is fine when everything is working. It becomes a problem when an attacker finds a way to write one arbitrary file.
If your file permissions are correct, that one file is all they get. It sits in wp-content/uploads/ (or wherever they landed), and unless PHP can execute there, it's just a file. If your file permissions are wrong, that same foothold lets them overwrite wp-config.php, drop a backdoor into your theme, or modify core WordPress files.
The gap between "attacker uploaded one file" and "attacker owns the site" is entirely determined by file permissions.
The correct permissions
There's a standard setup that works for the vast majority of WordPress installs. Every guide gives you the same numbers because they're the ones that make sense.
- Directories 755 (owner read/write/execute, group read/execute, world read/execute)
- Files 644 (owner read/write, group read, world read)
- wp-config.php 640 or tighter (owner read/write, group read, no world access)
- Any file containing secrets 600 (owner read/write only)
The commands to set these across a WordPress install:
find /path/to/wordpress -type d -exec chmod 755 {} \;
find /path/to/wordpress -type f -exec chmod 644 {} \;
chmod 640 /path/to/wordpress/wp-config.php
Run those from the parent of your WordPress directory, and the whole site is set to the correct baseline.
Ownership is the other half
Permissions alone aren't the whole picture. Who owns the files matters just as much.
The right setup on a typical Linux server is that the files are owned by a dedicated user (say, wpuser), and the web server runs as a different user (www-data, nginx, or a PHP-FPM pool user). The web server user is in a group that has read access to the files, but not write.
Why does that matter? Because most attacks that get code execution do so as the web server user. If the web server user doesn't own the files, it can't overwrite them. WordPress can still write to wp-content/uploads/ and wp-content/cache/, because you specifically grant write access to those directories. Everything else stays read-only from the web server's point of view.
This is the setup that makes "attacker got code execution in PHP" a lot less catastrophic. Without it, a code execution vulnerability is game over.
The exceptions that need write access
WordPress does need write access to a few places. Setting everything to read-only breaks the site. The exceptions are:
- wp-content/uploads/ WordPress writes uploaded media here. Needs write access from the web server.
- wp-content/cache/ if any caching plugin is active. Needs write access.
- wp-content/upgrade/ used during core and plugin updates. Needs write access, at least when updates are happening.
The rest, including plugins, themes, and core files, should not be writable by the web server user in the default case. Updates happen through SFTP or SSH by a different user with actual credentials.
The counter-argument is that this makes the "update from the dashboard" button not work, which some site owners depend on. That's a real trade-off. My take is that if a site is important enough to worry about security, it's important enough to do updates via a proper deploy process, not a dashboard button.
wp-config.php deserves its own paragraph
wp-config.php contains your database credentials, security keys, and any other secrets your site depends on. It should be the most tightly permissioned file on the whole install.
The minimum sensible setup:
- Permissions 640 (some setups go tighter to 400)
- Owner the site's file owner, not the web server user
- Group a group the web server user is in, so PHP can read it, but nothing else
Some hosts recommend 644 for wp-config.php. That's wrong. World-readable credentials are a bad idea even if the file is inside the webroot with a deny rule in front of it. Deny rules can be misconfigured. File permissions are unambiguous.
There's a longer version of this argument in the post about moving wp-config.php above the webroot, which is the other lever available here. Do both if you can.
Testing that permissions are right
The easiest sanity check is to try to write to a core file as the web server user:
sudo -u www-data touch /path/to/wordpress/index.php
If that succeeds, the web server user has write access to your core files. That's the setup you don't want. If it fails with a permission error, the setup is correct.
Do the same in wp-content/uploads/ and it should succeed, because uploads need to work. If both succeed, permissions are too loose. If both fail, uploads are about to break.
The database side
If file permissions decide how bad a compromised web server is, database security decides how much data an attacker can walk off with. Both matter. Both are worth doing properly.
The default WordPress database setup is not particularly secure. The install wizard asks for a database name, user, and password, and gives that user full privileges on the database. That's more than WordPress actually needs, and it means a compromised WordPress install can do anything to the database.
Least-privilege database user
WordPress needs a specific set of MySQL privileges to work. It does not need everything. The right setup is a database user with just the privileges required, and nothing more.
The privileges WordPress actually needs, at a normal running level:
SELECT,INSERT,UPDATE,DELETEon all tablesCREATE,ALTER,DROP,INDEXon all tables (only needed during updates and plugin installs)
The privileges it does not need, and shouldn't have on the running user:
GRANT OPTIONSUPERFILEPROCESS
The pragmatic setup is one user for normal operation with just SELECT, INSERT, UPDATE, DELETE, and a separate admin user for updates and installs with the schema-modifying privileges. Swap the credentials in wp-config.php when you're doing an update, swap them back afterwards.
Most sites don't bother with two users, which is understandable. Even a single least-privilege user with the schema privileges but without GRANT OPTION, SUPER, or FILE is a genuine improvement over the default.
Restrict where the user can connect from
The database user should only be able to connect from the web server. If your MySQL runs on the same server as WordPress, that's localhost. If it runs on a separate database server, it's the internal IP of the web server.
In MySQL, that means creating the user as:
CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'strong-password-here';
Not 'wordpress'@'%', which allows connections from anywhere. If your MySQL is bound to 0.0.0.0 (listening on all interfaces) and your database user allows %, an attacker who finds your credentials anywhere can connect directly to your database from the internet.
Bind MySQL to 127.0.0.1 if it's on the same server. If you need remote access for admin, use SSH tunnelling, not open MySQL ports.
The wp_ table prefix debate
Every WordPress security guide tells you to change the default wp_ table prefix. Some make it sound like the single most important thing you can do.
It isn't. The prefix change stops the very laziest scanners that look for tables named exactly wp_users or wp_options. Any halfway serious attack that reaches the database layer will discover the actual prefix in seconds, either from an error message or by running SHOW TABLES.
Change the prefix if you're setting up a new site. It costs you nothing. Don't change it on an existing site unless you're doing other database work at the same time, because the change is fiddly and touches every plugin that hardcodes the prefix (a lot of them do, incorrectly). The security gain is small.
Encrypted backups
Database backups often contain the entire site. Every password hash, every user email, every private post, every WooCommerce order. If backups leak, you've handed the attacker a copy of everything.
Encrypt them. Whatever backup solution you use, encrypt the backup files with a key that isn't stored on the same server. Restic, Borg, and gpg-encrypted mysqldump all work.
An unencrypted backup sitting on the same server as WordPress is worse than useless. It's an extra target that contains the same data as the live site, at a slightly different point in time.
What actually matters
If you have to pick the highest-value database and file items, the shortlist is:
- Web server user cannot write to core files, plugins, or themes
wp-config.phpis 640 and not world-readable- Database user has only the privileges WordPress actually needs
- MySQL bound to
localhost(or internal network), not the public internet - Backups are encrypted with a key that isn't on the same server
Everything else is worth doing but sits below these. If you close off those five, you've dealt with the majority of the "compromise gets worse because of the layer under WordPress" problem.
Where this fits
File permissions live in Chapter 4 of the handbook. Database security is Chapter 5. Between them they cover the layer underneath WordPress that most security guides skip past.
If this post left you unsure about any of the configuration, that's where the detailed walkthrough lives, including the exact permissions per file, the MySQL user setup, and the backup strategy that ties it together.
Get the book for £19.
Get the free WordPress Security Checklist
The security checks I'd run through on any WordPress site, delivered straight to your inbox.
Want to go deeper?
The first chapter of Protect My WP is free. Start with the foreword, then read Chapter 1 on hosting and server security. There is also a shorter guide that walks the same ground faster if you want the shape of the book first.