question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Wrong error: SECURITY_PASSWORD_SALT must not be None when SECURITY_PASSWORD_HASH set to "plaintext"

See original GitHub issue

The auth token retrieval does not work in a testing environment with SECURITY_PASSWORD_HASH being set to ‘plaintext’. Using ‘plaintext’ makes sense for running unit tests (the crypt routines otherwise may significantly affect the duration it takes to run the tests).

Consider this small application:

from flask import Flask, render_template, g
from flask.ext.sqlalchemy import SQLAlchemy
import flask.ext.security
from flask.ext.security import UserMixin, RoleMixin
from flask.ext.security.registerable import register_user

auth_required = flask.ext.security.auth_token_required
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECURITY_SEND_REGISTER_EMAIL'] = False
app.config['SECRET_KEY'] = 'a'
app.config['WTF_CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite://"

db = SQLAlchemy(app)

roles_users = db.Table('roles_users',
    db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
    db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))

class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship(
        'Role',
        secondary=roles_users,
        backref=db.backref('users', lazy='dynamic')
        )

user_datastore = flask.ext.security.SQLAlchemyUserDatastore(db, User, Role)
security = flask.ext.security.Security(app, user_datastore)

@app.route('/createtestuser')
def create_user():
    db.drop_all()
    db.create_all()
    register_user(email="mail1", password="testpassword1")
    return "OK"

app.run(debug=True, host="0.0.0.0")

Create a test user:

$ curl -i http://localhost:5000/createtestuser                                                                  HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 2
Set-Cookie: session=eyJfaWQiOnsiIGIiOiJNR0ZtTVRjd1lqVTRZMkppTjJFMk0yTm1NMll4TkRKbFltVXpNR0l5WW1FPSJ9fQ.CCu0Ww.dnadUzB93ML3oZt2EdrpbFS5bnA; HttpOnly; Path=/
Server: Werkzeug/0.10.4 Python/2.7.3
Date: Wed, 06 May 2015 14:19:07 GMT

OK

Attempt to retrieve auth token:

$ curl -i --data '{"password": "testpassword1", "email": "mail1"}' http://localhost:5000/login  -H "Content-Type: application/json"

Yields the following traceback:

Traceback (most recent call last):
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1836, in __call__
    return self.wsgi_app(environ, start_response)
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/xxx/venv/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/xxx/venv/lib/python2.7/site-packages/flask_security/decorators.py", line 205, in wrapper
    return f(*args, **kwargs)
  File "/xxx/venv/lib/python2.7/site-packages/flask_security/views.py", line 79, in login
    if form.validate_on_submit():
  File "/xxx/venv/lib/python2.7/site-packages/flask_wtf/form.py", line 166, in validate_on_submit
    return self.is_submitted() and self.validate()
  File "/xxx/venv/lib/python2.7/site-packages/flask_security/forms.py", line 238, in validate
    if not verify_and_update_password(self.password.data, self.user):
  File "/xxx/venv/lib/python2.7/site-packages/flask_security/utils.py", line 137, in verify_and_update_password
    password = get_hmac(password)
  File "/xxx/venv/lib/python2.7/site-packages/flask_security/utils.py", line 110, in get_hmac
    'set to "%s"' % _security.password_hash)
RuntimeError: The configuration value `SECURITY_PASSWORD_SALT` must not be None when the value of `SECURITY_PASSWORD_HASH` is set to "plaintext"

This error message is contradictory to the documentation which clearly states that SECURITY_PASSWORD_SALT “Specifies the HMAC salt. This is only used if the password hash type is set to something other than plain text. Defaults to None.”

So I set a salt which theoretically could work with the plaintext hashing method:

app.config['SECURITY_PASSWORD_SALT'] = b"xxx"

(I still leave the default for SECURITY_PASSWORD_HASH which is ‘plaintext’). The traceback disappears, but now the password validation is broken:

$ curl -i --data '{"password": "testpassword1", "email": "mail1"}' http://localhost:5000/login  -H "Content-Type: application/json"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 134
Set-Cookie: session=eyJfaWQiOnsiIGIiOiJNR0ZtTVRjd1lqVTRZMkppTjJFMk0yTm1NMll4TkRKbFltVXpNR0l5WW1FPSJ9fQ.CCu1kA.pNVcVQFzFV-tfK2KAPj2pCMivko; HttpOnly; Path=/
Server: Werkzeug/0.10.4 Python/2.7.3
Date: Wed, 06 May 2015 14:24:16 GMT

{
  "meta": {
    "code": 400
  },
  "response": {
    "errors": {
      "password": [
        "Invalid password"
      ]
    }
  }
}

That is, the password validation method does not properly account for this scenario, right?

