Following up discussion on the Security Team list.

one-time login links are not stored in the DB, but rather are simply generated based on the time and user last login - so anyone who can read the user table can generate a valid login link for any user.

http://api.drupal.org/api/function/user_pass_rehash/7

Thus, anyone with access to a DB dump can easily generate login links for any user who has not logged in since the DB dump was generated without needed to crack the passwords. Worse, someone who finds and SQL injection allowing them to SELECT data from the {user} table can generate login links for basically any site user immediately.

How can we deal with this? For D7 for simpletest we are adding a hmac based on db credentials.

http://api.drupal.org/api/function/drupal_valid_test_ua/7

We could do something similar so that the one-time link cannot be generated from SQL alone? Basically - we need to use some value from a file or based on a file where that value is not accessible via SQL, but is reasonably secret and reasonably stable.

Writing other data into SQL doesn't solve the problem, since e.g. in the event we wrote a salt to the the {user} table, valid values might still be in a DB dump, and in the event of an SQL injection, tey can just request a link for user 1 to get the salt written into the DB where it can be read.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

buherator’s picture

This approach could be optional, because in standard installations, following sysadmin best practices, SQL injection and the loss of the DB dump is uncommon. My Proposal:

0. On password reset request, send a similar link to the requesting users email address as in the current version
1. Let this link include some random magic number to prevent password resets using DB dumps. These magic numbers can be stored until expiration in the database.
2. If the user follows the link, instead of allowing him to change his pass, send another email to his email address with a random generated, strong password
(3. After logging in with the new password, force the user to change it)

Don't reset the users password until he follows the first link! This prevents malicious attackers from disabling logins by requesting password resets.

Damien Tournoud’s picture

I suggest we just generate a nonce and:

* send it to the client in plaintext,
* but hash it like a password when storing it in the database (in a separate table, not in the user table).

buherator’s picture

I can agree with this too.

pwolanin’s picture

@Damien - ah, that's maybe easier than my solution - though if it's robust it doesn't matter what table we store it in.

in IRC, chx suggested (ab)using the data column in {user}. With that we could backpoort to D6 with no schema change.

A few possible problems - right now I can request multiple one-time login links. All of them are valid until any one of them is used. With the method Damien proposes, only the one, most recent, link would be valid.

Thus, the method you propose opens Drupal up to an easy DoS attack - I can submit one-time links for all users every minute. Any user who needs to use a one-time link will have to use it within one minute or it will be invalidated. Also, requesting a link would cause a write to the DB, causing extra DB load.

pwolanin’s picture

downside of the method suggested by buherator - we need to store the magic number hashed or encrypted, otherwise we are back to the same problem as we do now, but then anyone with an existing user account will get access to the plaintext magic number, which makes this a sec improvment for only a subset of sites.

pwolanin’s picture

Also, a serious downside to requiring a subsequent password login with a new pass is that immediate login via these links is a way to bypass DoS attacks that trigger the login limiting we added to D7.

pwolanin’s picture

pwolanin’s picture

thinking about this more -I'm still stuck on either having a new table where we write nonce+hash values for each uid for each requested link (emptied for that uid when a login happens) or using something file based as suggested above.

Note that the linked issue above suggests requiring the current password in order to change the e-mail address. I think that + allowing the user to change their password without the current password when using a one-time link would be reasonably secure.

buherator’s picture

I didn't know about this sry, anyway, Damiens proposal seems much better to me than mine.

The "out-of-DB" way seems not that attractive because on many systems SQL SELECT's allow you to read files with the privileges of the mysqld (or whatever DBMS you use). On proper configurations the DBMS would only access to its own files, but this is not an everyday situation...

pwolanin’s picture

Well, look at the function I linked - I'd be surprised if you could mimic that via SQL.

Also, I think Damien's proposals has some serious downsides, unless we have a table that can store potentially many nonce values + hash values per user.

I'd think such a table would need 3 columns: uid, timestamp, and hash

However, this approach would require serious re-engineering, since now we populate a token with a one-time link every time an e-mail is composed to a user.

