Moving the discussion from #443154: Fatal errors in tests not reported as failures about security

#57
Dries - July 11, 2009 - 07:28

I wonder if this opens a security door:

+if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) {
+  // Log fatal errors.
+  ini_set('log_errors', 1);
+  ini_set('error_log', file_directory_path() . '/error.log');
+}

It basically enables people to write to an error.log stored in the publicly accessible file directory. This change makes me a bit nervous.

#63
boombatower - July 13, 2009 - 14:53

Something we had in SimpleTest when it went in core originally (or close to that time) was a key that was used in addition to the db prefix to confirm that SimpleTest was the one sending the prefix. The key is much easier to check than other methods (db or files), but technically isn't quite as secure. Once someone figures out the key (not easy of course) it doesn't matter if SimpleTest is doing anything. Although we can just neglect to generate the key until SimpleTest is installed...end remove the key on uninstall.

Not sure if drupal_get_private_key() would be of use here to use as the key.

Personally that the key seems like the simplest and least expensive option. One issue to consider although is that we need a way to check before invoking the db.

The need to check before setting the DB prefix is the real problem - I'm tempted to suggest that we should go back to requiring that a value be set in settings.php. Given that most people now rely on test bot, I don't think this would be a real impediment to development. Also, right now we do things like setting the DB prefix without any check that the Simpletest module is installed - that also seems a little odd.

To be more careful you could use the value in settings.php as a key to generate a simple hmac as part of the user agent header so that you are never sending the secret value. Or we could use a hash of the $databases variable as the secret key to avoid needing to set an extra value?

So the code in function curlInitialize() could look like:


    global $databases;

      if (preg_match('/simpletest\d+/', $db_prefix, $matches)) {
        $key = sha1(serialize($databases), TRUE);
        $salt = uniqid('', TRUE);
        $ua = $matches[0] . '/' . time() . '/' . $salt;
        $hmac = base64_encode(hash_hmac('sha1', $ua, $key, TRUE));
        $curl_options[CURLOPT_USERAGENT] = $ua . '/' . $hmac;
     }

This formulation would allow you to check that the request time when you receive it is within (e.g.) 2 or 5 seconds. Since you are always making these requests to yourself, clock synchronization is not an issue.

Obviously this adds a tiny overhead, but an attacker's ability to arbitrarily make such requests is now as secure as your DB pw, and their ability to sniff the header and replay it is minimized.

also fwiw, "The Hash extension requires no external libraries and is enabled by default as of PHP 5.1.2. It may be explicitly disabled by using the --disable-hash switch to configure. "

Comments

boombatower’s picture

Not sure we want to look ourselves in to a timebased mechanism. That now forces the test to run in a particular amount of time which varies ...and doesn't seem necessary.

I think the idea of md5()/hashing the db config string makes sense.

pwolanin’s picture

@boombatower - the code would only mean that each http connection needs to be made within the time window, since $this->curlInitialize(); is called in function curlExec($curl_options) which is called for *each* drupalGet(), drupalPost, etc.

It's essential to have a time window here, otherwise anyone observing a single request could replay it forever.

boombatower’s picture

protected function curlInitialize() {
    global $base_url, $db_prefix;
    if (!isset($this->curlHandle)) {
...

So your going to place it outside of if (!isset($this->curlHandle)) { ?

Was not clear, but if you don't it will only be called once per test case. Which then may cause time issue.

So instead you'll execute that everytime...that should work.

boombatower’s picture

I still question how necessary this is, since you are restricted to a db prefix of "simpletest\d+"

If someone actually uses simpletest\d+ for the prefix for their website, but then all drupal will do of course is switch to that site. If it doesn't exists Drupal will crash and burn during early bootstrap db phase.

Seems like a good idea to have backup in place...

The main issue seems to come from the logging being place in public directory...could possibly be moved to temp...but then so should al the simpletest stuff...which just doesn't sound right. Do remember of course that the db layer will crash so the only thing printed to the log will be a db error.

Either way we might as well go ahead and do #329177: Get rid of ugly simpletest regexp. while we here. I'll thus mark is as duplicate.

pwolanin’s picture

Status: Active » Needs review
StatusFileSize
new5.6 KB

Right, once per test would cause a timing issue potentially.

Here's a patch including additional entropy from file properties as well.

pwolanin’s picture

I didn't see your comment about the other issue - in the patch I switched a couple cases to the faster strpos(), so I guess we have to decide which we care about more.

Status: Needs review » Needs work

The last submitted patch failed testing.

pwolanin’s picture

hmm, ok - testbot doesn't like security...

I don't see the blog fails locally, but I do see 1 fail in categorize feed items. Something funny about the http requests? Is there another spot where we need to generate or pass these headers?

pwolanin’s picture

Status: Needs work » Needs review
StatusFileSize
new6.67 KB

a new common function for both simpletest and drupal_http_request

Status: Needs review » Needs work

The last submitted patch failed testing.

pwolanin’s picture

argh - these failures seem to be internal to simpletest:
SimpleTest error collecter 2 1 0
SimpleTest functionality 74 1 0

pwolanin’s picture

Status: Needs work » Needs review
StatusFileSize
new8.19 KB

Ah, missing install.php in the patch and missed changing the regex in the erorr collector.

boombatower’s picture

Status: Needs review » Reviewed & tested by the community

Code logic looks good, and bot likes it.

chx’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new8.59 KB

Requirements check added.

webchick’s picture

Discussed this patch some with Jimmy and chx tonight. This is holding up some other improvements in the testing system, including #443154: Fatal errors in tests not reported as failures.

It bothers me that this patch doesn't have tests to prove that it prevents the exploit it's attempting to protect against. Chx informs me however that such tests are really hard to write, esp. when #340283: Abstract SimpleTest browser in to its own object isn't in yet (and doesn't look like it'll get in soon since it's deadlocked in ideological discussion), but that they'll get added eventually...

I would ideally like to see Damien and Dries (although he's traveling this week) give this patch a sanity check, since they were the ones who spotted the security exploit in the fatal error catching patch.

damien tournoud’s picture

Status: Needs review » Reviewed & tested by the community

Reviewed carefully, I don't have anything to add.

pwolanin’s picture

@webchick - there is one test already to insure that we can't launch install.php

Status: Reviewed & tested by the community » Needs work

The last submitted patch failed testing.

pwolanin’s picture

ugh, guess there was a conflicting commit.

dries’s picture

Yep, needs a quick reroll.

boombatower’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new9.22 KB

Re-rolled.

dries’s picture

Status: Reviewed & tested by the community » Fixed

Committed to CVS HEAD. Thanks.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.