WordPress · Report #497724
Stored XSS in Post Preview as Contributor
How it was found
Root cause
I noticed that the get_the_content() makes a preg_replace_callback after all other validation and sanitization has been performed.
function get_the_content( $more_link_text = null, $strip_teaser = false ) {
global $page, $more, $preview, $pages, $multipage;
$post = get_post();
...
if ( $preview ) // Preview fix for JavaScript bug with foreign languages.
$output = preg_replace_callback( '/\%u([0-9A-F]{4})/', '_convert_urlencoded_to_entities', $output );
return $output;
}
Any JavaScript URL encoded characters will get replaced by the result of the _convert_urlencoded_to_entities() function.
function _convert_urlencoded_to_entities( $match ) {
return '&#' . base_convert( $match[1], 16, 10 ) . ';';
}
This function replaces the URL encoded characters with a corresponding HTML entity.
This in fact leads to a bypass of esc_url().
esc_url() usually prevents payloads such as javascript:alert(1) etc. It even understands javascriptA;alert(1);. However, the payload <a href="javascript%u003Aalert(/XSS/)">text</a> would not get sanitized by esc_url(). Due to the characters being converted back to HTML entities after the sanitization, the payload works again.
Steps for replication
- Create a new blog post
- Paste the following HTML:
<a href="javascript%u003Aalert(/XSS/)">text</a> - Preview the post and click the link
I have tested this with Firefox.
Impact
If an attacker can trick an admin into previewing a post and get him to click the link, he can execute arbitrary JavaScript code in the context of the admin user.
I have demonstrated in #428019 how using already existing CSS classes the link can be turned into an invisible overlay over the entire page, which makes exploitation way more likely.