Skip to content

The Basics of esc_html() and esc_attr() — WordPress’s “Escape on Output” Approach to XSS

Read enough WordPress theme or plugin code and you’ll notice that values are almost never echoed directly. Instead you see echo esc_html( $title ); or <a href="<?php echo esc_url( $url ); ?>">. These are WordPress’s escaping functions, the core tools for preventing XSS (cross-site scripting). This post looks at why there are several of them and, more importantly, when escaping should happen.

Note: XSS is a broad term for problems where a value shown on a page contains text that the browser interprets as HTML or script rather than as plain text. The root cause is a blurred boundary between “a value” and “the structure of the HTML.” It’s the same underlying idea as $wpdb->prepare() in the previous post, which keeps values separate from the structure of a SQL statement.

Why Escaping Is Needed

Take a search results page that displays “Search keyword: …” The keyword comes from the URL, so it’s an externally supplied value. Printing it as-is is where trouble starts.

// Not recommended (illustrative pseudo-code): outputs an external value directly
echo '<p>Search keyword: ' . $_GET['s'] . '</p>';

If $_GET['s'] contains characters like < or >, the browser reads them as HTML tags, not as text. What was meant to be a value becomes part of the page’s structure.

Escaping replaces characters that have special meaning in HTML with alternative notations (HTML entities), so the result is treated purely as displayed text. For example, < becomes &lt; and > becomes &gt;. On screen you still see a <, but the browser no longer treats it as a tag.

Why There Are Different Functions for Different Places

WordPress provides several escaping functions because which characters are “special” depends on where the value is placed.

Function Typical destination What it guards
esc_html() Text between HTML tags Converts <, >, &, etc. to entities
esc_attr() HTML attribute values (class="...") Keeps the value from breaking out of the attribute, including quotes
esc_url() href / src and other URLs Removes unsuitable schemes and tidies invalid characters
esc_js() Strings inside inline JavaScript Handles characters that would break a string literal

The same string can sit between tags, inside an attribute, or in a URL, and the browser’s idea of a “boundary” differs in each case: quotes are the boundary in an attribute, while the scheme (https:, etc.) matters in a URL. That’s why choosing the function that matches the destination is the key point. Putting an esc_html()-wrapped value into an attribute, or using esc_attr() output as a URL, leaves the protection misaligned.

Escape on Output, Not on Input

The second important idea is that escaping happens right before output, not when a value is saved. WordPress’s coding guidance sums this up as “escape late.”

The reason is that one stored value may be printed in several places: as text in one spot, as an attribute value in another, as part of a URL elsewhere. If you transform it for HTML at save time, it may get double-converted in one place or under-protected in another. Keeping the value in its original form and converting it at the moment of output, for that specific destination, makes it harder to lose track of the boundary.

Input cleanup such as sanitize_text_field() at save time has a different purpose, and neither replaces the other. Sanitizing makes a value acceptable as incoming data; escaping keeps it from being misread at its destination.

When You Want to Keep Some HTML

Sometimes you don’t want every tag treated as text. An archive page description entered in the admin, with paragraphs or emphasis, is one example. For that, WordPress has wp_kses_post(), which keeps only tags on an allow list and strips everything else. esc_html() (“escape everything”) and wp_kses_post() (“let only approved markup through”) are a choice based on how much HTML you want the output to carry.

How the wpmm-blog Theme Uses Them

Checking the files of this blog’s own theme, wpmm-blog, the escaping functions appear as follows (counts as of September 2026):

Function Occurrences in the theme Example use
esc_html() about 70 Page titles, headings, text between tags
esc_url() about 47 Favicons, images, link targets
esc_attr() about 29 Attribute values (such as an analytics ID)
esc_js() 1 A string inside an inline script
wp_kses_post() 1 Archive description (some HTML allowed)

The search results heading is an easy example. In index.php, the search keyword is printed as esc_html( get_search_query() ), so an externally supplied value is wrapped right at the point of output. Unlike the bad example above, a keyword containing < shows up on screen as plain text.

The JSON-LD structured data is output with wp_json_encode() rather than an HTML escaper. It’s a different destination, JSON, so a different function is chosen. The “match the destination” principle shows up here too.

A Quick Order of Questions

When unsure which function to use, work through these in order:

Question What to pick
Where in the HTML will the value be placed? The function for that destination (text → esc_html, attribute → esc_attr, URL → esc_url)
Should part of the HTML be kept? An allow-list function such as wp_kses_post()
When should the conversion happen? Right before output, not at save time

Summary

Escaping functions keep the boundary between “a value” and “the structure of the HTML” clear at the moment of output. There are several because each destination draws that boundary differently, and escaping happens late because one value may be used in many places. When writing or reading theme or plugin code, get in the habit of checking, for each echo, where the value ends up and whether the matching function wraps it, whether or not the value seems to come from outside. Read together with the previous $wpdb->prepare() post, it shows the same “don’t trust input” mindset taking different forms for two destinations: SQL and HTML.