Skip to main content
Security is one of the biggest factors involved in any review. All code that we push into a production or staging environment must be completely secure. There are a huge number of factors that must be considered with security, but they mostly boil down into a few key things:
  • Never trust user input
    User input can never be trusted, regardless of the source. “User input” actually means anything that’s not written into the code itself, and includes things like external HTTP request data and translations.
  • Sanitise on input, escape on output
    Your database should always store a safe value, so all data coming into the database needs to be sanitised before saving. This could mean ensuring a value is a number, or that it only contains valid HTML. Sites also tend to be write-once, read-many, so moving the expensive sanitise step to when you write it can have better performance.

    This data also needs to be escaped for the relevant context on output. If you’re echoing into a HTML attribute, the code needs to be escaped for the attribute context; the same applies for text output into a HTML context. Escaping is always context-specific, so must be done at the last possible step, ideally immediately before output.
  • Check user authorisation and intent
    The capabilities required to perform a task should always be well-defined, and should be checked as early as possible. In addition, always check the user’s intent: did the user themselves trigger this action?

    The key tools in WordPress to achieve this are the roles and capabilities system, and the nonce system.

Sanitisation

Validation and sanitisation are two separate but related concepts. When validating data, you are looking for certain criteria in the data. Or simply put, you’re saying “I want the data to have this, this, and this”. Sanitisation on the other hand is about removing all the harmful elements from the data. In essence you’re saying “I don’t want the data to have this, this, and this”. But the difference is more than just conceptual. With validation, we store the data once we have verified it’s valid. If not, we discard it. With sanitisation, we take the data, and remove everything we don’t want. This means that we might change the data during the sanitisation process. So in the case of user input, it is not guaranteed that all the input is kept. So it’s important that you choose the right sanitisation functions, to keep the data intact.
Refer to the Sanitisation section of the Security Functions guide for specific sanitisation functions.

Validation

Validation is a technique to ensure that input is secure before using it in your code. When validating data, you are verifying that it corresponds to what the program needs. This only works if you have a list of criteria that you can check to determine that the data is valid.

Safelisting

The simplest validation method is safelisting. This only works when there is a precise set of possible values that the data can have. These are also sometimes called enumerated types or enums. Let’s say you have a dropdown with Options A and B. You can safelist the data by checking that the submitted value is either A or B.
if ( in_array( $my_array, [ 'A', 'B' ], true ) ) {
	return true;
}
To do this, we use the in_array() PHP function. This function returns true when the needle, the submitted value, is in the haystack, the list of possible positions. So safelisting simply means that we compare the submitted data against a list of acceptable values. This works well for controls such as checkboxes, radio buttons, selects, and dropdowns.

Qualifying data

When qualifying data, we try to find out whether it meets a precise set of criteria. An example would be validating a phone number or a hex color. You can use regular expressions to check if the data meets the criteria.
Regular expressions can seem daunting. ChatGPT is your friend.

Choosing the right qualifications

When validating data, it’s crucial that you choose the right set of qualifications, and express this correctly in the code. Imagine that you have a setting in your theme for entering a link to a Twitter profile. You want to have a valid URL for this setting, so you use the sanitize_url() WordPress function before saving to the database. sanitize_url() will check your URL and clean it up if necessary. But it won’t check if the URL is actually a Twitter profile URL. So you need to add a validation step to ensure that the URL is a Twitter profile URL.
function prefix_validate_twitter_profile_url( $url ) {
    if ( 0 !== strpos( $url, 'https://twitter.com/' ) ) {
        return;
    }

    return sanitize_url( $url );
}
Refer to the Validation section of the Security Functions guide for specific validation functions.

Escaping

Escaping is used to ensure that data is safe to be output to the browser. WordPress offers a number of escaping functions. The type of escaping function to use depends on the context in which the data is output. This is because data might be safe or unsafe depending on where it is output. Data that is perfectly safe to output between two HTML tags might not be safe to output inside of a piece of inline JavaScript. When writing code, always escape immediately before output. This is referred to as late escaping. This makes it clear when and how data is escaped, making the code easy to review and to understand. It also avoids introducing security issues by accident. If the contents of a variable are escaped first, and then later in the code output, then this code is secure. However if at a later time, then escaping is removed from the variable, then all the instances in which the variable is output are now vulnerable.

Escaping Translations

Translations are to be treated as insecure data, and therefore need to be escaped before output. WordPress offers a number of helper functions for this:
  • esc_html__(): Wrapper for esc_html( __() )
  • esc_html_e(): Wrapper for echo esc_html( __() )
  • esc_html_x(): Wrapper for echo esc_html( _x() )
  • esc_attr__(): Wrapper for esc_attr( __() )
  • esc_attr_e(): Wrapper for echo esc_attr( __() )
  • esc_attr_x(): Wrapper for echo esc_attr( _x() )
A common scenario is dealing with translations that do contain HTML. Escaping is trickier, because WordPress does not include an easy to use helper function for this. If possible, extract all HTML from translated strings:
printf(
    esc_html__( 'Fixed in version %s', 'textdomain' ),
   '<strong>' . esc_html( $version ) . '</strong>'
);
In this snippet, the translated string can be HTML-escaped. sprintf() then inserts an escaped variable wrapped in HTML tags. If this approach is not possible, wp_kses() can be used:
echo wp_kses(
   esc_html__( 'Fixed in version <strong>4.5</strong>', 'textdomain' ),
   [ 'strong' => [] ]
);
When dealing with multiple HTML tags, or HTML tags that accept attributes, the code needed to make wp_kses() work can become quite verbose. In these cases, it is better to use wp_kses_post(). This function is the same as wp_kses(), but with a predefined list of allowed tags and attributes used in regular post content.

Translations containing URLs

When it comes to URL in translations, we can be dealing with two different cases: The URL does not need to be adapted per language. In this case remove the URL from the translated string, and replace it with a placeholder. The URL needs to be translatable: Remove the URL from the translated string, and make it translatable separately. Make sure to wrap the translation function used for the URL in an esc_url() call.