-
Notifications
You must be signed in to change notification settings - Fork 189
[Login] Change of logic for Password reset to use tokens #2856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
LGTM |
|
@ridz1208 this needs |
|
@ridz1208 phpcs this |
|
After I input email, got 500 error. |
|
@kongtiaowang just so you know this is going to 17.2! |
|
@johnsaigle lets discuss |
|
I added both major and minor release tags, because the change itself is minor, but it depends on another PR which is major.. |
d2c5492 to
c8b1ec4
Compare
|
rebasing on major for this https://github.com/aces/Loris/pull/3023/files |
c8b1ec4 to
3805af9
Compare
* 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 |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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')); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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']); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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()..
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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??
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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 :)
|
@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
4a8d336 to
7d4290f
Compare
7d4290f to
712fec6
Compare
| `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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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']); |
There was a problem hiding this comment.
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..
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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')); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.'; |
There was a problem hiding this comment.
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.
|
Adding blocked because this PR will be broken down into several more concise changes. After the PRs are issued this one will be closed |
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:
_validatefunctions never called (all saves went straight to processing)showPasswordExpiryScreen()in https://github.com/aces/Loris/pull/2608/files#diff-2c13360f65cfe03fa685449b9074176eL371Logic changes:
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.
Issue 2: mudularization of login logic caused the partial loss of the password expiry logic Publicly accessible module support #2608
Issue 3: caused by issue 2 Removal of password_expiry logic affected behaviour of create_user and forgot_password functionality.
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.
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