So I set a hashing method different from plaintext:

app.config['SECURITY_PASSWORD_HASH'] = "sha512_crypt"

Now the password validation works and I retrieve an auth token. But, really, this should work with the default settings (no hash method, no salt), or there should be an improved error message.

As a side note: As you can see above, the response always contains a Set-Cookie: session=eyJfaWQiOnsiIGIi...... header. Why is that? Is that proper behavior? Does it come from the Flask-Login extension? Its documentation states that it only sets a cookie when using remember=True upon executing the login routine. How can I make the minimal app shown above make not set a cookie upon each request? Should I open another issue for that?

Issue Analytics

  • State:closed
  • Created 8 years ago
  • Comments:5

github_iconTop GitHub Comments

1reaction
yueranyuancommented, Jan 6, 2016

The reason that you have having these errors is because the password that you are using is misidentified by passlib. This can happen when your password choice happens to start with one of the strings that passlib uses to identifying hashing schemes e.g. $5$, $6$, $2$, $2a$, $2b$, $2x$, $2y$.

This causes Flask-Security to think that your password was saved using a hash and not plaintext.

if _pwd_context.identify(user.password) != 'plaintext':
        password = get_hmac(password)

As you can see Flask now thinks that the saved user.password is encoded with a hash (even if it’s not) and therefore that it was salted (even if it’s not). So in order to check your input password against the user.password in the system, it salts your input password. That is why it throws the “no salt” error. So you are correct in saying that the error message is ‘incorrect’ but Flask-Security /is/ legitimately trying to salt your password.

You might ask: So given passlib’s proclivity for false positives, why do we use it to identify password hashes at all? Isn’t the the SECURITY_PASSWORD_HASH flag enough to tell Flask-Security whether user.password is encoded in plaintext? Well technically yes, but the reason that passlib’s identify function is used is so that Flask-Security can gracefully deal with hashing algorithm changes i.e. if you change SECURITY_PASSWORD_HASH between runs, this identify call allows Flask-Security to load passwords using older hashing schemes and rehash it using your new hashing scheme.

This bug is more than just the error message, if you had a salt defined then Flask-Security would mistakenly salt your input password and it passlib would also misidentify the stored password as having been hashed rather than plaintext which may lead to a decoding error in passlib.

How can this be fixed? We can’t just change the error message or suppress the error for the reason given above. The source of the error is in a fundamental ambiguity in the way that password hashes are done. Because the hashing method is not stored with the password, after a hash has been written it’s impossible to know whether it was a plaintext password that looks like a hash or actually a hash. The easiest method imo is to output some kind of identifying string in front of plaintext passwords e.g. $plaintext$. But this could potentially be breaking for old code running on databases that don’t have the identifying string (which might be avoidable with some backwards-compatibility checks though I haven’t thought it through completely yet).

An alternative is to just make SECURITY_PASSWORD_HASH=plaintext indicate a second plaintext check. That is to say when config is set to plaintext, Flask-Security would put a try-catch around verify_and_update_password and in case of failure (possible misidentification), would just do a string match between the input password and the stored password without using passlib. This is a potential security flaw since someone with access to the hashed user.password would be able to unlock the account without the extra layer of security provided by the salt. But since plaintext mode is obviously insecure to begin with, this might not be horrible.

It might also be desirable to just store the password hash type in the db rather than relying on the identifier. But this also breaks backwards compatibility in a less comfortable way since it also forces a change to the db schema.

There are probably other workarounds that I haven’t thought of yet.

0reactions
jirikuncarcommented, Jun 14, 2017
  • SECURITY_PASSWORD_SALT closed via ff0ec74df0f36670e4a8216e1345b25810be2089
  • encrypt_password must be used in app context in order to get correct configuration (wontfix)
Read more comments on GitHub >

github_iconTop Results From Across the Web

create horizontal_pod_autoscaler error, conditions must not ...
create horizontal_pod_autoscaler error, conditions must not be None # ... must not be `None`") # noqa: E501 ValueError: Invalid value for ...
Read more >
Change the message format to HTML, Rich Text Format, or ...
If the recipient's email program is set to convert messages, for example, then a message you send formatted as HTML could be converted...
Read more >
Configuration — Flask-Security 5.0.2 documentation
If not None new/changed passwords will be checked against the database of breached ... If set to False then a user can register...
Read more >
Configuration — Flask-Security 3.0.0 documentation
SECURITY_PASSWORD_SALT, Specifies the HMAC salt. This is only used if the password hash type is set to something other than plain text. Defaults...
Read more >
BuiltIn - Robot Framework
If such an argument is given as a string, it is considered false if it is an empty string or equal to FALSE...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found