we need to validate what characters you expect in the HTTP_HOST
from SA http://drupal.org/node/324824
original report:
From: "Anthony Ferrara"
"
Hello. While at Drupal Camp, I found and confirmed a vulnerability in
Drupal 6 (and 7). Here are the detailsIn /includes/bootstrap.inc, the function conf_path is defined as such:
function conf_path($require_settings = TRUE, $reset = FALSE) {
static $conf = '';if ($conf && !$reset) {
return $conf;
}$confdir = 'sites';
$uri = explode('/', $_SERVER['SCRIPT_NAME'] ?
$_SERVER['SCRIPT_NAME'] : $_SERVER['SCRIPT_FILENAME']);
$server = explode('.', implode('.', array_reverse(explode(':',
rtrim($_SERVER['HTTP_HOST'], '.')))));
for ($i = count($uri) - 1; $i > 0; $i--) {
for ($j = count($server); $j > 0; $j--) {
$dir = implode('.', array_slice($server, -$j)) .
implode('.', array_slice($uri, 0, $i));
if (file_exists("$confdir/$dir/settings.php") ||
(!$require_settings && file_exists("$confdir/$dir"))) {
$conf = "$confdir/$dir";
return $conf;
}
}
}
$conf = "$confdir/default";
return $conf;
}Notice that it's using both HTTP_HOST and SCRIPT_NAME (and
SCRIPT_FILENAME) are used unvalidated. This provides an injection in
both. Take the following case.There are 2 Drupal installs on the same server (note, not same
account). The server uses IP based virtual hosts (at least for the
"source" of the attack). As an outsider, all I care, is that the 2
sites are on the same server, and that I can find the absolute path to
the 2nd one (think error messages as a possible source of this
information).So, at this point I have the following data
Site1: IP 192.168.1.101
Site2: absolute path /home/othersite/public_htmlNow, consider the following php code:
$f = fsockopen('192.168.1.101:80');
$headers = 'GET index.php HTTP/1.1
HOST: ../../../../../../../home/othersite/public_html/sites';Now, since it's IP based virtual hosting, the drupal site is still hit.
The host header ($_SERVER['HTTP_HOST']) now contains the sting
'../../../../../../../home/othersite/public_html/sites';This line is where the vulnerability is:
$server = explode('.', implode('.', array_reverse(explode(':',
rtrim($_SERVER['HTTP_HOST'], '.')))));Let's step through the variable at each stage
1: $_SERVER['HTTP_HOST'] =
'../../../../../../../home/othersite/public_html/sites';
2. rtrim(1) = '../../../../../../../home/othersite/public_html/sites';
3. explode(':', 2) =
array('../../../../../../../home/othersite/public_html/sites');
4. implode('.', 3) =
'../../../../../../../home/othersite/public_html/sites';
5. explode('.', 4) = array('', '', '/', '', '', ......
'/home/othersite/public_html/sites');
6. $server = array('', '', '/', '', '', ......
'/home/othersite/public_html/sites');Next, the function loops over the array, and does a:
$dir = implode('.', array_slice($server, -$j)) . implode('.',
array_slice($uri, 0, $i));So, what does that first implode do? Simple: restores the previous
path... So if $uri is empty (it happens cause of the clause in the for
($i = count($uri) - 1; $i > 0; $i--) {), the raw path gets ported to
$dirSo at that point, $dir =
'../../../../../../../home/othersite/public_html/sites';When it hits the piont:
if (file_exists("$confdir/$dir/settings.php") ||
(!$require_settings && file_exists("$confdir/$dir"))) {remember tha earlier: $confdir = 'sites'
so that first part of the if breaks down to
if(file_exists('sites/../../../../../../../home/othersite/public_html/sites/settings.php'));remember, that the file DOES exist (because it's site 2)...
So now, I have site1's filebase running on site2's database. Now,
picture site1 is running a module that I can use to inject SQL, or php,
or do bad things, etc...The fix:
validate what characters you expect in the HTTP_HOST...
So
$host = preg_replace('[^a-z0-9\.-]', '', $_SERVER['HTTP_HOST']);
Here's the patch that was applied to 6.x. But we mght change it to be like what's in install.php
| Comment | File | Size | Author |
|---|---|---|---|
| #23 | bootstrap-conf-path-5x-324875-23.patch | 2.45 KB | pwolanin |
| #20 | bootstrap-conf-path-6x-324875-20.patch | 2.55 KB | pwolanin |
| #16 | bootstrap-conf-path-7x-324875-16.patch | 4.66 KB | pwolanin |
| #14 | bootstrap-conf-path-7x-324875-14a.patch | 4.66 KB | pwolanin |
| #13 | bootstrap-conf-path-7x-324875-13.patch | 4.51 KB | pwolanin |
Comments
Comment #1
pwolanin commentedalso pointed out by Anthony Ferrara, we should also blacklist \ as well as / (this part will need backport).
Comment #2
pwolanin commentedthet 2x calls to strpos is still much faster than 1 call to preg_match() (approx 4x faster on my machine).
Comment #3
webchickThanks, committed to HEAD. Looks like the other part needs porting back to 6.x now.
Comment #4
pwolanin commentedsetting back to 7.x - I think we should actually be doing this check in conf_init() before the calls to conf_path(). Also, in conf_init(), there is already a call to preg_replace() which is not safe for ipv6 addresses. So, I propose we move that preg_replace() to before the calls to conf_path() and revise the pattern to be mostly or totally ipv6 safe, and then we can omit these strpos calls all together.
Comment #5
pwolanin commentedDamZ posted this on on the original sec issue:
Comment #6
pwolanin commentedhere's a new patch for consideration.
separated out the validation into a separate function so that it's potentially testable via a simple unit test.
Comment #7
pwolanin commentednow with tests...
Comment #8
pwolanin commentedGabor suggested that we should be using the UTF-8 safe strtolower function, http://api.drupal.org/api/function/drupal_strtolower/7
However, this function is not yet available at this point in bootstrap, so trying that causes a fatal error. Perhaps we can just use mb_strtolower()?
Comment #9
gábor hojtsyIs mb_*() available at all times with PHP 5.x?
Comment #10
gábor hojtsyHum, actually IDNA defines a backwards compatible mapping to ASCII chars for international domain names, so the server should end up handling ASCII based hosts. Should not require handling UTF-8, but it might make sense to double check on a testing server.
Comment #11
pwolanin commentednote, the reason for lowercasing here is that if you are using a conf directory like sites/example.com, Drupal is likely to redirect you to install.php if you try to visit the site (e.g. using curl) with an address like Example.com or EXAMPLE.COM. Thus, we should force the host name to lowercase before looking for the conf directory. I don't see how this can used as an attack, but seems like a sensible rule to enforce lowercase.
(cross-ref to dup issue: http://drupal.org/node/325778)
Comment #12
damien tournoud commentedLet's do a positive check:
Comment #13
pwolanin commentedper discussion with chx and DamZ, we should be safe with "a-zA-Z-" (as per RFC 952), plus "_" as per (virtually RFC 2181)
ipv6 loopback address with a port may have HTTP_HOST
[::1]:8888, and in general may have[]So here's a simplified version that does a preg_match that allows that.Comment #14
pwolanin commentedDamZ prefers to block
..too.Comment #15
lilou commentedtypo in bootstrap.test :
t('Get the IP address from the current visitor from the server variables, check hostname vlidation.'),Comment #16
pwolanin commentedComment #17
damien tournoud commentedEven if we spent *way* too much time on this, I think we came up with a not so ugly solution. Good enough for me :)
Comment #18
dries commentedReviewed and this looks good, so committed to CVS HEAD. Thanks for the high-quality issue/comments. Great work.
Comment #19
pwolanin commentedthis better version should be backported too.
Comment #20
pwolanin commentedHere's a backport for testing.
Comment #21
gábor hojtsyLooks good. Would use some more testing on Drupal 6 to make sure it is the right fix for this version as well.
Comment #22
pwolanin commentedpatch still applies cleanly. @Gabor - what sort of testing d you think is needed?
Comment #23
pwolanin commented6.x patch still applies cleanly. What kind of additional testing (if any) is required?
Here's a version for 5.x also.
Comment #24
gábor hojtsySeeing that it was committed with a working test for D7, I've committed this to Drupal 6 as well. It is a pretty well abstracted API function change and is well tested on D7. Moving to Drupal 5.
Comment #25
drummCommitted to 5.x.
Comment #26
gábor hojtsySeems like the answer to #22 is that tests were missing to ensure it works with a missing HTTP_HOST. Please add such a test on 7.x, once we get the fix in. Unfortunately the code is broken for clients not sending over a HTTP_HOST: http://drupal.org/node/346285
Comment #27
drummI am confused about why this issue got re-opened. Is there anything that needs to be done here that #346285: Drupal 5.14 & Drupal 6.8 will not load on clients that do not transmit HTTP_HOST does not cover?
Comment #28
gábor hojtsyI've reopened in the expectation to have a 7.x test committed here, but if the other issue covers that as well, then fine for me to keep closed.
Comment #30
mcarbone commentedSo I don't completely understand the thread above, but I do know that this fix has broken some command-line Drupal scripts I have written. Namely, the bootstrap fails because $_SERVER['HTTP_HOST'] is an empty string. I can fix this by adding one in of course, but I wonder if the drupal_valid_http_host function should accept an empty host variable, or if you all think there should be another way to indicate a command-line bootstrap so that the validation is skipped.
Comment #31
mcarbone commentedSwitched back to active. Let me know if I should start a new thread.
Comment #32
drummBoth drupal.sh and Drush set $_SERVER['HTTP_HOST'], so I think setting it is the best practice.
Comment #33
pwolanin commentedtagging