Disable users with breached passwords#4541
Conversation
|
One additional thing, we probably want to announce this change to pypi-announce and to distutils-sig whenever we merge it. We probably want to get some messaging sorted out for that as well. |
2b33ae2 to
ff596b5
Compare
|
Ok, besides the two open questions:
this is ready to go. This does add a setting: However the default for that option is the HIBP service, so we do not need to change anything in production. The local development environment has that setting to enable the |
|
Thanks. Will take a look tomorrow. |
hugovk
left a comment
There was a problem hiding this comment.
Some language suggestions based on https://web.archive.org/web/20180410101124/https://material.io/guidelines/style/writing.html and https://plainlanguage.gov/guidelines/words/use-simple-words-phrases/
|
|
||
| # How do you know this? | ||
|
|
||
| We utilize a free security service from HaveIBeenPwned. When registering, |
There was a problem hiding this comment.
Use plain English "use" instead of "utilize".
|
|
||
| We utilize a free security service from HaveIBeenPwned. When registering, | ||
| authenticating, or updating your password, we generate a SHA1 hash of your password and | ||
| use the first five (5) characters of the hash to determine if the password has been |
There was a problem hiding this comment.
Just "5" instead of "five (5)".
There was a problem hiding this comment.
"has been compromised" - > "is compromised"
| {% block content %} | ||
| # What? | ||
|
|
||
| During your recent attempt to log in or upload to PyPI, we noticed that your password |
| # What? | ||
|
|
||
| During your recent attempt to log in or upload to PyPI, we noticed that your password | ||
| has appeared in public data breaches. To protect you and other users, we have |
There was a problem hiding this comment.
"has appeared in" - > "appears in"
|
|
||
| During your recent attempt to log in or upload to PyPI, we noticed that your password | ||
| has appeared in public data breaches. To protect you and other users, we have | ||
| preemptively reset your password and you will no longer be able to log in or upload to |
There was a problem hiding this comment.
"you will no longer be able to" - > "you can no longer" or "now you can not"
| If you receive an error message saying that "This password has appeared in a breach or has otherwise been compromised and cannot be used", you should change it all other places that you use it as soon as possible. | ||
| </p> | ||
| <p> | ||
| If you've received this error while attempting to log in to or upload to PyPI, then your password has been reset and you will no longer be able to log into PyPI until you <a href="{{ request.route_url('accounts.reset-password') }}">reset your password</a>. |
There was a problem hiding this comment.
"you've" - > "you",
"log in to" - > "log in",
"you will no longer be able to" - > "you cannot"
There was a problem hiding this comment.
I didn't like the way this sentence read changing you've to you, but I expanded it out to "you have".
| ( | ||
| None, | ||
| ( | ||
| "This password has appeared in a breach or has otherwise " |
There was a problem hiding this comment.
"has appeared in" - > "appears in"
Delete "otherwise" and trailing fullstop
| ( | ||
| "http://localhost/help/#compromised-password", | ||
| ( | ||
| "This password has appeared in a breach or has otherwise " |
There was a problem hiding this comment.
"has appeared in" - > "appears in"
Delete "otherwise"
| class HaveIBeenPwnedPasswordBreachedService: | ||
|
|
||
| _failure_message_preamble = ( | ||
| "This password has appeared in a breach or has otherwise been compromised and " |
There was a problem hiding this comment.
"has appeared in" - > "appears in"
Delete "otherwise" and trailing fullstop
| @implementer(IPasswordBreachedService) | ||
| class NullPasswordBreachedService: | ||
| failure_message = "This password has appeared in a breach." | ||
| failure_message_plain = "This password has appeared in a breach." |
There was a problem hiding this comment.
"has appeared in" - > "appears in"
|
Addressed the feedback by @hugovk. |
| user_service.update_user(user.id, password="foo") | ||
| assert user.password != "!" | ||
|
|
||
| # Now we'll actually test our disble function. |
| def create_service(cls, context, request): | ||
| return cls() | ||
|
|
||
| def check_password(self, password, *, tags=None): |
There was a problem hiding this comment.
I think I don't quite understand under what circumstances we want to use this?
There was a problem hiding this comment.
We use it in local development, because we set all users to have the password "password", which appears in breaches. Thus without the NullPasswordBreachedService in local development, we would never be able to log into those users, and would have to generate secure password for them.
| @@ -178,6 +178,9 @@ <h3 id="compromised-password">{{ compromised_password() }}</h3> | |||
| <p> | |||
| If you receive an error message saying that "This password has appeared in a breach or has otherwise been compromised and cannot be used", you should change it all other places that you use it as soon as possible. | |||
There was a problem hiding this comment.
Do we need to update the wording of the quoted error message here?
f3ea5d8 to
b4f937c
Compare
|
Feedback from @brainwane addressed and responded to! |
| <h3>What should I do?</h3> | ||
| <p> | ||
| To regain access to your account, | ||
| <a href="{{ request.route_url('accounts.reset-password') }}">reset your password</a> |
There was a problem hiding this comment.
I think this should be request.route_url('accounts.request-password-reset')
| # What should I do? | ||
|
|
||
| To regain access to your account, reset your password on PyPI | ||
| ({{ request.route_url('accounts.reset-password') }}). We also recommend that you go to |
There was a problem hiding this comment.
I think this should be request.route_url('accounts.request-password-reset')
| If you receive an error message saying that "This password appears in a breach or has been compromised and cannot be used", you should change it all other places that you use it as soon as possible. | ||
| </p> | ||
| <p> | ||
| If you have received this error while attempting to log in or upload to PyPI, then your password has been reset and you cannot log in to PyPI until you <a href="{{ request.route_url('accounts.reset-password') }}">reset your password</a>. |
There was a problem hiding this comment.
I think this should be request.route_url('accounts.request-password-reset')
|
Noticed that we're sending folks to the wrong URL in the emails/FAQ. This route flashes "Invalid token: no token supplied", as indeed no token is supplied. |
b4f937c to
fb02683
Compare
|
Going to go ahead and merge this once tests pass. I believe that all of the feedback was addressed. If there are any further changes we can address that in a future PR. |
This pull request effectively disables a user if their password appears in the HIBP data until they reset their password. Whenever the user attempts to authenticate, we will check their password using the HIBP breached password service, and if it comes back as a positive hit we will disable their password, preventing any future attempts to authenticate, send them an email with information, and finally return an error ultimately preventing them from authenticating.
While we can't iterate over our database and look for any users that have compromised credentials, this will effectively do the same thing since once a password has appeared in the HIBP data, it will no longer be usable to log into PyPI.
This will be in effect both for users authenticating using the log in form in the UI and for users uploading files to PyPI.
This is a fairly abrupt interruption to the user's flow-- they are attempting to do something and we forcibly prevent them and require them to do something else first. However this is considered to be an acceptable trade off in this case, because these password are known to the general public, and can be used in credential stuffing attacks on PyPI. This was recently the cause of a fairly large security issue on NPM, where reused credentials by one of the users for a popular package were used to upload malicious packages (more information).
This branch is currently working, however it still needs:
password, which doesn't work because it has been compromised. Likely we should just have a no-op breached password service in local development.Maybe give a different error when we're failing during authentication versus when changing the password? Consistency in error message makes it easier to google the problem, but the error message is more generic and harder to know exactly what just happened.I'm submitting this PR now, so that people can start to review the messaging that we're sending to users. Particularly the emails that i've written (plain text and HTML) and the additional information I've added to the FAQ.
Fixes #4471.