pwolanin’s picture

Discussing this with greggles - a reasonable first step would be to populate settings.php with a random string on install and use that string in the hash/hmac. You can (if you want to ) make this much safer by populating this value some other way - e.g. use file_get_contents() and load a sting from a file outside the docroot which is excluded from your normal backups.

A problem with the code that's in D7 for simpletest is that it very likely breaks if you have multiple webservers for the same site. Maybe that's ok for simpletest, though once we add this code possible simpletest can use it too.

For D6 we'd have the option of making the added security optional for existing installs (i.e. same problems as now) or optionally breaking one-time logins until the admin populates a string into settings.php

buherator’s picture

Ouch, that fileinode() seems hard job (possibly impossible) from SQL, and from some leaked backup too, very nice!

However, I still won't recommend the usage of the _content_ of some file, because of the difficulities of the proper server configuration - most of the users won't think about this issue, won't care about that file, and won't revoke the DBMS's access to it (sometimes they are even restricted to do that), and we are back to where we started from.

pwolanin’s picture

@buherator - unfortunately the fileinode() is almost sure to be different if you have different web servers.

I still see the risk of being able to read a file as much lower than the typical SQL injection risk - in any case, inclusion of some basic hardening of this as a good step forward, especially if it's something that can be backported to 5 and 6.

buherator’s picture

OK, even the thousand miles journey starts with one step, I just want to reveal the possible attack surfaces.

Pasqualle’s picture

#600718: Improve the password reset functionality marked as duplicate

My idea would be to send random "reset code" in the password reset email, and add a column to {users} table (or create new table). On the one time login page the user needs to enter the reset code to access his account.
the db column could be something like password_reset_hash = some_hash($reset_code . $something)

