HOW TO: Handle query strings for WordPress in nginx (like a crazy person would)

April 5, 2020

You need a method for nginx to uniquely handle incoming requests that contain a specific query string. For the purposes of this guide, we'll be looking at proper handling of WordPress core's post/page password protection feature.

It's common practice for WordPress site admins to use nginx to deny all IPs except for a specific list, which they specify like allow 123.123.123.123 on any requests to wp-admin or wp-login.php

The rule might look something like this:

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

This way, only pre-approved users (administrators, editors, etc.) can access the login page.

But an interesting problem arises if your WordPress site has public facing posts or pages that are set to Password Protected:

These might be posts that are directed to a different userbase than admins/editors (ie. a customer mailing list, or staff associated with the website's company who are not involved in maintaining it).

Once a page is set to use WordPress core's password protected feature, frontend users need to enter the correct password which is validated through wp-login.php. Here's what the password submission request looks like:

https://YOUR_DOMAIN.com/wp-login.php?action=postpass

Naturally, as long as the user does not have their IP whitelisted in nginx - they will be blocked by nginx before WordPress can validate the password.

Somewhat less obviously, the site admins/editors won't know about this problem, even if they test themselves because they are whitelisted already.

A solution

One way to handle this is to exempt requests that contain the query string postpass, from the location block that performs the whitelisting check. This way, only WordPress will be responsible for validation of password protected page, not nginx .

While we can't make nginx match a query string in a location block. Something like location ~ ^?action=postpass {allow all} will NOT work. Location blocks match for URIs, not query strings.

But we can get a little creative and use nginx's error page handler:

error_page 418 = @passwordprotected;
if ( $query_string ~ "postpass" ) { return 418; }

location @passwordprotected {
    allow all;
  }

The first line instantiates a new "error" page that responds with a 418 status code. You can pick any number but don't use an existing http status code like 200, 301, 402, 503, etc. You can find a full list of http status codes at https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.

The second line instructs nginx to send that 418 response to a request if the query string postpass is present.

The third line opens a location block that specifies what the "error" response does. In this case, the "error" simply allows all IPs access, overriding the existing whitelist.

Fun fact: 418 is technically a recognised status code "I'm a teapot". It was part of an April fool's joke in 1998.