If you’re a WordPress developer that writes HTML/CSS/JS (which is 100% of theme developers and 99% of plugin developers), you need to know the basics of front end security for WordPress. WordPress gives you all the tools you need to make your theme or plugin secure. You just need to know how and when to use each tool.
For example, one huge responsibility you have as a front-end developer is to prevent unescaped content from being printed to the page.
Security Is Your Responsibility
Before we talk specifics, I’d like to squash an idea that I would (previously) use to rationalize a “less strict” view of front end security:
“If a hacker is already able to change the code/database/content, what difference does it make if my front-end code is secure? Couldn’t they already do more substantial damage?”
Yes, if a hacker already has control of the code or database, chances are they don’t care how well-secured the output is (they can probably change it anyway).
Even so, you still have a responsibility to make the front-end code secure. A few reasons why:
- Proper front end security also prevents user error from causing big problems. If the user accidentally puts a special character in a field they weren’t supposed to, the page won’t explode.
- Some attacks are limited. Perhaps the hacker had minimal control and could only change a single piece of content in the database. You can prevent that narrow attack from becoming much larger.
- Security is an onion. The outermost (often inedible) layer is the front-end display. Good security practices make it that tiny bit harder for a hacker to exploit a site.
Enough with the pep talk. Let’s talk about securing our WordPress themes and plugins.
What Are We Worried About?
A major concern for a front end developer is avoiding Cross-Site Scripting (XSS for short, because ‘CSS’ would cause all sorts of problems. Puns and one-liners welcome.).
XSS vulnerabilities allow bad people (“hackers”) to steal information. Typically that means stealing cookies and session information. If a bad person can send your active session back to themselves (hence the “cross site” part), they could use that to be logged in to your account and do anything you could do (bad news bears).
What is Escaping?
In computer science the word “escape” has a special meaning:
To convert a character or string of characters to be interpreted literally within a specific context, typically to prevent those characters from being interpreted as code.
Put another way, in the context of WordPress front-end development: Escaping changes possibly evil content into safe content.
The biggest danger here is generally
<script> can be output on the page an executed, that’s super dangerous. Escaping means turning that into <script> which is safe.
Why You Need To Escape: An Example
In case you’re not familiar with XSS, a quick example will demonstrate the risk.
Suppose your theme had a field to add a link for further reading. A “Read More” link, if you will. On the WordPress dashboard, it might look like this:
In your theme, you’d like to display this link at the bottom of the post, like so:
So, you open up
single.php (the file responsible for displaying blog posts) and add the following near the bottom:
<?php $read_more_link = get_post_meta( get_the_ID(), 'read_more_link', true ); ?> <!-- Don't do this --> <a href="<?php echo $read_more_link; ?>">Read More</a>
Suppose someone maliciously enters this text into your custom field:
When you visit the page, here’s what you’ll see:
- Redirect the user
- Hijack the user’s cookies
- Do other bad things
An Escape for Everything
Now that we know how important escaping is, let’s look at the escaping methods WordPress provides and give some context for each.
Used for: Output that should have absolutely no HTML in the output.
What it does: Converts HTML special characters (such as
&) into their “escaped” entity (
A common example would be displaying text-only custom fields in a theme:
<?php $dog_name = get_post_meta( $post_id, 'dog_name', true ); ?> <span class="dog-name"><?php echo esc_html( $dog_name ); ?></span>
Used for: Output being used in the context of an HTML attribute (think “title”, “data-” fields, “alt” text).
What it does: The exact same thing as
esc_html. The only difference is that different WordPress filters are applied to each function.
esc_attr used on an image:
<img src="/images/duck.png" alt="<?php echo esc_attr( $alt_text ); ?>" title="<?php echo esc_attr( $title ); ?>" >
Used for: Output that is necessarily a URL. Examples would be image src attributes and href values.
What it does: A more thorough, specific escaping than the
esc_html functions, which removes or converts any characters not allowed in URLs into their URL-safe format.
esc_url when you need to output a link or dynamic image path:
<a href="<?php echo esc_url( $url ); ?>">Link Text</a> <img src="<?php echo esc_url( $image_url ); ?>" >
What it does: Converts special characters like
esc_js is probably the least used of the esc_ functions, for a few reasons:
- Most JS is loaded in a separate file.
- Most JS isn’t written inline as attributes.
- For non-inline JS,
json_encodeis a better option.
But, if you ever need to escape some inline JS, here’s how you’d do it:
<?php $message = get_post_meta( get_the_ID(), 'onclick_message', true ); ?> <a href="/blog/" onclick="alert('<?php echo esc_js( $message ); ?>')">...</a>
Useful in particular for
<script> blocks making use of WP variables. A simple example:
<?php $categories = get_categories(); ?>
var allCategories = <?php echo json_encode( $categories ); ?>;
// Do something interesting with categories here
Used for: Output that needs to allow some HTML, but not all tags or attributes.
What it does: Strips the content of any tags or attributes that don’t match the list of rules passed in.
wp_kses if there is a context that allows certain tags (e.g. inline formatting tags like <strong> and <em>) to be printed as HTML.
A basic example would be displaying comments:
What are the _e and _x versions of the escaping functions (
These are convenience functions (to make your life easier), and they’re useful when printing translatable strings: text that can be changed with a WordPress translation file.
If you’re developing a theme or plugin for wide distribution (vs. a one-use client project), you’ll want to make every string internationalization-friendly. That means PHP strings that were once under your control become editable by translators – hence the need for escaping (you can’t trust anybody):
The second parameter to the
_e functions is the translation domain. It is used by translators when they write and generate their translations.
_x functions (like esc_html_x) are essentially the same as their
_e counterparts, with an added “context” argument to explain the context a word or phrase is used in. Useful for words with multiple meanings:
If you crack open the default WordPress theme (named “Twenty Fifteen” at the time of this writing), you’ll see code like this outputting the title of a post:
You’ll also see unescaped calls to
the_content like so:
You may be panicking. Why aren’t those functions escaped?! 2 reasons:
1. WordPress automatically escapes them
the_title are convenience functions – tools that make front-end development easier. As such, to make it even easier for developers,
WordPress already automatically escapes the output for
2. Context: Some Content is HTML
In the case of
the_content, the output is expected to be unescaped HTML. When the user adds a
to their content, we can assume they actually want a
output to the page – instead of the escaped version.
If you have security concerns with a user being able to add scripts to
the_content‘s output, you can use
Handy tip: If you want to use
the_title as an HTML attribute, use the_title_attibute to save yourself an escape.
the_title_attribute in action:
Escape the Things
I work with a lot of plugins and themes – commercial, free, and custom-built – and I see lots of unescaped content (and resulting XSS vulnerabilities). Hopefully, this article will equip you with the basic tools you need to make your WordPress things safer.
Did I overlook anything? Let me know in the comments!