pros:
The password reset link could remain the same as it is now.
The reset code in the email is required to reset the password.
This is much better than a secret word in the settings.php
Can not reset the password without email. Right now you can create a one time login link without sending any email. (check the issue #600718)

cons?:
yes, it is a db write when requesting a password reset. Maybe with a new table (instead of new column) the table could be moved to a faster storage.

pwolanin’s picture

@Pasqualle - this is essentially the same as one of my suggestions above. See #10.

There is no reason to make the user enter the code manually - embedding it in the link provides the same level of security.

This would be problematic in D7 even, and more so in D5, D6 for reasons I describe above.

Pasqualle’s picture

I am not sure what embedding the code in the link means for you, and I am quite confused with the comments.

(a) /user/reset/1/1255145008/26b5a387b0634b59cb23f3893bda6e7f &code=DE522573
(b) or just changing the hash value?

I am talking about option (a) where this reset code is not stored in the db (just the hash($reset_code) is stored) and generated with every new request, so the attacker does not have any chance to reset the password without the email (even if he has a read access to db and settings.php).

I do not know which problems are you referring to as the only one seems that under DOS attack you can not use the password reset function. Is that really an issue?

pwolanin’s picture

@Pasqualle - I'm not following what you propose - at the least it has the issue re: token generation noted above

Pasqualle’s picture

Status: Active » Needs review
FileSize
4.83 KB

working reset code
This patch is submitted as an explanation of the idea, not a final solution..

pwolanin’s picture

Status: Needs review » Needs work

@Pasqualle - did you even read my comments?

There is a DoS issue since in D7 we adding rate limiting to login attempts - the bypass of a DoS on your account login through multiple attempts is this one-time link.

Pasqualle’s picture

Yes, I have read the comments, but to be honest I have no clue what are you talking about. Why would a one time login help is case of DoS, and which change in the patch break it? But I am not really eager to know the answer, I just wanted to show what could be a possible solution. If you say it does not work in some cases I believe you..

pwolanin’s picture

Priority: Normal » Critical

I think this is critical - D7 blocker

we shoudl at least get a simple hardening in.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
3.03 KB

Here's such a simple hardening that could readily be backported to D6 and D5 even.

pwolanin’s picture

looking deeper: due to this, we should maybe leave $db_url in the list of bootstrap globals? http://api.drupal.org/api/function/update_fix_d7_requirements/7

pwolanin’s picture

Wow - D6 to D7 update is really broken - but at least the salt is written to the settings.php file per this revised patch.

coltrane’s picture

This looks really good to me as a first step to hardening the one-time login process. To describe for new readers, the patch in #25 creates a settings.php variable that's used for salting one-time login links. The hash is created during update/install. If the hash isn't set the patch defaults to a hashed serialization of the databases setting. You can also specify the contents of a file to use instead.

My only reason for not setting to RTBC is I think there is value in generalizing the variable name. A file-level hash could be useful for developers similarly needing a salt not stored in the DB. Instead of $user_pass_rehash_salt how about just $salt?

brianV’s picture

I agree with coltrane.

The patch itself looks good, but I think we should use a generic name for the salt variable, as it may be reused in other contexts.

pwolanin’s picture

Well - this is exported as a global variable, hence I choose a longer name to avoid collisions in naming.

coltrane’s picture

Offers:
$system_salt
$drupal_system_salt
$drupal_system_key

pwolanin’s picture

Here's a version with a more generic variable name, plus a helper function to facilitate re-use. Re-used also to harden the Drupal token system against hash extension attacks which might (with sufficient effort) reveal the private key.

coltrane’s picture

Waiting on testbot but if it checks out I think this is RTBC

+++ includes/common.inc
@@ -4500,7 +4513,7 @@ function drupal_get_token($value = '') {
-  return (($skip_anonymous && $user->uid == 0) || ($token == md5(session_id() . $value . variable_get('drupal_private_key', ''))));
+  return (($skip_anonymous && $user->uid == 0) || ($token == drupal_get_token($value)));

Great catch to use drupal_get_token() here.

1 days to code freeze. Better review yourself.

coltrane’s picture

Status: Needs review » Reviewed & tested by the community

Looking good and tests pass.

Dries’s picture

+++ includes/common.inc
@@ -4482,7 +4495,7 @@ function drupal_get_private_key() {
   $private_key = drupal_get_private_key();
-  return md5(session_id() . $value . $private_key);
+  return md5(drupal_get_hash_salt() . md5(session_id() . $value . $private_key));

Any reason we call md5() twice? If so, please add a code comment.

pwolanin’s picture

@Dries - yes to prevent hash-extension attack

re-rolling.

pwolanin’s picture

Only changes are in the code comments.

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Looks great now. Committed to CVS HEAD. Thanks.

pwolanin’s picture

Version: 7.x-dev » 6.x-dev
Status: Fixed » Patch (to be ported)

Thanks.

We should also backport this for D6.

markus_petrux’s picture

subscribing

Sohodojo Jim’s picture

subscribing

pwolanin’s picture

bump to remind myself to backpot

ñull’s picture

subscribe and bump to backport

pwolanin’s picture

Status: Patch (to be ported) » Needs review
FileSize
4.26 KB

Here is a quick re-roll - the patch above dates from a time 7.x was less diverged from 6.x, so it mostly applied.

pwolanin’s picture

bump - still needs review

Status: Needs review » Needs work

The last submitted patch, harden-one-time-590656-42-D6.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
4.36 KB

Still applies for me w/ a little fuzz. Here's a clean re-roll.

Gábor Hojtsy’s picture

Is this tested on PHP 4? Who tested the patch?

killes@www.drop.org’s picture

I've looked over the patch and don't see why it would fail on php4, but can't test it.

It uses sha1 in one place but uses md5 twice in other places. Should probably be standardized.

pwolanin’s picture

Status: Needs review » Needs work

indeed, I guess since it's using md5, we shoudl just use it everywhere.

Also, I see a bug:

global $databases

should be

global $db_url
scor’s picture

Status: Needs work » Needs review
FileSize
696 bytes
4.35 KB

fixed global $db_url variable.

Status: Needs review » Closed (outdated)

Automatically closed because Drupal 6 is no longer supported. If the issue verifiably applies to later versions, please reopen with details and update the version.