Secure Code Guide
Using Input from Superglobals
When taking in input from superglobal variables such as $_GET, $_POST, $_SERVER, or $_REQUEST, you should wrap it around a sanitization function. WordPress provides several such functions. This is also a good time to force the variable's type (for example, if we are retrieving a numeric ID, that it is cast as an integer).
A few of the most common sanitization functions WordPress provides are:
- sanitize_text_field( $value ) This can be used for processing most strings and numbers.
- sanitize_textarea_field( $value ) This can be used for processing the value of a textarea input, which preserves line breaks.
- sanitize_email( $email ) This can be used for removing non-compliant characters from email addresses.
- sanitize_file_name( $value) This is very useful for returning a properly encoded filename to store a file with.
Let's look at an example of how we can take in an order ID from $_GET.
$order_id = (int)sanitize_text_field( $_GET['order_id'] );
If you are processing a string from a textarea field, you should use the sanitize_textarea_field function. This sanitizes the string while preserving textarea formatting such as line breaks.
$mytextarea_string = sanitize_textarea_field( $_POST['mytextarea'] );
Building Secure URLs for <a href=""> Tags
WordPress provides the add_query_arg function to easily add query variables to URLs for use in anchor tags (<a href=""></a>), and elsewhere.
Query variable values should always be encoded using the urlencode PHP function. This ensures unencoded characters are converted or removed.
Instead of writing:
$my_url = 'https://photoreal.io/agent-order-details?order_id=' . $order_id . '&user_id=' . $user_id;
You can write:
$my_url = add_query_arg( array(
'order_id' => urlencode( $order_id ),
'user_id' => urlencode( $user_id )
), 'https://photoreal.io' );
The first argument for add_query_arg is the array of query vars and their respective values. The second argument is the base URL that the query vars should be appended to. add_query_arg does all the magic of adding the delimiters when building the URL, which is very handy.
When outputting a URL, always wrap it in the esc_url function. This ensures that characters in the URL are properly escaped when output to the browser client-side. Here is an example:
<a href="<?php echo esc_url( $my_url ); ?>">Click me!</a>
Escaping Variables for Display
It is important to escape variables for display to prevent XSS (cross-site scripting) attacks. Whenever you echo a variable for display to the end-user, always wrap them with an escape function.
For displaying text:
<p><?php echo esc_html( $my_var_to_display ); ?></p>
For outputting values for input attributes, use esc_attr.
<input name="my_text" type="text" value="<?php echo esc_attr( $my_text_value ); ?>" />
For outputting a string into a textarea to preserve line breaks, use esc_textarea.
<textarea name="my_textarea"><?php echo esc_textarea( $my_textarea_value ); ?></textarea>
Secure WPDB Queries
When making MySQL queries using $wpdb, it is important that any query requiring a variable fed to it should be prepared, rather than directly injected into the query string.
For example, instead of writing this:
$my_results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}posts WHERE ID={$order_id} AND post_type='photo-order'");
Write this:
$my_results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}posts WHERE ID=%d AND post_type='photo-order'",
array(
$order_id
)
)
);
The reason is $wpdb->prepare cleans the variables passed in the MySQL query string. You can denote the type of the parameter you are passing by specifying a placeholder. In the example above, I use %d to specify that ID should be an integer. Here are the different type placeholders you can use, depending on the MySQL column type.
| MySQL Data Type | wpdb::prepare Placeholder | Notes |
|---|---|---|
| TINYINT, SMALLINT, INT | %d | For integer types. |
| BIGINT | %d | For big integers, though might overflow in PHP; use with caution. |
| FLOAT, DOUBLE, DECIMAL | %f | For floating-point numbers. |
| CHAR, VARCHAR, TEXT | %s | Strings need to be escaped to prevent SQL injection. |
| DATE, DATETIME | %s | Although they technically hold dates, they are stored as strings. |
| TIME | %s | Time is also stored as a string in MySQL. |
| ENUM | %s | Enums are treated as strings. |
| SET | %s | Sets are treated like strings where each option is comma-separated. |
| BINARY, VARBINARY | %s | Binary data should be escaped properly. |
| BLOB, LONGBLOB, MEDIUMBLOB, TINYBLOB | %s | Binary Large OBjects, treated as strings for escaping purposes. |
| BIT | %s | Bit fields are treated as strings. |
Also note that you can define multiple parameters by adding placeholders where you would otherwise inject variables directly. You would define the actual variables in the same order in the array that comes after your query string in your $wpdb->prepare call.
You don't need to worry about using a placeholder for $wpdb->prefix, as it is not user input. For any variables that do not rely on user input, it is safe to directly inject the variables. It is just with user input that it is critical to use $wpdb->prepare so the values can be sanitized to prevent MySQL injections.
