Sanitize Early, Escape Late, Always Validate
If you’re working with WordPress for a while, I am sure you’ve heard about such words like “sanitizing” or “escaping” or at least have seen in the code sanitize_text_field()
, esc_html()
or similar functions.
This tutorial is intended to sort this whole data validation thing out once and forever.
The whole tutorial is based around one simple rule: “Trust nothing and no one”.
In other words – if you get data from anywhere, it may always contain some malicious code, that could break your website or at least some of its pages. Even if you get this data from your own database.
Data Sanitization
Sanitization – is a process of securing user input. It is kind of more liberal of an approach to accepting user data than validation.
Let me show two examples here. The first one – is an example of SQL-injection, when not securing user input may lead to a disaster. The second one is not so serious, but also shouldn’t be left without attention.
SQL injection example
Not so long ago I found an interesting PHP code in the website of one of my new clients.
$price_str = "_price BETWEEN " . $price_min . " AND " . $price_max;
...
$sql = "SELECT ID, title FROM `" . $table_name . "`" . $cat_str . $param_str . $price_str;
$results = $wpdb->get_results( $sql );
You might say – so, what? Definitely there is absint( $price_min )
or something.
Do not be so sure! A little bit above in the code we have:
parse_str( $_POST['data'], $form_data );
...
$price_min = $form_data['price_min'];
As you might’ve guessed we have some kind of a product filter here. The good news is that if we have $wpdb->get_results()
used, attacker couldn’t do something crazy like this
100 AND 200; DROP TABLE wp_users; --
Because multi-queries are not supported by WordPress $wpdb. In order to make it happen your website code should use mysql_multi_query
or mysqli_multi_query
PHP-functions.
Anyway, let’s try to inject is somehow. At first it can be easily checked if there is an SQL injection with:
100-SLEEP(5)
If there is some page loading delay, then it is a green light! Now we can inject this query.
5 UNION SELECT user_pass, user_email FROM wp_users WHERE ID=1 --
Using this technique it is possible to extract all of the information from the wp_users
table. Depending on a server configuration you can even export user data into a text file with INTO OUTFILE '/path/to/file.txt'
.
Unexpected HTML in user input example
That’s a simple one. Let’s assume we have any <input type="text" />
or <textarea>
fields. What do you think will happen if user inputs HTML inside? Left unsanitized this HTML would appear in database. For example if we’re just using WordPress functon:
update_user_meta( $user_id, 'some_key', $_POST[ 'my-field' ] );
And then, if escaping data is also ignored, we may come to this.
So, for this specific purpose sanitize_textarea_field()
should be perfect. I completely understand that in some cases you need HTML, then you can feel free to use wp_kses()
.
Sanitization WordPress functions and process
As you remember from this tutorial title “Sanitize early”, that means that we have to clean up user input as soon as we get it.
I like doing it this way:
// clean user input first
$number = ! empty( $_POST[ 'number' ] ) ? absint( $_POST[ 'number' ] ) : 0;
$text = ! empty( $_POST[ 'text' ] ) ? sanitize_text_field( $_POST[ 'text' ] ) : '';
// now feel free to update metadata in database
update_user_meta( $user_id, 'number', $number );
update_user_meta( $user_id, 'text', $text );
There are a lot of sanitization functions, some of them are just used by WordPress code and we may never need them. Below is my go-to list:
Function | Description |
---|---|
absint() | For positive integers. |
sanitize_text_field() | For text fields. |
sanitize_textarea_field() | For textarea fields where we need to save line breaks. |
sanitize_email() | For emails. |
sanitize_title() | For URL slugs. |
wp_kses() | This function allows you to set a list of allowed HTML tags and their attributes. |
esc_url_raw() | For URLs. |
Also want to show you an example how you should never do. I would like to highlight it, because it is very commonly used, even in WordPress tutorials, lol.
// NEVER DO THAT
update_user_meta( $user_id, 'text', esc_attr( $_POST[ 'text' ] ) );
Using this code will lead even to two issues:
- There is no check if
$_POST[ 'text' ]
is actually provided, so the PHP notice may appear “Notice: Undefined index…”. - Once we used
esc_attr()
before saving out data into database, the text will be prepared to use in HTML attribute, but then, when we get this data from the database, it for example may be double-escaped.
In some cases also you don’t even need use any of the mentioned functions, it is when you’re using <select>
or <input type="radio">
fields. In this case it is better to just check whether the value provided is among the possible values of the field. For example:
// that's the array of possible values
$possible_values = array( 'value-1', 'value-2', ... );
$value = isset( $_POST[ 'field' ] ) && in_array( $_POST[ 'field' ], $possible_values ) ? $_POST[ 'field' ] : '';
Or for multiple values:
// that's the array of possible values
$possible_values = array( 'value-1', 'value-2', ... );
$value = isset( $_POST[ 'field' ] ) && is_array( $_POST[ 'field' ] ) ? array_filter( $_POST[ 'field' ], function( $el ) { return in_array( $el, $possible_values ); } ) : array();
Securing database input
Now let’s come back to this example and do some improvements in it. How to protect it?
Well, when running WordPress SQL queries like $wpdb->query()
, $wpdb->get_col()
, $wpdb->get_var()
, $wpdb->get_row()
, $wpdb->get_results()
you have to always wrap them into $wpdb->prepare()
. How to do it we are going to talk in just a little bit.
On the other hand, $wpdb->insert()
, $wpdb->update()
, $wpdb->replace()
, $wpdb->delete()
shouldn’t be wrapped with $wpdb->prepare()
because it is already inside them.
I know that we don’t see the whole query here, so it is difficult to understand the bigger picture. But on the base of what we have I made a query with $wpdb->prepare()
like this.
$results = $wpdb->get_results(
$wpdb->prepare(
"
SELECT ID, title FROM `" . $table_name . "`" . $cat_str . $param_str . "_price BETWEEN %d AND %d
",
$price_min,
$price_max
)
);
$wpdb->prepare()
may remind you PHP sprintf()
function. Our string has some placeholders in it that start with %
sign. And each placeholder is going to be treated (sanitized) specific way, for example %d
is for integers, %f
– float, %s
– string.
Data Validation
Validation – it is kind of a more strict way of securing user data than sanitizing it. Actually we’ve already done some validation in this tutorial a little bit above.
The validation can and should be used for:
- Check that the fields are not empty to prevent PHP warnings.
- Check that required fields have not been left blank.
- Check that specific type of fields are contain the valid data, for example an email, a phone number and a post code fields.
- Check that a quantity field is greater than 0 and so on…
My go-to functions here are isset()
, empty()
, preg_match()
, strpos()
and maybe is_email()
. For example let’s check that a name field does not contain numbers:
if(
isset( $_POST[ 'first_name' ] )
&& $_POST[ 'first_name' ]
&& ! preg_match( '/\\d/', $_POST[ 'first_name' ]
) {
// validation passed
}
It is actually a real WooCommerce example, you can find it here.
Data Escaping
Escaping (securing output) – it is when we have to process our data before printing it on an HTML page. In order to show you why we actually need, please check the example below.
XSS attack example
When we are talking about escaping it is usually about what we get from a database. But remember, that the database is not a trusted data source. Just let’s assume we’re printing HTML like this:
<label for="<?php echo $id ?>"><?php echo $label ?></label>
It seems very simple and clear, doesn’t look like we have some vulnerability here, does it?
Our next assumption is that $label
is coming directly from the database and contains something like <script>window.location = "https://rudrastyh.com"</script>
. Instead of displaying the label of a field, your website users will be redirected to my website ;) Looks good? Only for me, but neither for you nor your users. You might say that a redirect doesn’t look like something very dangerous, but it is just the tip of the iceberg, for example the same way your cookies can be stolen and somebody will log in as an administrator.
The same thing applies to $id
variable which is inside the HTML attribute. The code will be a little bit different though "><script>window.location = "URL"</script>
. Did you notice that here we have symbols ">
to close the HTML attribute and tag first?
To prevent that kind of things from happening in the above example, all we have to do is to wrap the output in esc_attr()
and esc_html()
accordingly. Here is how:
<label for="<?php echo esc_attr( $id ) ?>"><?php echo esc_html( $label ) ?></label>
When to Escape Data?
The question here is do we need to escape always or there are some cases when we do not need it? That’s a very good question, because in official WordPress documentation it is said, that some of WordPress functions take care of preparing the data for output and as an example they mention the_title()
function.
Let’s check it now! I am not even saying about changing the title in phpMyAdmin, which is also possible of course, so let’s create a post with the title like this:

And on the website pages where the title is going to be printed either with the_title()
or get_the_title()
, we got this:

But look, WP_Posts_List_Table in /wp-admin is not broken, although it uses the same get_the_title()
function to print titles.

What does it mean?! 🤔
In WordPress admin pages the function get_the_title()
is escaped with esc_html()
this way:
add_filter( 'the_title', 'esc_html' );
We can come to a conclusion like:
WordPress escapes everything where it is really important and at the same time it gives the freedom to the users when we talk about a website front end.
And here is what I think – it is on your choice to decide whether to escape the titles etc or not when creating templates for your custom theme, but if you’re developing a plugin, or some kind of UI for WordPress admin, escaping is always a must.
Escaping functions
Function name | Description |
---|---|
esc_attr() | Prepares the data for the usage inside HTML attributes. |
esc_html() | Prepares the text for its usage inside HTML. |
esc_url() | Checks, tries to fix and cleans URLs. |
esc_js() | Escapes a string for its usage as an inline JavaScript, like onclick="" , onsubmit="" or inside a <script> tag. Please note, the text strings in JavaScript in this case must be always wrapped in single quotes! |
esc_textarea() | Prepares a string for the usage inside a <textarea> tag. |
Please also keep in mind:
- Do NOT use
esc_attr()
to escape data forsrc
,href
attributes – useesc_url()
instead. - Do NOT use it for
value
attributes as well, because it could lead to lost HTML entities and incorrect values stored in database, useesc_textarea()
instead. It is becauseesc_attr()
doesn’t double encode entities.
Escaping with localization
Can we trust translation files? Of course no.
So, it is also worth mentioning a couple of localization functions like esc_html__()
, esc_html_e()
, esc_html_x()
, esc_attr__()
, esc_attr_e()
, esc_attr_x()
which are not only translate strings but also escape them.
Example:
esc_html_e( 'Hello World', 'some_text_domain' );
// absolutely the same
echo esc_html( __( 'Hello World', 'some_text_domain' ) );

Misha Rudrastyh
Hey guys and welcome to my website. For more than 10 years I've been doing my best to share with you some superb WordPress guides and tips for free.
Need some developer help? Contact me
Hi, thanks for the lovely articles. As a professional WordPress developer and lead Belgian WordCamp organizer it’s always a pleasure to meet people like you. Keep up posting these great tutorial articles ;-)
Hi,
Thank you so much! 🙃 I hope to visit a Belgian WordCamp one day!
Great article Misha.
Been a fan following your easy to understand and concise articles.
In your (and in lot many other) example of
wpdb->prepare
why is that$table_name
is not interpolated with placeholders same way as other params?referring to this
Thank you Sameer,
It is because
$table_name
is hardcoded somewhere in the code. It is not a part of user input in any way.