Skip to content

Conversation

@ridz1208
Copy link
Collaborator

@ridz1208 ridz1208 commented Jun 5, 2017

This pull request Changes the Password reset logic to prevent users from resetting other users' passwords and thus blocking their accounts.

List of changes:

Bugfixes:
Logic changes:
  • Removal of the password expiry logic
  • Avoid password reset before email confirmation (main point of this PR) Redmine 8525

Some history

  • Issue 1: any user can enter any other user's username on the "forgot password" page and reset their password. This will generate a new random password and deactivate the current one. the user's account will not be accessible until they use the password sent to them by email.

    • solution: entering the username only generates a token which is emailed to the user's email address. the old password is still active until the token from the email is used to reset the password
  • Issue 2: mudularization of login logic caused the partial loss of the password expiry logic Publicly accessible module support #2608

    • solution: completed removal of password expiry logic. in order to make this PR testable
  • Issue 3: caused by issue 2 Removal of password_expiry logic affected behaviour of create_user and forgot_password functionality.

    • The current usage of create_user is
      1. the user requests an account (without choosing a password)
      2. dcc approves the account and generates a new random password and sends it to user
      3. the password is automatically set to expired
      4. the user uses the emailed password to login and is redirected to the password expiry page
    • solution 1: create_user logic should not involve password_expiry. Instead the user should be asked for a password at the moment of requesting an account. once the account is approved, the user will have access to loris...
    • The current usage of lost_password is
      1. user clicks on forgot password and is redirected to reset page.
      2. entering the username will replace the user's password by a random generated one and set it to expired
      3. the user is emailed a new password
      4. the user uses the emailed password to login and is redirected to the password expiry page
    • solution 2: forgot_password functionality was implemented to use the Token sent to the user in order to allow the user to choose a new password.
  • Issue 4: caused by issue3 solution 1 the delete_account functionality depends on the user's password never being set-up. In order to be able to delete a user, this user should have never been activated (otherwise foreign keys would fail). one of the only indicator of a user never being active is the absence of a password in the database.

    • solution: find another indicator of user's inactivity and use it instead to determine if a user can be deleted.
  • Issue 5: caused by clean-up by avoiding to try to instantiate users which do not already exist in the database (which triggers multiple notices and warnings) the create_user logic breaks since it needs an empty user object

    • solution: check if user is in db, otherwise bypass all database calls but return an empty object reday to be populated.

@ridz1208 ridz1208 added the Category: Bug PR or issue that aims to report or fix a bug label Jun 5, 2017
@ridz1208 ridz1208 added this to the 17.1 milestone Jun 5, 2017
@ridz1208 ridz1208 requested a review from driusan June 5, 2017 16:04
@johnsaigle
Copy link
Contributor

LGTM

@johnsaigle johnsaigle removed their assignment Jun 5, 2017
johnsaigle
johnsaigle previously approved these changes Jun 5, 2017
@johnsaigle
Copy link
Contributor

@ridz1208 this needs phpcs

@ridz1208 ridz1208 modified the milestones: 17.2, 17.1 Jun 21, 2017
@jstirling91
Copy link
Contributor

@ridz1208 phpcs this

@kongtiaowang
Copy link
Contributor

After I input email, got 500 error.

@johnsaigle
Copy link
Contributor

@kongtiaowang just so you know this is going to 17.2!

@johnsaigle
Copy link
Contributor

Hey @ridz1208 just got reminded of this after making my own PRs: #3044 and #3042.

I like your solutions better but there should be a discussion on this at some point. Just wanted to add a note here to close the above PRs if we go with your solution.

@ridz1208
Copy link
Collaborator Author

@johnsaigle lets discuss

@driusan driusan removed this from the 18.1 milestone Oct 2, 2017
@driusan
Copy link
Collaborator

driusan commented Oct 2, 2017

I added both major and minor release tags, because the change itself is minor, but it depends on another PR which is major..

@driusan driusan added this to the 19.0 milestone Oct 2, 2017
@ridz1208 ridz1208 removed this from the 19.0 milestone Oct 20, 2017
@ridz1208 ridz1208 added this to the 19.1 milestone Oct 20, 2017
@ridz1208 ridz1208 changed the base branch from 17.1-dev to minor October 21, 2017 17:23
@ridz1208 ridz1208 added the State: Blocked PR or issue awaiting an external event such as the merge or another PR to proceed label Oct 27, 2017
@ridz1208 ridz1208 added State: Needs rebase PR that needs to be rebased to proceed (conflicts, wrong branch...) and removed State: Blocked PR or issue awaiting an external event such as the merge or another PR to proceed labels Nov 20, 2017
@ridz1208 ridz1208 added the State: Needs work PR awaiting additional work by the author to proceed label Nov 20, 2017
@ridz1208 ridz1208 force-pushed the 2017_05_04_password_change_issue branch from d2c5492 to c8b1ec4 Compare November 21, 2017 19:02
@ridz1208
Copy link
Collaborator Author

