A rule of thumb when writing themes and plugins—always escape the values you output. The reason is to stop a <script> that snuck into a post or a form field from being executed as-is.
There’s only one rule to learn: choose the function based on where you’re printing.
The right answer for each output location
| Where you’re printing | Function to use | Example |
|---|---|---|
| Text on the page | esc_html() | <p><?php echo esc_html($name); ?></p> |
| A tag’s attribute value | esc_attr() | <img alt="<?php echo esc_attr($alt); ?>"> |
| URLs for href, src and the like | esc_url() | <a href="<?php echo esc_url($url); ?>"> |
| The contents of a textarea | esc_textarea() | <textarea><?php echo esc_textarea($v); ?></textarea> |
| A JavaScript string | esc_js() | For inline use (prefer wp_localize_script) |
| A value where you want to keep the HTML | wp_kses_post() | Rich text, post-content equivalents |
<?php $img = get_field('photo'); ?>
<a href="<?php echo esc_url(get_permalink()); ?>" class="card">
<img src="<?php echo esc_url($img['url']); ?>" alt="<?php echo esc_attr($img['alt']); ?>">
<h3><?php echo esc_html(get_the_title()); ?></h3>
</a>How WordPress functions handle escaping
“It goes through a function, so it’s safe” isn’t a given. Whether escaping happens differs from function to function.
| Function | Behavior |
|---|---|
the_title() | Generally output safely |
the_permalink() | Output safely as a URL |
the_content() | Passes through post-content filters (HTML allowed) |
the_field() (ACF/SCF) | Does not escape—prints the value as-is |
The get_…() family | Only returns a value—you escape it yourself |
Functions starting with get_ “return a value,” so you have to escape them yourself when you echo. Just internalizing that one point makes your code consistent.
Clean incoming data before you save it
The counterpart to escaping on output is sanitizing before you save.
| Purpose | Function |
|---|---|
| Single-line text | sanitize_text_field() |
| Multi-line text | sanitize_textarea_field() |
| Email address | sanitize_email() |
| URL | esc_url_raw() (for saving) |
| Integer | absint() / (int) |
| Slug | sanitize_title() |
| Key name | sanitize_key() |
| Body text where HTML is allowed | wp_kses_post() |
$name = sanitize_text_field($_POST['name'] ?? '');
$email = sanitize_email($_POST['email'] ?? '');
$count = absint($_POST['count'] ?? 0);Values that came from outside ($_POST, $_GET, $_COOKIE) must have their type and shape pinned down before you use them—that’s the baseline stance.
Form saving takes a set of three
Any routine that saves data, in the admin or on the front end, needs all three: nonce, capability and sanitization.
// The form side
function my_form() {
?>
<form method="post">
<?php wp_nonce_field('my_save_action', 'my_nonce'); ?>
<input type="text" name="nickname">
<button type="submit">Save</button>
</form>
<?php
}
// The receiving side
add_action('admin_post_my_save', function () {
// 1. Verify the nonce (did this come from a screen on this site?)
if (!isset($_POST['my_nonce']) || !wp_verify_nonce($_POST['my_nonce'], 'my_save_action')) {
wp_die('Invalid request');
}
// 2. Check the capability (is this person allowed to do it?)
if (!current_user_can('edit_posts')) {
wp_die('You do not have permission');
}
// 3. Clean the value, then save
update_option('my_nickname', sanitize_text_field($_POST['nickname'] ?? ''));
wp_safe_redirect(admin_url('options-general.php'));
exit;
});- Nonce—a single-use value confirming the submission came from a screen on your site
- Capability—
current_user_can()confirms “is this person allowed to do this?” - Sanitization—pins down the shape of the value
Miss any one of the three and you’ve left a hole. Checking the nonce but not the capability is especially common, so get in the habit of writing them as a set.
TipsWorking with custom fields using get_post_metaAn example of updating meta when a post is savedUse prepare when you touch the database directly
When you write your own SQL with $wpdb, always pass values through prepare().
global $wpdb;
// ❌ dangerous (this is how SQL injection happens)
$wpdb->get_results("SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'");
// ✅ hand it to a placeholder with prepare
$wpdb->get_results(
$wpdb->prepare("SELECT * FROM {$wpdb->posts} WHERE post_title = %s", $title)
);Choose between %s (string), %d (integer) and %f (float). And if WP_Query or get_posts() can do the job, using those is safest of all.
Redirect with wp_safe_redirect
wp_safe_redirect(home_url('/thanks/'));
exit;wp_safe_redirect() only sends people to URLs on your own site, which prevents “open redirects” that push visitors to an external site. Forget exit and processing continues, so always write them as a pair.
Combining with translation functions
Themes built for multiple languages have functions that combine translation with escaping.
<?php esc_html_e('Contact', 'my-theme'); ?> <!-- prints -->
<?php echo esc_attr__('Search', 'my-theme'); ?> <!-- for attributes -->They’re __() (returns a value) and _e() (prints) with esc_html and esc_attr attached. Once the naming pattern clicks, you can guess the one you need.
Summary
- Choose the function by where you’re printing—body
esc_html(), attributesesc_attr(), URLsesc_url() - Functions starting with
get_only return a value. Escape them yourself when youecho(the_field()too) - Saving routines need the set of three: nonce, capability (
current_user_can) and sanitization - If you write SQL, use
$wpdb->prepare(). Redirect withwp_safe_redirect()plusexit
Escaping is a habit with very little to memorize and a lot of payoff. Once your hands do it automatically, your safety level goes up a notch on its own.