Hardening - session regeneration

pwolanin - July 10, 2008 - 16:21
Project:Drupal
Version:6.x-dev
Component:user.module
Category:bug report
Priority:critical
Assigned:pwolanin
Status:needs work
Description

note, this was part of SA-2008-044 for D6

The original bug report to the security team:

As you know, all anonymous users are assigned a session id. When that anonymous user authenticates, the session is supposed to regenerate to provide a new session_id. We are finding that this is not happening on our site.

After running the authentication process through a debugger, I discovered that the reason the session is not being regenerated is due to a Workflow_NG action that gets executed as part hook_user(). We currently have a redirect action tied to user login, so that instead of a user being redirected to ?q=user/xxxx, the user is brought to a welcome back page (node/148).

In user.module:

<?php
function user_login_submit($form_id, $form_values) {
global
$user;
if (
$user->uid) {
  
// To handle the edge case where this function is called during a
   // bootstrap, check for the existence of t().
  
if (function_exists('t')) {
    
$message = t('Session opened for %name.', array('%name' => $user->name));
   }
   else {
     
$message = "Session opened for ". check_plain($user->name);
   }
      
//die('current user = '.$user->uid);
  
watchdog('user', $message);

  
// Update the user table timestamp noting user has logged in.
  
db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $user->uid);

  
user_module_invoke('login', $form_values, $user);

  
sess_regenerate();
   return
'user/'. $user->uid;
}
}
?>

We see the db_query() call execute, and the user_module_invoke() executes. The redirect action in Workflow_NG gets executed as part of this. This uses drupal_goto(), which has an exit() call at the end. Because of this, sess_regenerate() never gets called.

I believe that this vulnerability exists in core because of the order in which the functions are called. I propose that sess_regenerate() be called prior to the user_module_invoke() call and have confirmed that changing the order of the function calls in user.module fixes the issue.

<?php
...
// Update the user table timestamp noting user has logged in.
db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $user->uid);

sess_regenerate();
user_module_invoke('login', $form_values, $user);

return
'user/'. $user->uid;
}
?>

#1

pwolanin - July 10, 2008 - 16:27
Status:active» needs review

patch for 7.x (note, simple re-roll of the 6.x patch originally by Heine as suggested by Erich C. Beyrent in the e-mail above).

AttachmentSize
sess_regen_hardening_280934-1.patch 771 bytes
Testbed results
sess_regen_hardening_280934-1.patchfailedFailed: Failed to apply patch. Detailed results

#2

Dries - July 11, 2008 - 02:21
Status:needs review» needs work

1. I'd explicitly mention in the code comments that we want to regenerate the session ID before calling the hook, and why.

2. I think we should write a test for this. For obvious reasons, this is really important to get right and keep right.

#3

pwolanin - July 11, 2008 - 12:42

Any suggestions on how to write the test? I think that would require a dummy module (and/or a dummy module hook?)

#4

Damien Tournoud - July 11, 2008 - 12:48

Great, another use case for http://drupal.org/node/268063!

We need to write a small test module with a hook that breaks the contract, for example by redirecting the user to another page, and check if the session has been regenerated.

#5

pwolanin - July 11, 2008 - 13:04
Status:needs work» needs review

Added more of a code comment - the test really requires a dummy module. Postponing the test until we have a mechanism (e.g. issue linked above) for dummy modules in the testing framework.

AttachmentSize
sess_regen_hardening_280934-5.patch 981 bytes
Testbed results
sess_regen_hardening_280934-5.patchfailedFailed: Failed to apply patch. Detailed results

#6

Damien Tournoud - July 11, 2008 - 13:14

@pwolanin: you don't need to wait for #268063: Move includes/tests to simpletest/tests & provide hidden .info propery... The only thing this issue does is add the ability to hide a module from the modules administration page.

The following conventions where discussed (especially with chx, webchick and boombatower on IRC), and need to be documented properly:

  • Test modules go to the /tests subdirectory of the module they are testing, or in /modules/simpletest/tests for include tests,
  • You add hidden = TRUE to the test module's .info file,
  • The test module should be named "_test" (if it makes sense)

#7

sg3524 - August 14, 2008 - 14:20

Good to see this addressed.

Just deleted a longer comment but can't figure out how to delete this one altogether.

#8

Anonymous (not verified) - November 11, 2008 - 10:50
Status:needs review» needs work

The last submitted patch failed testing.

#9

swentel - November 11, 2008 - 15:14

Reroll patch (now uses drupal_session_regenerate)

AttachmentSize
sess_regen_hardening_280934-9.patch 1.04 KB
Testbed results
sess_regen_hardening_280934-9.patchfailedFailed: Failed to install HEAD. a href=http://testing.drupal.org/pifr/file/1/sess_regen_hardening_280934-9.patchDetailed results/a

