Security Advisory: Stored XSS in Jetpack

Security Risk: Dangerous

Exploitation Level: Easy/Remote

DREAD Score: 8/10

Vulnerability: Stored XSS

Patched Version: 3.7.1

During a routine audit for our WAF, we discovered a critical stored XSS affecting the Jetpack WordPress plugin, one of the most popular plugins in the WordPress ecosystem.

Vulnerability Disclosure Timeline:

  • September 10th, 2015 – Initial report to Automattic security team
  • September 10th, 2015 – Automattic security team acks receipt of report, sets patch date for September 22nd
  • September 28th, 2015 – Patch made public with the release of Jetpack 3.7.1 and 3.7.2
  • October 1st, 2015 – Sucuri Public Disclosure of Vulnerability

Are You At Risk?

The vulnerability affects users of Jetpack version lower or equal to 3.7 that uses the contact form module present in the plugin (it is activated by default). An attacker can exploit this issue by providing a specially crafted malicious email address in one of the site’s contact form pages. As the email is not sanitized properly before being output on the ‘Feedback’ administrative section, the attacker could use this bug and a bit of web browser hackery to execute JavaScript code on the administrator’s end, allowing them to do whatever they wants with the site (hiding a backdoor for future exploitation of the hacked site, injecting SEO spam, etc.).

Technical Details

We first noticed the XSS in Jetpack while checking how the plugin managed emails that were sent through pages that contained a contact form:

The ’email’ value being filtered
The ’email’ value being filtered

At first, It looked okay. It’s passing the email address through the same filter (pre_comment_author_email) that WordPress uses for sanitizing emails that are submitted to its comment component. Plus, it’s passing the resulting string to a custom strip_tags() method that does the following:

The strip_tags method
The strip_tags method

It passes the string to wp_kses() with an empty array as its second parameter, which prevents us from injecting any HTML tags in the email address. Keeping these constraints in mind, we went forward and tried to insert dummy characters, until we got this result when visiting the Feedback section of the administration panel:

We were able to insert an unsanitized single quote in a single quoted href attribute. In a regular pentesting scenario, this generally means we have a XSS attack vector, as we could insert a payload like the following to inject malicious script:

foo' onmouseover='alert(1);//

However, we got different results when trying this payload:



It appears the module verified the format of our email address both client-side and server-side. So we dove deeper into how things were being handled on the server. We found that the script would validate our email address using the WordPress is_email() function before saving it in the database. We knew from past investigations that this function allowed characters that can be dangerous in certain contexts, notably in the local part of the address (before the ‘@’ symbol):



That provides us with a good list of characters to use, but we still had another challenge: ‘(‘, ‘)’, ‘:’,’;’ and the space character are blocked by this regular expression, and they’re somewhat needed in order to craft a useful malicious payload. The first issue came when trying to separate our closing href from the onmouseover event we’re trying to inject, something we easily managed to do by inserting slashes (‘/’) between the attributes, which acts like a separator in most browsers. So the beginning of our payload would look like:


The last issue we had to get rid of was the fact we couldn’t insert any parenthesis and semicolons. The first idea we had was to insert HTML entities inside the onmouseover attribute, but that led to another issue: HTML entities ends with semicolons.


We had to have a look at Mozilla’s HTML5 Tokenizer to find something that would help us overcome this hurdle, using decimal HTML entities (A for example).

Simplified, the above snippet does the following:

  1. Knowing that we look at a decimal HTML entitiy, check if the current byte we’re analyzing has a value between ‘0′ and ‘9’ (essentially, a decimal number) and if it is, add its value to the ‘value‘ variable.
  2. Else, if we’re staring at a semicolon, do one of the following:
    1. If we stored decimals before, go to NS_HTML5TOKENIZER_HANDLE_NCR_VALUE, where we will convert the ‘value’ variable to its corresponding character.
    2. Else, if this is not an entity, we’re just ignoring it.
  3. Otherwise, if we didn’t meet a semicolon and we didn’t parse a decimal number, do one of the following
    1. If we didn’t store decimals in the ‘value’ variable before, do the same thing as step 2.2 and ignore this corrupted entity.
    2. If we did, then do as in step 2.1, go to NS_HTML5TOKENIZER_HANDLE_NCR_VALUE and throw a debugging error message notifying the developer he messed up.

This means that if instead of writing a semicolon, for example, we wrote it’s decimal entity counterpart ; and switched the ending semicolon with something else like &#59/**/, it would work too, and we would successfully circumvent the constraints is_email() gives us, as we use only allowed characters.


From this point, the attacker has a full blown stored XSS attack vector to compromise the security of this site’s users even further, something you definitely don’t want to see happen.

Update as Soon as Possible

There are a couple of things to bear in mind when thinking about this vulnerability. Cross-Site Scripting (XSS) vulnerabilities come in various flavors, some more severe than others. In most instances, the vulnerabilities you hear attributed to XSS are what you’d call reflective XSS vulnerabilities. This is where the user can cause something to function on the browser via a carefully crafted URL string, but it’s happening on the pre-verbal surface of the site.

In this case however, we’re talking a Stored XSS vulnerability; meaning a visitor to your website is able to pass arbitrary code to your web server and wait for someone to log into your WordPress panel. This is fundamentally different. The users logging in could be subscribers, authors or even administrators. The attackers simply has to wait for the right user to log in, without introducing any additional indicators that might alert the administrator (think a phishing campaign). This is compounded by the popularity of a plugin like JetPack, deployed by default in many installations via their hosts or the install packages.

This also presents readers with an example that illustrates how with enough time and energy, bugs can be identified and exploited for malicious intent. In the attackers mind, the goal is to own one, own them all. Attacking extensions to CMS applications like Jetpack (which has an install base of over 1 million active installs) is a gold mine for any attacker. It’s important you take the time to update. If for whatever reason you can’t, we encourage you to to leverage a Security Stack that provides you proactive defenses against not only known issues, but unknown as well. We encourage you to look at protection platforms like a Website Firewall or equivalent technology that provides some form of virtual patching and hardening to help stay of these emerging threats.

  1. Does the WP blog have to be using the Contact Form in the site, or just by having Jetpack installed and the contact option enabled is good enough to allow the XSS vulnerability to be used? Trying to determine impact here, as most blogs do not use the Contact Form option in Jetpack as far as I can tell…

    1. When inspecting the POST request it has a referring url and form ID number in the POST args. If I messed with those, the request would fail. It’s not to say that there has to be a form present, but it would likely be much harder to exploit in the absence of one.

  2. “During a routine audit for our WAF”

    Does that mean it was being exploited when you discovered it?

  3. Thank you for this, along w/ the details. So many sites having Jetpack by default, even if they’re not enabled… To clarify, is there still vulnerability even if the plugin isn’t active?

    1. If the plugin is disabled, you cannot access the “feedback” tab in wp-admin. In that case even if XSS were stored, the browser wouldn’t/couldn’t execute it without being able to view that page.

  4. In the event somebody managed to use the vulnerability before JetPack could be updated to the latest version – how would one check to assure everything is okay? In other words – are there any tell tale signs that an installation was hacked/messed with before the “fix”?

Comments are closed.

You May Also Like