WordPress: How to protect from “Ops” side

Artem Hatchenko
6 min readDec 8, 2023

--

If you have used WordPress I suppose you read some articles or tips on how to protect your WP. Most articles focused on methods from WP, like using a strong password, changing the default path to the admin page, using only authorized plugins, etc. But even if you followed all of them and one day suddenly noticed, that your blog turned into an advertisement for a sneakers shop (my last case 😄), I suggest checking your server side.

PHP

It doesn’t make a difference if you Apache only, PHP-FPM with Nginx combination, or Apache+Nginx, because in the end all of them require installed PHP in your system. So, let’s start with it.

We are interested in the file “php.ini”. Depending on the distribution you are using, it may be located on different paths.

In RedHat based it will be: /etc/php.ini
In DEB based: /etc/php/8.2/fpm/php.ini
Where “8.2” is a version of the installed PHP

Open it and look for “disable_functions” parameter. By default it’s empty. In a comment for this option, we can see the following:

; This directive allows you to disable certain functions.
; It receives a comma-delimited list of function names.
; https://php.net/disable-functions

Update it to the following:

Here’s a brief description of each disabled function:

  1. exec: Executes an external program.
  2. passthru: Executes an external program and displays raw output.
  3. shell_exec: Executes a command via shell and returns the complete output.
  4. system: Executes an external program and displays the output.
  5. proc_open: Opens a process for input/output.
  6. popen: Opens a process for reading or writing.
  7. curl_exec: Executes a cURL session and returns the output.
  8. curl_multi_exec: Run the sub-connections of the current cURL handle in parallel.
  9. parse_ini_file: Parses a configuration file and returns the settings.
  10. show_source: Outputs the source code of a file.

This is not a complete list of what can be disabled for security purposes, but disabling certain functions may lead to plugins, themes, etc. not working. This is a list of disabled functions that I have been using for my site for a long time.

Also, let’s find the following parameters and update them to the following values:

Descriptions for each of them can be found in “php.ini” file near with parameter.

Don’t forget to restart your PHP Process Manager after changes.

System users and group

In case you use Nginx in front of PHP Process Manager, you need to organize access to both processes for the site directory. Which possible options?

  1. Add permission to all for the directory recursively.
    Of course, it’s a joke, if you see a suggestion to execute something like chmod -R 777 /path/to/your/dir to fix the permission issue, just close this “guide”.
  2. Run both processes from the same user, for example, use “nginx” user for this.
    Since you can modify the user from who runs Nginx and PHP PM (in my case it’s PHP-FPM), this will work. But it’s not good not only from a security perspective but also from the operation side. For example, we changed to use Nginx and PHP-FPM to use “nginx” user, and start them, what’s happens? Nginx process running as before, but PHP-FPM can’t create PID file, write logs, or open a socket (if you use a socket instead of a port), because the necessary directories belong to “apache” user (in my case I use a RedHat-based OS, in other distributions can be different user for PM by default) and we started process from “nginx” user. We can add extra permissions to directories, but don’t, see point 1.
  3. Use a common group for Nginx and PHP PM users.
    The basic idea is that since Nginx only serves static files, the rest of the requests are passed to the PHP PM, so it requires only read permission, we will set as owner for WP directory “apache” user and common group for Nginx and PHP PM. In this case doesn’t matter which value of “umask” by default for non-root users in your system (002 or 022), in both cases, a group can read. We can create a new group, add both users to it, and change the group in PHP PM configuration or use the default group for PHP PM.

Let’s look at the 3rd option using my setup as an example:

  • OS: RockyLinux (RedHat-based)
  • Web Server: Nginx
  • PHP Process Manager: PHP-FPM
  • Default users: “nginx” and “apache”
  • Default groups: “nginx” and “apache”
  • umask: 002

Checking current users/groups for services (depends on your system path to directories with config files can be different). Also check, that these users have no shell (it’s not by default), they don’t require it.

I’m planning to go with “apache” group, so let’s add “nginx” user to it:

sudo usermod -aG apache nginx

Changing owner and group for directory for WordPress:

sudo chown -R apache:apache /var/www/html/artem_services

“Reset” the permissions to default based on your “umask” for directories and files inside WP directory:

sudo find /var/www/html/artem_services/ -type d -exec chmod 775 {} \;
sudo find /var/www/html/artem_services/ -type f -exec chmod 664 {} \;

Why 775 for directories and 664 for files? Because my “umask” by default is “002”, it means that new directories by default will be created with permissions “775” and files with “664”. We can check “/etc/profile” to look for rules to set “umask” by default.

# By default, we want umask to get set. This sets it for login shell
# Current threshold for system reserved uid/gids is 200
# You could check uidgid reservation validity in
# /usr/share/doc/setup-*/uidgid file
if [ $UID -gt 199 ] && [ "`/usr/bin/id -gn`" = "`/usr/bin/id -un`" ]; then
umask 002
else
umask 022
fi

So in my system “umask” for the root user will be 002, and for regular users — “022”. Also, you can run “umask” command without any parameters to check the current value.

If you’ve not faced “umask” before, you need to know, that default permissions without “umask” to a directory in Linux is 777 and for files — 666. So take the default permissions and value of umask and apply bitwise NOT, and with umask “002” we have 775 and 664 accordingly.

To test upload a media file via WP UI and check the corresponding directory, then delete it (also via UI), and check that the files were successfully deleted.

You can configure permissions in the same way using the existing group, or create a new one. Also, if necessary you can add “Setgid” to directory, all these methods will work and restrict unauthorized access to the directory.

Nginx

Add your IP address to allow for Admin console only from it. Even if you use a strong password and a non-standard user, attackers can take advantage of the vulnerabilities of WP, or can add the load to your server by brute-force at least.

To do this you can use the following example:

location = /wp-login.php {
allow 1.2.3.4;
deny all;
fastcgi_index index.php;
fastcgi_pass unix:/var/run/php-fpm/www.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

Don’t forget to change “proxy_pass” to your’n, and IP as well.

If you host your server behind CDN, it makes no sense, because all requests will be from CDN IPs, in this case, you have to configure WAF rule on CDN side.

And let’s deny all hidden directories and files, such as git, env files, etc. To do this you can use the following example:

location ~ /\. {
deny all;
return 444;
access_log off;
}

Notice, that we will use 444 HTTP code — “No Response”. It means that we will “ignore” requests to this location and not spend our resources on bots.

Uncategorized

Obvious recommendations, but still…

  • Don’t use any Control Panel if possible or don’t expose it to the world at least.
  • Don’t use login/password method for authentication, just disable it in SSH server configuration and use key instead.
  • Don’t use FTP at all. If necessary go ahead with SFTP/FTPS/FTP over SSH.
  • If you use WP plugin for comments, disable the opportunity to attach files to comments, if it’s not necessary.

Other recommendations are too obvious, so this is it. In the next article, I’ll share with you how I cleaned my WP site after it was hacked.

--

--