rebasing on major for this https://github.com/aces/Loris/pull/3023/files

@ridz1208 ridz1208 force-pushed the 2017_05_04_password_change_issue branch from c8b1ec4 to 3805af9 Compare November 22, 2017 16:16
@ridz1208 ridz1208 changed the base branch from minor to major November 22, 2017 16:16
* Remove unused variable in main.php

* Fix PHPMD error

//validate token and load password change page
$validToken = $DB->pselectRow(
'SELECT *, TIMESTAMPDIFF(HOUR,time,CURRENT_TIMESTAMP) as token_age
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driusan should we expire the tokens ? if so, I need to start using this token_age variable

);

if (!empty($validToken)) {
header("Location: {$baseURL}/login/password-change/?token=$token");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

token is passed to the password change page to get the username

@driusan is there a better way of passing the username to the password-change page ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could look into using the PHP session, but I don't see any problem with doing it this way.


$this->addHidden('token', $_GET['token']);
$this->addHidden('username', $username);
$this->form->addFormRule(array(&$this, '_validate'));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driusan shouldn't this be done automatically on all forms ??

without this line, the _validate function is never called

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would make sense, but we have a lot of pages which aren't forms that are extending NDB_Form instead of NDB_Page and I think it should be investigated separately from this PR...

header("Location: {$baseURL}/");
}

$username = \User::getUsernameFromId($validToken['user_id']);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new function... not sure about it

