DEV Community

David Woolf
David Woolf

Posted on • Originally published at indexforwp.com on

How to secure your REST API routes in WordPress

In our last article we looked at creating your own routes in the WordPress REST API using register_rest_route, along with some basic examples of requiring and checking for parameters. Today, we’ll go over a better way to handle validation and sanitization for data passed to your routes.

Where we left off before

Here is the final code we ended up with in our last session:

add_action('rest_api_init', 'register_your_routes');

function register_your_routes() {
    register_rest_route(
        'ndx/v1',
        'my-endpoint',
        array(
            array(
                'methods' => WP_REST_Server::READABLE,
                'callback' => 'callback_function',
                'permission_callback' => '__return_true'
            ),
            array(
                'methods' => WP_REST_Server::EDITABLE,
                'callback' => 'another_callback_function',
                'permission_callback' => '__return_true'
            )
        )
    );

    // our new route
    register_rest_route(
        'ndx/v1',
        'my-endpoint/(?P<id>\d+)',
        array(
            array(
                'methods' => WP_REST_Server::READABLE,
                'callback' => 'callback_function_with_id',
                'permission_callback' => '__return_true'
            )
        )
    );
}
Enter fullscreen mode Exit fullscreen mode

We created a route called my-endpoint with GET and POST methods that do not require a parameter passed in, along with a similarly named route that requires an integer at the end (for example: my-endpoint/10).

Defining arguments

In our previous article, we created a new route that required an integer at the end:

// requires an id parameter, which must be a number
register_rest_route(
    'ndx/v1',
    'my-endpoint/(?P<id>\d+)',
    array(
        array(
            'methods' => WP_REST_Server::READABLE,
            'callback' => 'callback_function_with_id',
            'permission_callback' => '__return_true'
        )
    )
);
Enter fullscreen mode Exit fullscreen mode

Once again, here is how the regular expression defining the works:

'(?P<id>\\d+)' // the full argument (parathenses are to group it)
'?P' // denotes that this is a parameter
'<id>' // the name of the parameter
'\\d+' // indicates the paramter should be an integer
Enter fullscreen mode Exit fullscreen mode

While regular expression are hard to read, this one takes care of a couple things we’ll cover in this article:

  • The route will not run if the id is missing (in our case, the original rest route will run, which can be intentional)
  • An error will be sent back if the id is not an integer (although it will just say the route doesn’t exist)

There are also a few of things this style won’t do:

  • The route won’t send a proper error back if the type is wrong (what if we want to let the user know they need to send an integer versus an ambiguous error about the route not existing)
  • The data won’t be sanitized in any custom way (for example: the ID needs to be less than 10)
  • We can’t pass in a default value

Adding additional checks for the argument:

To add the features described above, all we need to do is add an argument called args to our method:

register_rest_route(
    'ndx/v1',
    'my-endpoint/(?P<id>\\d+)',
    array(
        array(
            'methods' => WP_REST_Server::READABLE,
            'callback' => 'callback_function_with_id',
            'permission_callback' => '__return_true',
            'args' => array(
                'id' => array(
                        // parameters go here
                )
            )
        )
    )
);
Enter fullscreen mode Exit fullscreen mode

The args argument is a keyed array, with each key corresponding to the parameter. The key values are also an array with 4 options:

  • default: default value if the parameter is missing
  • required: set the parameter to be required or not
  • validate_callback: a function to validate something about the parameter. Returns true or false, and will send a formatted error back if false.
  • sanitize_callback: a function to sanitize the data before sending it to the callback

What’s interesting about these options is that, because of how we defined our parameter in our route name, we already have done most of this work:

  • the parameter is required
  • the parameter must be an integer

For testing, let’s change our route to pass in as many data types as possible:

register_rest_route(
    'ndx/v1',
    'my-endpoint/(?P<id>[a-zA-Z0-9_-]+)',
    array(
        array(
            'methods' => WP_REST_Server::READABLE,
            'callback' => 'callback_function_with_id',
            'permission_callback' => '__return_true',
            'args' => array(
                'id' => array(
                )
            )
        )
    )
);
Enter fullscreen mode Exit fullscreen mode

We now have a new regex expression (?P<id>[a-zA-Z0-9_-]+) which lets us pass in strings or numbers. Next, let’s add all of our available arguments into the args array:

register_rest_route(
    'ndx/v1',
    'my-endpoint/(?P<id>[a-zA-Z0-9_-]+)',
    array(
        array(
            'methods' => WP_REST_Server::READABLE,
            'callback' => 'callback_function_with_id',
            'permission_callback' => '__return_true',
            'args' => array(
                'id' => array(
                    // NEW CODE HERE
                    'default' => 0,
                    'required' => true,
                    'validate_callback' => function($value, $request, $key) {
                        return true;
                    },
                    'sanitize_callback' => function($value, $request, $param) {
                        return $value;
                    }
                )
            )
        )
    )
);
Enter fullscreen mode Exit fullscreen mode

The sample above has been coded to basically be useless. Validation always returns true and sanitization just returns the value untouched. Let’s break down each argument:

Default value

The default argument provides a default value if none is passed. Because we are encoding the parameter as part of the route name, this code will never be called. Not providing a value in the URL will either return an error that the route does not exist, or call another endpoint with the same name that does not have a parameter attached to the end (in our example, we have my-endpoint and my-endpoing/<id>.

Requiring a value

The required argument lets you defined an argument as required or not. Again, because we are encoding the parameter as part of the route name, this code will never be called.

Validation

Validating parameters is a great way to quickly check a parameter and say that it is either valid (true) or not valid (false). You only return true or false in validate_callback. Here is an example where an id greater than 10 will be considered invalid:

'validate_callback' => function($value, $request, $param) {
    return $value < 10;
}
Enter fullscreen mode Exit fullscreen mode

Sanitization

Sanitization of parameters is different from validation because we return the value back in some form. Note that sanitize_callback and validate_callback do not get called in any specific order, and are simply additional filters to ensure whatever data passed in fits the logic of the original callback function. In our example, let’s remove negative numbers:

'sanitize_callback' => function($value, $request, $param) {
    $integer_value = (int) $value;

    return $integer_value < 0 ? 0 : $integer_value;     
}
Enter fullscreen mode Exit fullscreen mode

Now, with our validate_callback and sanitize_callback functions we have ensured only numbers 0-10 are allowed to be passed through.

Additional arguments for quick validation and sanitization

There are a many more args for quickly validating a parameter without using function callbacks:

array(
    'type' => // array | object | integer | number | string | boolean | null
    'description' => // a description used in the API schema
    'format' => // hex-color | email | date-time | ip | uuid
    'enum' => // array of allowed values
    'minimum' => // minimum integer value (inclusive)
    'maximum' => // maximum integer value (inclusive)
    'exclusiveMinimum' => // minimum integer value (exclusive)
    'exclusiveMaximum' => // maximum integer value (exclusive)
);
Enter fullscreen mode Exit fullscreen mode

Note: the format option requires the type to be defined as a string

Wrap up

Ensuring user input to any function, method or API should always be verified before taking action. While you can do all of the above in the full callback function for your route, it’s preferred to separate this out and prevent the callback from ever being fired if something is wrong.

I also want to make sure it’s stressed that you can (and should) create as many rules for any parameters used in your callback, not just the ones that might be defined as part of your endpoint name.

Author

Top comments (0)