Note: The following article is an extract from my guide on creating secure WordPress plugins. As well as XSS, it will contain advice on avoiding SQL injection, CSRF and other vulnerabilities. You can get hold of it here: WordPress Plugin Security Handbook.

Cross Site Scripting (XSS)

The most common vulnerability found in WordPress related code is Cross Site Scripting (XSS).

XSS flaws occur whenever an application takes untrusted data and sends it to a web browser without proper validation or escaping.

XSS allows attackers to execute scripts in the victim’s browser which can hijack user sessions, deface web sites, or redirect the user to malicious sites.

There are two main types of XSS:

Persistent (or Stored) Reflected

With persistent XSS, the vulnerable code will be stored server side, either in a database on on the file system, and then surfaced when a user visits a page.

With reflected XSS, an attacker crafts a specially formatted URL which is intended to cause harm once a user clicks on it.

Reflected XSS is less dangerous because it relies on an attacker convincing the victim to click on the specially crafted URL.

Both types of XSS are caused when a WordPress plugin trusts user-supplied input.

Where could user input come from?

GET or POST parameters in forms (for example, your admin page)

Cookies

HTTP request headers

Key lesson: Do not trust user input.

How to prevent XSS?

Whenever you are outputting user-supplied data, make sure it is properly escaped. When you’re building a user interface, at the last moment before untrusted data is dynamically added to HTML, escape it.

Let’s look at some examples of XSS vulnerabilities that have affected plugins, and have subsequently been fixed to see what we can learn.

XSS Persistent Example

Let’s take a look at an example of an XSS persistent vulnerability.

WP-Stats

The vulnerable Plugin is called WP-Stats (version <= 2.51) In this example, the plugin has requested a URL from an admin user on the admin screen. But before displaying it, it should be escaped with esc_url to prevent XSS. Before change, we can see an option called “stats_url” being echoed out within the href attribute, after being stripped of slashes. [php] echo '<li><a href="'.stripslashes(get_option('stats_url')).'">'.__('My Blog Statistics', 'wp-stats').'</a></li>'."

"; [/php] After the change, we can see the URL being run through the escaping method called “esc_url”. [php] echo '<li><a href="'.esc_url( get_option( 'stats_url' ) ).'">'.__('My Blog Statistics', 'wp-stats').'</a></li>'."

"; [/php] Also, within the same plugin we see another XSS vulnerability: [html] <form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>?page=<?php echo plugin_basename(__FILE__); ?>"> [/html] In this case, the PHP_SELF server super global is being echoed without being escaped. This is dangerous because a malicious user could inject dangerous code into the action just via a specially crafted URL. Example:

http://wordpress/form.php%E2%80%9D%3E%3Cscript%3Ealert%28%E2%80%98Vulnerable%E2%80%99%29%3B%3C%2Fscript%3E

Would lead to the HTML form tag looking like this:

<form method=“post” action=“/form.php”><script>alert(‘Vulnerable’);</script>?page=…

After the fix:

<form method="post" action="<?php echo admin_url( 'admin.php?page='.plugin_basename( __FILE__ ) ); ?>">

Key lesson: Don’t echo PHP_SELF into form action attributes. Use esc_url or as above, admin_url if the page is an administration page.

Example XSS (Reflected)

QTranslate

QTranslate is a plugin for helping manage language translation throughout your site.

The edit GET parameter is not sanitised before being show to a user. A specially craft URL like the one below can be used to inject malicious code into the page.

http://wordpress/wp-admin/options-general.php?page=qtranslate&edit=%22%3E%3Cscript%3Ealert%28%2FVulnerable%2F%29%3B%3C%2Fscript%3E

To prevent this, the esc_html function should be used before echoing out the contents of “edit” parameter to the user on the options-general.php page.

YOP Poll

YOP Poll allows you to add surveys to your site.

Echoing out user-supplied data in Javascript:

function close_window() { var yop_poll_various_config = new Object(); yop_poll_various_config.poll_id = '<?php echo yop_poll_base64_decode( $_GET['poll_id'] ) ?>';

Fixed version:

yop_poll_various_config.poll_id = '<?php echo esc_js(yop_poll_base64_decode( $_GET['poll_id'] )) ?>';

Key lessons

Encode data before use in a parser ( JS, CSS , XML )

You are writing data into HTML attributes, use:

esc_attr

You are writing data into HTML, use:

esc_html

You are writing user supplied data into JavaScript, use:

esc_js

You are asking a user for a URL and writing it into HTML, use:

esc_url_raw / esc_url

References

https://codex.wordpress.org/Data_Validation#Output_Sanitization

https://codex.wordpress.org/Function_Reference/esc_html

https://codex.wordpress.org/Function_Reference/esc_attr

https://codex.wordpress.org/Function_Reference/esc_js

https://codex.wordpress.org/Function_Reference/esc_url

Further reading

WordPress Plugin Security Handbook