08.06
Last weekend, I evaluated a couple Wordpress plugins that I wanted to add to this site. One of the plugins I loooked at was Scripts Gzip: its goal is to reduce the number of requests needed to fetch CSS/JS. As the author puts it:
It does not cache, minify, have a “PRO” support forum that costs money, keep any logs or have any fancy graphics. It only does one thing, merge+compress, and it does it relatively well.
I happened to take a look at the plugin’s code because I was curious how it was fetching the CSS/JS. However, I soon discovered a number of fairly serious security vulnerabilities. I immediately contacted the author with an explanation of what I had found. He responded quickly, acknowledged the issues, and released a new version of the plugin that addressed all of the vulnerabilities I had found.
Since a fixed version of the plugin has now been released, I feel comfortable discussing the flaws publicly. I’ll be giving a brief synopsis along with details about each vulnerability, ordered below from most to least serious.
1. Arbitrary Exposure of File Contents
This vulnerability was quite serious. It allowed an attacker to view the complete contents of any file within the Wordpress directory, even sensitive PHP files like wp-config.php.
The problem came from this function in gzip.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <?php
private function collectData($options)
{
$returnValue = array('data' => '', 'latestModified' => null);
foreach($options['files'] as $file)
{
$oldDirectory = getcwd();
$parsedURL = parse_url($file);
$file = $parsedURL['path'];
$file = preg_replace('/\.\./', '', $file); // Avoid people trying to get into directories they're not allowed into.
$parents = '../../../'; // wp-content / plugins / scripts_gzip
$realFile = $parents . $file;
if (!is_readable($realFile))
continue;
if ($options['mime_type'] == 'text/css')
{
chdir($parents);
$base = getcwd() . '/';
// Import all the files that need importing.
$contents = $this->import(array(
'contents' => '',
'base' => $base,
'file' => $file,
));
// Now replace all relative urls with ones that are relative from this directory.
$contents = $this->replaceURLs(array(
'contents' => $contents,
'base' => $base,
'parents' => $parents,
));
}
else
{
$contents = file_get_contents($realFile);
}
$returnValue['data'] .= $contents;
if ($options['mime_type'] == 'application/javascript')
$returnValue['data'] .= ";\r\n";
chdir($oldDirectory);
$returnValue['latestModified'] = max($returnValue['latestModified'], filemtime($realFile));
}
return $returnValue;
}
|
The key to understanding this vulnerability is understanding the inputs. $options is an array containing user data, including the names of files that the user has asked the script to gzip. So, we understand that lines 6-13 are sanitization/validation checks that prevent the user from trying to load files outside of the Wordpress directory or trying to load non-existent files. However, we can also see that on line 34, the file requested by the user is loaded without any other special validation. That code path is executed when the user requests that JS be gzipped.
As a concerete example, we can say that http://example.com/wordpress/wp-content/plugins/scripts-gzip/gzip.php?js=wp-config.php would load the Wordpress configuration file on a vulnerable installation.
2. “Limited” Exposure of File Contents
A less serious security vulnerability occurs if the attacker tries loading CSS instead of JS. As we can see from the code above, if the script is loading CSS it calls an import function, which is reproduced in part below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php
/**
* Import a CSS @import.
*/
private function import($options)
{
$dirname = dirname($options['file']);
if (!is_readable($dirname))
return $options['contents'];
// SECURITY: file may not be outside our WP install.
if (!str_startsWith(realpath($options['file']), $options['base']))
return $options['contents'];
// SECURITY: file may not be a php file.
if (strpos($options['file'], '.php') !== false)
return $options['contents'];
// Change the directory to the new file.
chdir($dirname);
$newContents = file_get_contents(basename($options['file']));
// The rest of the code has been omitted for space considerations
}
|
It’s clear that this code path does do some extra validation. However, we can also see that the validation is not strong enough. It employs a blacklist model (forbidding files ending in .php) rather than a whitelist model (eg: only allowing files ending in .css). Accordingly, an attacker can request potentially sensitive but not forbidden files (eg: .htaccess) that are within the Wordpress directory.
As another concerete example, we can say that http://example.com/wordpress/wp-content/plugins/scripts-gzip/gzip.php?css=.htaccess would load the .htaccess file for Wordpress, if it exists.
3. Path Disclosure
This vulnerability is the least serious of the bunch. However, a path disclosure vulnerability can be useful for hackers looking to gain more information about a target. It can also make other sorts of attacks easier to pull off. This vulnerability required an attacker to be able to write a file somewhere within the Wordpress directory (possibly if they’re allowed to upload attachments). If the file contained a line with url(‘…’) and the script were directed to parse it it as CSS, the result returned would contain the absolute path to the Wordpress directory.
Summary
All of these vulnerabilities have been resolved in the latest version of the plugin, 0.9.1. I urge anyone using Scripts Gzip to upgrade as soon as possible. I’d especially like to thank the plugin’s author, Edward Hevlund, for his speedy responses to my emails, his attentiveness to security issues, and (of course) for a wonderfully useful plugin.