Persistent Cross-Site Scripting (XSS) Vulnerability in 404 to 301
One of the things we think is important when disclosing vulnerabilities in WordPress plugins is to provide the details so that others can review those, that isn’t a view held by everyone as one WordPress security companies has been holding back details while claiming to put the WordPress community first. There are a number of reason we feel that is important, starting with the fact that we often find vulnerabilities haven’t actually been fixed, which is easy to spot and then get fixed if you can see all of the details. Another reason is that we have often seen that upon reviewing the vulnerability report someone will spot an additional security issue in the same plugin. Having the details also can allow for spotting the same type of vulnerability in other plugins. The final two came together recently for us to spot a minor persistent cross-site scripting (XSS) vulnerability in the plugin 404 to 301 and suggest further improvement to their securing user input brought in to the plugin.
The report that made us look into this was from Louis Dion-Marcil of a related persistent cross-site scripting (XSS) vulnerability. While checking over that to add to our data set we noticed that there was still a more limited issue. The original vulnerability could have allowed malicious JavaScript to run when just visiting the plugin’s admin page. From seeing a number of other reports we were aware that there is potential this type of vulnerability by creating a link that runs JavaScript, for example, “javascript:alert(“XSS”);” and found that it could be implemented in a referer user input in the plugin. The limit of that here is not only do you have click on the link, but the malicious code would be visible before clicking the link:
While that isn’t a huge threat, it would be easy to fix and in looking over the code we found that there was room for improvement over the code changes made in version 2.3.1, which fixed the previous vulnerability.
In version 2.3.0 the refer input was not sanitized when brought into the plugin in the function get_error_data() (in the file /public/class-404-to-301-public.php):
private function get_error_data() { $server = array( 'url' => 'REQUEST_URI', 'ref' => 'HTTP_REFERER', 'ua' => 'HTTP_USER_AGENT', ); $data['date'] = current_time('mysql'); $data['ip'] = $this->get_ip(); foreach ( $server as $key => $value ) { if ( ! empty( $_SERVER[ $value ] ) ) { $string = $_SERVER[ $value ]; } else { $string = ''; } $data[ $key ] = $this->get_clear_empty( $string ); } return $data; }
and not escaped on the plugin’s admin page (in the file /admin/class-404-to-301-logs.php):
339 | $ref_data = apply_filters( 'i4t3_log_list_ref_column', $this->get_empty_text('<a href="' . $item['ref'] . '" target="_blank">' . $item['ref'] . '</a>', $item['ref'] ) ); |
In 2.3.1 the value is escaped when output:
340 341 342 343 | $ref = sanitize_text_field( $item['ref'] ); // Apply filter - i4t3_log_list_ref_column $ref_data = apply_filters( 'i4t3_log_list_ref_column', $this->get_empty_text('<a href="' . $ref . '" target="_blank">' . $ref . '</a>', $ref ) ); |
The function used to escape the value sanitize_text_field() properly secure against the issue raised in the previous vulnerability, but doesn’t deal with the second since the value “javascript:alert(“XSS”);” isn’t modified by any changes made by that function. One way to fix this would be to use the esc_url() function when outputting the value as URL. The limitation of that and to the previous fix is that it would relatively easy to use the underlying value somewhere else in the code and forget that you needed to escape it. The safer option is to sanitize the value when it comes into the plugin, that way it always secure when outputting it.
After we notified the developer of the issue and the possible solutions version 2.3.3 was released, which sanitizes the input
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | private function get_error_data() { // Get request data. $url = empty( $_SERVER['REQUEST_URI'] ) ? '' : trailingslashit( esc_url( $_SERVER['REQUEST_URI'] ) ); $ref = empty( $_SERVER['HTTP_REFERER'] ) ? '' : esc_url( $_SERVER['HTTP_REFERER'] ); $ua = empty( $_SERVER['HTTP_USER_AGENT'] ) ? '' : $_SERVER['HTTP_USER_AGENT']; $data['date'] = current_time('mysql'); $data['ip'] = $this->get_ip(); $data['url'] = $this->get_clear_empty( $url ); $data['ref'] = $this->get_clear_empty( $ref ); $data['ua'] = $this->get_clear_empty( $ua ); return $data; } |
Proof of Concept
Request a page that does not exist with your web browser’s referer set to “javascript:alert(document.cookie);”.
Afterwards, when visiting the page /wp-admin/admin.php?page=i4t3-logs any available cookies to be shown in alert box when clicking on the From link of the relevant listing.
Timeline
- 8/31/2016 – Developer notified.
- 8/31/2016 – Version 2.3.3 released, which fixes the issue.