in Backend Tech, System Admin, Tips

Bulletproof WordPress via Nginx

I assume many developers consider WordPress as a joke since it’s made with “PHP”. However, WordPress is still powering a lot of websites. So quite often it is inevitable to do some work on a project that deals with WordPress.

Personally, I’ve had to deal with many WordPress sites and resolve security issues. The most common issues that I observed have been:

  • Backdoor attack to use the infected host to perform various types of attack
  • Stealing an admin cookie
  • Using the stolen cookie to post many dangerous posts
  • Using the stolen cookie to upload other scripts in wp-content directory
  • And so on

The most used attack paths the hackers/hacking tools seem to be wp-admin/(post-new|post|admin-post|).php and /wp-login.php.

Anyhow, the most impactful defense mechanism that I found was to whitelist IP address that belongs to a certain admin user. So far, nothing else has beaten that solution, so I call it a bulletproof Nginx config for WordPress site.

Here’s my Nginx config that I used for my clients to prevent hackers from attempting to intrude a WordPress site.

  location ~ /wp-admin/admin-ajax\.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_read_timeout 300;
    fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
    allow all;
  }

  location ~ (/wp-admin/.*\.php|wp-login\.php$) {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_read_timeout 300;
    fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;

    ## whitelist IPs
    allow x.x.x.x;
    deny all;
    error_page 403 = @wp_ban;
  }

  location @wp_ban {
    rewrite ^(.*) https://mysite.com permanent;
  }

  location ~* /(?:uploads|files|wp-content|wp-includes|akismet)/.*.php$ {
    deny all;
    access_log off;
    log_not_found off;
  }