#10

pwolanin - November 11, 2008 - 19:00
Status:needs work» needs review

#11

System Message - November 16, 2008 - 22:00
Status:needs review» needs work

The last submitted patch failed testing.

#12

pwolanin - November 17, 2008 - 02:12
Assigned to:Anonymous» pwolanin
Status:needs work» needs review

failure was due to HEAD not installing properly. Re-rolled patch with the addition of the PHP 5.2-only feature of setting "httpsonly" on the session cookie as an additional hardening.

ran all tests myself: 7299 passes, 0 fails, and 0 exceptions

AttachmentSize
sess_regen_hardening_280934-12.patch 1.65 KB
Testbed results
sess_regen_hardening_280934-12.patchre-testing

#13

scor - November 18, 2008 - 18:29
Status:needs review» reviewed & tested by the community

patch working on my machine as well: 7299 passes, 0 fails, and 0 exceptions

#14

webchick - November 18, 2008 - 18:35
Status:reviewed & tested by the community» needs work

Up above, Dries recommended we write a test for this so it stays fixed. Now we have the ability to create hidden modules that can be used for testing purposes, so let's do that. :)

#15

pwolanin - November 18, 2008 - 22:01

@webchick - argh - these tests killed my afternoon... but working now.

Here's a patch with only the tests. Without the patch from #12, there are 2 fails.

AttachmentSize
sess_regen_tests_only_280934-15a.patch 4.96 KB

#16

pwolanin - November 18, 2008 - 22:04
Status:needs work» needs review

the full patch

AttachmentSize
sess_regen_hardening_280934-16.patch 4.96 KB
Testbed results
sess_regen_hardening_280934-16.patchfailedFailed: 7326 passes, 2 fails, 0 exceptions a href=http://testing.drupal.org/pifr/file/1/sess_regen_hardening_280934-16.patchDetailed results/a

#17

System Message - November 18, 2008 - 22:50
Status:needs review» needs work

The last submitted patch failed testing.

#18

pwolanin - November 18, 2008 - 23:17
Status:needs work» needs review

whoops - posted the test-only patch twice. And testbot has demonstrated that without the rest of the patch there are 2 fails...

AttachmentSize
sess_regen_hardening_280934-18.patch 6.57 KB
Testbed results
sess_regen_hardening_280934-18.patchpassedPassed: 7522 passes, 0 fails, 0 exceptions Detailed results

#19

keith.smith - November 18, 2008 - 23:38

when (if) you reroll, there's an "incorrectoly" in there.

#20

pwolanin - November 19, 2008 - 02:56

w/ typo fix

AttachmentSize
sess_regen_hardening_280934-20a.patch 6.57 KB

#21

scor - November 19, 2008 - 06:44
Status:needs review» needs work

there is a trailing space on the line session_set_cookie_params($lifetime, $path, $domain, $secure, TRUE);

#22

pwolanin - November 19, 2008 - 12:30
Status:needs work» needs review

ok - fixed (though I think webchick has also been removing trailing spaces before commit).

AttachmentSize
sess_regen_hardening_280934-22.patch 6.58 KB
Testbed results
sess_regen_hardening_280934-22.patchpassedPassed: 7522 passes, 0 fails, 0 exceptions Detailed results

#23

catch - November 21, 2008 - 00:29

This all looks RTBC to me, except it needs to be re-rolled for a couple of missing t() calls in the assertions. Told pwolanin this in irc so consider the following patch pre-approved.

#24

pwolanin - November 21, 2008 - 14:14
Status:needs review» reviewed & tested by the community

ok, wrapped assertion text in t() as required.

AttachmentSize
sess_regen_hardening_280934-23.patch 6.59 KB
Testbed results
sess_regen_hardening_280934-23.patchpassedPassed: 7522 passes, 0 fails, 0 exceptions a href=http://testing.drupal.org/pifr/file/1/sess_regen_hardening_280934-23.patchDetailed results/a

#25

Dries - November 24, 2008 - 06:13
Status:reviewed & tested by the community» fixed

Thanks pwolanin. Great work as usual! :)

#26

pwolanin - November 24, 2008 - 12:53
Status:fixed» patch (to be ported)

is it worth backporting the httponly cookie part? I think adding that param will just be a no-op with older version of PHP.

#27

pwolanin - December 7, 2008 - 04:51
Version:7.x-dev» 6.x-dev

#28

pwolanin - December 7, 2008 - 05:07
Status:patch (to be ported)» needs review

Tested this locally and works with PHP 5.2 and Drupal 6.x

AttachmentSize
sess_hardening_280934-27.patch 819 bytes

#29

Gábor Hojtsy - December 8, 2008 - 11:45