// TODO: problem, the insert below generate SQL FK errors because there
// in no logged in user to track history. see database.class.inc line 895
// user is being set to 'unknown' at this stage
$DB->insert(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driusan need to resolve issue above

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The history table shouldn't have any foreign keys. It should be denormalized because it's a history table.. I think the solution is just to remove the constraint.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okai so for now can I just ignore the warnings ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I think so.


$row = $DB->pselectRow($query, array('UID' => $username));

if (empty($row)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driusan review carefully

do you foresee issues with logic ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just the usual "0" issues with empty()..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driusan I meant with moving around the ordering of the calls in the user constructor. if you expand the code u'll notice that I put this check and moved down a few variable declaration. Is any page relying on fetching an empty user ?

(ie probably create_user)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought either you or @xlecours had found one when you looked into it a few weeks ago or another reason, but I can't remember the details? (You're likely right about create_user.) If there are any, hopefully the intergration tests would catch it..

As far as changing the logic goes the only issue I see is that it doesn't really accomplish what the comment says it's trying to, because the early return makes it vulnerable to timing attacks, so a sufficiently advanced hacker could still derive the existence/non-existence of the username.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah.. i'll put a little sleep function on it to confuse the Russian/Chinese hackers trying to break into loris...

this kind of avoids like 30 notices/warnings so if there is not functional issue I still like it there

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also phishing =/= brute forcing =/= timing attacks but now I'm just being mean

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says it's not returning an error so that phishers won't know that the user doesn't exist. They can still figure out whether the user exists by timing the response time with this implementation. Just because it's not an advanced technique for statistically deriving a password doesn't mean it's not a timing attack.

Copy link
Collaborator Author

@ridz1208 ridz1208 Nov 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright, I just wanna clear up a couple things.

  • whether the user exists or not, the "thank you ..." message is still being displayed
  • if i have to keep the same amount of work done whether the user exists or not, then we have to figure out a way to do all that work without generating errors and warnings
  • there is a typo on line 68 where it says "does" instead of "does not" that I just corrected

@johnsaigle, in this case, wouldnt you use a bruteforce technique in order to accomplish a phishing attack? I might be confused about termnology but what I mean is:
Assuming the page returns a message like "this user does not exists" (which it doesnt), someone could write a script that accesses the forgot password page and try out usernames in a brute force manner (intelligent or not all combinations of names/words/wtv) to get information about existing users ?? am I wrong??

Copy link
Contributor

@johnsaigle johnsaigle Nov 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the page returns a message like "this user does not exists" (which it doesnt)

^ this is what I was trying to emphasize.

The user fills out and submits a form. Client-side Javascript updates the message. Then the server goes and does some work of validation and potentially sending an email. However it doesn't need to update the form submission page so timing is not an issue. As long as the page doesn't present a different response based on whether a user exists, and this is done client-side, there's no response to time.

In other words I don't think you need to manually ensure the same amount of work is being done given the approach you've taken here. 🥇

In the case that there does exist a different message for users that don't exist, you would enumerate the existing users in that way i.e. by the string "this user does not exist" in the response body rather than the timing of the response.

(I might be mistaking something because I didn't read all the code for this patch but I was basing my comments on modules/login/test/password_reset.md. Everything looks good there.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnsaigle yeah, I saw that yesterday in the test plan and changed it. if you look at the testplan I updated yesterday it should be corrected now :)

@ridz1208 ridz1208 added State: Discussion required PR or issue that requires the resolution of a discussion with the relevant parties to proceed and removed State: Needs rebase PR that needs to be rebased to proceed (conflicts, wrong branch...) labels Nov 26, 2017
@ridz1208
Copy link
Collaborator Author

@driusan I made a couple commits after my comments so some show as "outdated" but they are not. make sure to read all of them

email tests

SQL

last changes on old branch

password reset

nada

sooooo many things

2017_05_04_password_change_issue
@ridz1208 ridz1208 force-pushed the 2017_05_04_password_change_issue branch from 4a8d336 to 7d4290f Compare November 27, 2017 01:24
@ridz1208 ridz1208 force-pushed the 2017_05_04_password_change_issue branch from 7d4290f to 712fec6 Compare November 27, 2017 01:38
`time` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`ID`),
CONSTRAINT `fk_password_recovery_users_1` FOREIGN KEY (`email`) REFERENCES `users` (`Email`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this table adds yet another new naming/capitalization scheme to the schema? We really need to come up with one official way..

But anyways, there's also a foreign key missing on the user_id column and I'm not sure why the time column has the on update trigger. What's it supposed to be the time of?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since Alex, every new PR i've been making or reviewing I'm expecting snake case...

unless I am modifying a table already followin another convention.

So no this follows the same convention i've been using and that I assumed was the convention for new structures

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's ever been the case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol then add it to the list

try {
$timePoint =& TimePoint::singleton($_REQUEST['sessionID']);
$argstring .= "filter%5Bm.VisitNo%5D=".$timePoint->getVisitNo()."&";
$timePoint = TimePoint::singleton($_REQUEST['sessionID']);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to change? I'm afraid of conflicts with the NoMainPHP PR if this is unnecessary..

Copy link
Collaborator Author

@ridz1208 ridz1208 Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this is coming from ur own commit, not sure why it's popping in my PR at all

I tried a rebase -i its still there

);

if (!empty($validToken)) {
header("Location: {$baseURL}/login/password-change/?token=$token");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could look into using the PHP session, but I don't see any problem with doing it this way.

* @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3
* @link https://www.github.com/aces/Loris/
*/
class AuthenticateToken extends \NDB_Form
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's not a form on this page. Why not just extend NDB_Page?

Copy link
Collaborator Author

@ridz1208 ridz1208 Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point


$this->addHidden('token', $_GET['token']);
$this->addHidden('username', $username);
$this->form->addFormRule(array(&$this, '_validate'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would make sense, but we have a lot of pages which aren't forms that are extending NDB_Form instead of NDB_Page and I think it should be investigated separately from this PR...

// TODO: problem, the insert below generate SQL FK errors because there
// in no logged in user to track history. see database.class.inc line 895
// user is being set to 'unknown' at this stage
$DB->insert(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The history table shouldn't have any foreign keys. It should be denormalized because it's a history table.. I think the solution is just to remove the constraint.


$row = $DB->pselectRow($query, array('UID' => $username));

if (empty($row)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just the usual "0" issues with empty()..

// Always claim success to phishers.
$this->tpl_data['success'] = 'Password reset. '
. 'You should receive an email within a few minutes.';
. 'You should receive an email within a few minutes.';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driusan @ridz1208 I think everything I said in the other thread is actually wrong given this line.

I thought that a message like this would be displayed via Javascript (aka client-side) which would make timing attacks less of a concern since you're not depending on the server finishing up work to update the front-end. Given this approach, the two of you are right -- timing attacks are a concern.

@ridz1208 ridz1208 added the State: Blocked PR or issue awaiting an external event such as the merge or another PR to proceed label Nov 28, 2017
@ridz1208
Copy link
Collaborator Author

Adding blocked because this PR will be broken down into several more concise changes.

After the PRs are issued this one will be closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Category: Bug PR or issue that aims to report or fix a bug State: Blocked PR or issue awaiting an external event such as the merge or another PR to proceed State: Discussion required PR or issue that requires the resolution of a discussion with the relevant parties to proceed State: Needs work PR awaiting additional work by the author to proceed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants