4

I've had an Nginx server running for over a year with no issues, it serves HTML and PHP files just fine for the Wordpress sites on it. Recently there was some brute force attacks on the wp-login.php so I decided to limit access to these areas by IP using Nginx's deny all rules.

I've implemented the rule below and IP's not in the list get a 403 as expected but allowed IPs are served a downloaded wp-login.php file instead of the site page.

Here is the domain rules:

server { 
   listen      10.99.0.20:8080;
   server_name  www.example.com;
   root /home/www.example.com/public_html;
   index index.html index.htm index.php;
   include conf.d/whitelisted.conf;
   include conf.d/wp/restrictions.conf;
   include conf.d/wp/wordpress.conf;
 }

The whitelisted.conf includes a load of whitelisted IP's, I won't post the list but it ends:

   ...
   allow 1.2.3.4;

   # DROP THE WORLD #
   deny all;

The restrictions.conf to which I've added the new deny rule (last) is:

# Global restrictions configuration file.
# Designed to be included in any server {} block.
location = /favicon.ico {
        log_not_found off;
        access_log off;
}

location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
}

# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~ /\. {
        deny all;
}

# Block PHP files in uploads, content, and includes directory.
location ~* /(?:uploads|files|wp-content|wp-includes)/.*\.php$ {
  deny all;
}

 # location ~ ^/(wp-admin|wp-login\.php) {
                allow 1.2.3.4
                deny all;
 }

The wordpress.conf file is:

 # WordPress single site rules.
# Designed to be included in any server {} block.

# This order might seem weird - this is attempted to match last if rules below fail.
# http://wiki.nginx.org/HttpCoreModule
location / {
  try_files $uri $uri/ /index.php?$args;
}

# Add trailing slash to */wp-admin requests.
rewrite /wp-admin$ $scheme://$host$uri/ permanent;

# Directives to send expires headers and turn off 404 error logging.
location ~* ^.+\.   (ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
   access_log off; log_not_found off; expires max;
}

# Block PHP files in uploads directory.
location ~* /(?:uploads|files)/.*\.php$ {
  deny all;
}

# Block PHP files in content directory.
location ~* /wp-content/.*\.php$ {
  deny all;
}

# Block PHP files in includes directory.
location ~* /wp-includes/.*\.php$ {
  deny all;
}

# Block PHP files in uploads, content, and includes directory.
location ~* /(?:uploads|files|wp-content|wp-includes)/.*\.php$ {
  deny all;
}

# Pass all .php files onto a php-fpm/php-fcgi server.
location ~ [^/]\.php(/|$) {
  fastcgi_split_path_info ^(.+?\.php)(/.*)$;
  if (!-f $document_root$fastcgi_script_name) {
    return 404;
  }
  # This is a robust solution for path info security issue and works     with "cgi.fix_pathinfo = 1" in /etc/php.ini (default)

  include fastcgi_params;
  fastcgi_pass unix:/var/run/php-fpm/php5-fpm.sock;
  fastcgi_index index.php;
  include /etc/nginx/fastcgi_params;
  fastcgi_buffer_size 128k;
  fastcgi_buffers 256 16k;
  fastcgi_busy_buffers_size 256k;
  fastcgi_temp_file_write_size 256k;
  fastcgi_read_timeout 18000;  
}

...and finally the fastcgi_params is:

fastcgi_param   QUERY_STRING        $query_string;
fastcgi_param   REQUEST_METHOD      $request_method;
fastcgi_param   CONTENT_TYPE        $content_type;
fastcgi_param   CONTENT_LENGTH      $content_length;

fastcgi_param   SCRIPT_NAME     $fastcgi_script_name;
fastcgi_param   REQUEST_URI     $request_uri;
fastcgi_param   DOCUMENT_URI        $document_uri;
fastcgi_param   DOCUMENT_ROOT       $document_root;
fastcgi_param   SERVER_PROTOCOL     $server_protocol;
fastcgi_param   SCRIPT_FILENAME         $document_root$fastcgi_script_name;
fastcgi_param   PATH_INFO       $fastcgi_script_name;

fastcgi_param   GATEWAY_INTERFACE   CGI/1.1;
fastcgi_param   SERVER_SOFTWARE     nginx/$nginx_version;

fastcgi_param   REMOTE_ADDR     $remote_addr;
fastcgi_param   REMOTE_PORT     $remote_port;
fastcgi_param   SERVER_ADDR     $server_addr;
fastcgi_param   SERVER_PORT     $server_port;
fastcgi_param   SERVER_NAME     $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param   REDIRECT_STATUS     200;

Appreciate someone pointing me in the right direction. Many thanks.

d1ch0t0my
  • 443
  • 7
  • 22

1 Answers1

2

In your configuration, PHP files are processed by the following block:

location ~ [^/]\.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    if (!-f $document_root$fastcgi_script_name) {
        return 404;
    }
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php-fpm/php5-fpm.sock;
    fastcgi_index index.php;
    include /etc/nginx/fastcgi_params;
    fastcgi_buffer_size 128k;
    fastcgi_buffers 256 16k;
    fastcgi_busy_buffers_size 256k;
    fastcgi_temp_file_write_size 256k;
    fastcgi_read_timeout 18000;  
}

By adding location ~ ^/(wp-admin|wp-login\.php) { ... } you are diverting those URIs to be processed as static files.

One solution is to duplicate all of the required FastCGI statements into the new location, so that nginx processes the URI as a PHP file, for example:

location ~ ^/(wp-admin|wp-login\.php) {
    allow 1.2.3.4
    deny all;

    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    if (!-f $document_root$fastcgi_script_name) {
        return 404;
    }
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php-fpm/php5-fpm.sock;
    fastcgi_index index.php;
    include /etc/nginx/fastcgi_params;
}
Richard Smith
  • 45,711
  • 6
  • 82
  • 81