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.