PROTECT MY WP

WordPress File Permissions and Database Security

By 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.

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:

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:

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:

The privileges it does not need, and shouldn't have on the running user:

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:

  1. Web server user cannot write to core files, plugins, or themes
  2. wp-config.php is 640 and not world-readable
  3. Database user has only the privileges WordPress actually needs
  4. MySQL bound to localhost (or internal network), not the public internet
  5. 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.

More on this topic

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.