Extract is always crazy, since it could overwrite whatever is in the current scope. However, we only have $old_session_id in this scope, so it might not be a problem there I guess. Still for extra safety and best practice, I'd use extract(..., EXTR_PREFIX_ALL, 'session') or something, so we'd get $session_lifetime, $session_path, etc. Otherwise looks good.

#30

pwolanin - December 8, 2008 - 14:03

@Gabor - given that this is such a short function with almost no variables in scope, the extract should be harmless (i.e. this version is in 7 already). If you see some reason to worry, we could just use EXTR_SKIP?

#31

Gábor Hojtsy - December 8, 2008 - 15:10
Version:6.x-dev» 5.x-dev
Status:needs review» reviewed & tested by the community

EXTR_SKIP and EXTR_OVERWRITE are just not good enough generally because you never know then which value you got at the end, and that could be quite confusing for working with the variables. Since this was already accepted in Drupal 7, and that it has a very low likeliness that there will be some new variables added later to this function which might conflict with the extracted ones (even those, which are not visible in variables now used from the extract), I did commit this to Drupal 6. Thanks.

Moving down to Drupal 5.x for consideration.

#32

drumm - December 10, 2008 - 18:10
Status:reviewed & tested by the community» fixed

Committed to 5.x.

#33

drumm - December 11, 2008 - 00:23
Version:5.x-dev» 6.x-dev
Status:fixed» reviewed & tested by the community

Rolled back for 5.x. #345495: Wrong parameter count for session_set_cookie_params() error after drupal 5.13 update and 6.7 update

#34

Gábor Hojtsy - December 11, 2008 - 00:30
Status:reviewed & tested by the community» needs work

Rolled back for 6.x.

#35

hass - December 11, 2008 - 11:10

sub

#36

Damien Tournoud - December 11, 2008 - 11:18

Incredible, so there *are* people using Drupal on PHP4 :)

#37

dzhu - December 11, 2008 - 15:44

From patch code:

+  // This has no effect for PHP < 5.2.0.

This HAS effect at PHP 4.3.9, the error:

Wrong parameter count for session_set_cookie_params() in file /path/to/drupal/includes/session.inc in line 103.

Where the line 103 is:
  session_set_cookie_params($lifetime, $path, $domain, $secure, TRUE);

#38

Heine - December 11, 2008 - 15:45

@ dzhu, please read the issues before commenting (see #33, #34). Thank you.

#39

dzhu - December 11, 2008 - 15:49

According to system requirements for Drupal 5 and 6, it's PHP version 4.3.5 compatible...

2 Heine, thanks, but what's with Drupal 6.7 current code? Will it be changed?

#40

Heine - December 11, 2008 - 15:51

@dzhu Releases cannot be changed. The fix will go out as 6.8 and 5.14 later today.

#41

Gábor Hojtsy - December 11, 2008 - 19:12

Drupal 6.8 and 5.14 is out with this change.

#42

michtoen - December 11, 2008 - 21:02

What are the plans to go on with this issue?

#43

andypost - December 12, 2008 - 01:08

Investigation of HTTPOnly flag

For anyone looking for which browsers support the HTTPOnly flag, per my research:

IE 6 SP 1 and higher.
Firefox 3 and higher.
Opera 9.50 and higher.

As of Safari 3.1.1 (April 2008), Safari did not yet support this flag.

This cookie flag was developed by Microsoft and is slowly making its way into other browsers: http://msdn2.microsoft.com/en-us/library/ms533046.aspx

Firefox 2.0 also supports them, but only since version 2.0.0.5.

http://bugzilla.mozilla.org/show_bug.cgi?id=178993

This feature is partially supported by browsers

#44

expatme - December 12, 2008 - 01:37

so basically you dont need this if using php5 ?

#45

pwolanin - December 13, 2008 - 15:13

Do we want to provide this for people using PHP 5.2+? I guess the solution is to just wrap the code in an if block and check the PHP version.

I.e. similar to the code in system.install:

<?php
 
if (version_compare(phpversion(), '5.2.0') >= 0) {
   
extract(session_get_cookie_params(), EXTR_PREFIX_ALL, 'session_')
   
// Set "httponly" to TRUE to reduce the risk of session stealing via XSS.
    // This has no effect for PHP < 5.2.0.
   
session_set_cookie_params($session_lifetime, $session_path, $session_domain, $session_secure, TRUE);
  }
?>

#46

andypost - December 13, 2008 - 21:13

Suppose version check is required but what's about people using lower php versions?

#47

pwolanin - December 14, 2008 - 02:17

People using lower versions of PHP will (continue to) lack this feature. We suggest you get on PHP 5.2+ in general.

 
 

Drupal is a registered trademark of Dries Buytaert.