7

Using WP 4.8.2

What is the best way to check the requesting URL when processing a request with the rest-api?

For example, a site receives a request and you want to check if it came from an 'allowed' URL. And fail if the URL is not allowed.

This does not work:

function my_check_request_url( $request, $url ) {

    $bits = parse_url( $url );

    if ( $bits['host'] != 'example.com' )
       $request = false;

    return $request;

}
add_filter( 'rest_request_from_url', 'my_check_request_url', 10, 2 );
  • After commenting out the conditional, the response is still sent. So I think I'm using the wrong hook. – shanebp Oct 5 '17 at 17:02
  • have you checked what $request and $url vars look like via var_dump or similar, I find that inspecting the inputs and outputs always lead to a proper answer. – farinspace Oct 5 '17 at 23:48
  • 2
    referring url is easily faked and can't be used for any sort of security. – Milo Oct 7 '17 at 14:59
  • We're using tokens and ssl. We'd also like to check the referring url, regardless of whether or not it can be faked. – shanebp Oct 7 '17 at 15:29
  • 2
    it is an API open to the web, what is this talk about referees as authentication and SSL is just not relevant. You are probably also disabling CORS protections... Unless it is available to only logged in users, this has zero security in it. – Mark Kaplun Oct 7 '17 at 17:41
5
+100

That filter is definitely not the one you are looking for. That filter fires before returning the result of WP_REST_Request::from_url() which appears to be a factory method that is only used internally to handle embeds.

A better option is to return a WP_Error instance on the rest_pre_dispatch filter.

Some caveats:

As mentioned by @milo, the referer is not reliable and shouldn't be used for a security check.

Additionally, it is not guaranteed to be set.

With those out of the way, here is an example of how you might use the rest_pre_dispatch filter to cause the request to fail if it is from a bad referer:

function wpse281916_rest_check_referer( $result, $server, $request ) {
    if ( null !== $result ) {
        // Core starts with a null value.
        // If it is no longer null, another callback has claimed this request.
        // Up to you how to handle - for this example we will just return early.
        return $result;
    }

    $referer = $request->get_header( 'referer' );

    if ( ! $referer ) {
        // Referer header is not set - If referer is required, return a WP_Error instance instead.
        return $result;
    }

    $host = wp_parse_url( $referer, PHP_URL_HOST );

    if ( ! $host ) {
        // Referer is malformed - If referer is required, return a WP_Error instance instead.
        return $result;
    }

    if ( 'mysite.com' !== $host ) {
        // Referer is set to something that we don't allow.
        return new WP_Error(
            'invalid-referer',
            'Requests must contain a valid referer',
            compact( 'referer' )
        );
    }

    // Otherwise we are good - return original result and let WordPress handle as usual.
    return $result;
}
add_filter( 'rest_pre_dispatch', 'wpse281916_rest_check_referer', 10, 3 );
4

Anything you receive from the client is considered user input and should not be trusted. As the header can be easily manipulated and abused, my suggestion is not to use this method if you are relying on it for sensitive data.

If the requests are coming from a page, you can have another approach. Otherwise, anyone can send a request to the API from nowhere and alter the referrer.

Let's say you have a bunch of pages that are filtered as "Allowed". You can create a nounce only for these pages, and then validate them in your request.

If a nounce exists and is valid, then the request is allowed. Otherwise, block it.

  • 4
    +1... it is an API .... the assumption you get calls only from browsers is ridiculous. – Mark Kaplun Oct 7 '17 at 17:36
  • Yes, I think nounce is a better approach since it doesn't exist if someone directly sends a request to API. – Jack Johansson Oct 8 '17 at 22:50
4

@ssnepenthe's aswer is right in saying that the hook you are using is not the right one something in incoming request.

Request information are available immediately to PHP, so you could use the earliest hook available to check them. And if you want to do this in the context of request API you should use the earliest hook of a REST API request. 'rest_pre_dispatch' suggested by @ssnepenthe is fine, maybe another option could be rest_authentication_errors that would allow you to return an error in case something is wrong.

But Jack Johansson is right in saying that HTTP headers (like the referer header used in @ssnepenthe's aswer) are not trustable, as they are very easily changed by the client. So it would be just like put a security guard in front of a door who just ask "it is safe to let you enter?" to anyone who want to go in: that's not going to work.

But the soluition proposed Jack Johansson's answer (a nonce) is not a real solution either: the whole point of nonces is to change with time, and a public API endpoint can't have things that change based on time. Moreover, WP nonces are trustable only when there's a logged-in user, which might not be the case for a public API and if an user is logged in, there's probably no reason to check the incoming domain: you trust the user, not the user machine.

So, what to do?

Well, even if HTTP headers are not trustable, not all the information available on $_SERVER comes from headers.

Normally, all the $_SERVER values whose keys starts that starts with HTTP_ comes from headers and have to be treated as unsafe user input.

But, for example, $_SERVER['REMOTE_ADDR'] contains the IP address used for the TCP connection to your server, which means it is trustable1.

Which also means, that either:

  • properly configuring the server to generate the $_SERVER['REMOTE_HOST'] value (for example in Apache you'll need HostnameLookups On inside your httpd.conf) that value
  • using gethostbyaddr to do a reverse DNS lookup to resolve the domain name of the IP stored in $_SERVER['REMOTE_ADDR']

you could obtain quite reliably an host name that you could use to check against a whitelist (for the code, you could adapt the code from @ssnepenthe's aswer where you would replace $referer = $request->get_header('referer') with $referer = gethostbyaddr($_SERVER['REMOTE_ADDR'])).

But there's an issue.

If your webserver is behind a reverse proxy (quite common solution, actually) the TCP connection to the webserver is actually made by the proxy, so $_SERVER['REMOTE_ADDR'] will be the IP of the proxy, and not the IP of the client who originally sent the request.

The original request IP in such cases is usually available as $_SERVER['HTTP_X_FORWARDED_FOR'], but being one of those $_SERVER values that start with HTTP_ it is not really trustable.

So, if your webserver is behind a reverse proxy2 even the $_SERVER['REMOTE_ADDR'] would not be useful for such guard and a domain-based whitelist could only implemented at the proxy level.

In short, a reliable solution for API endpoint securing should be either implemented using some real authentication mechanism (e.g. oAuth) or should be done acting directly on the server configuration and not at application level.


Notes

1 Well, in theory it could be broken if someone hacked your ISP or if an attacker acts from inside your LAN, in both cases there's very little that you could do to be safe.

2 If you don't know if you are behind a reverse proxy, you can send a request from your local PC and check if $_SERVER['REMOTE_ADDR'] on the server match the local PC IP and also if $_SERVER['HTTP_X_FORWARDED_FOR'] is present and it matches the local PC IP.

  • The OP is trying to get the referrer, so I assumed he wants to do it on a page, no directly pinging the API. – Jack Johansson Oct 9 '17 at 23:30
  • @JackJohansson actually the OP never mentioned referer :) They say they want "to check if it came from an 'allowed' URL" which sounds they are looking to whitelist API endpoint for speficic domains, and also the code snippet in OP gives the same idea to me. – gmazzap Oct 10 '17 at 8:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.