You've already forked Mailu
mirror of
https://github.com/Mailu/Mailu.git
synced 2025-07-01 00:44:57 +02:00
Merge #1783
1783: Switch to server-side sessions r=mergify[bot] a=nextgens ## What type of PR? bug-fix ## What does this PR do? It simplifies session management. - it ensures that sessions will eventually expire (*) - it implements some mitigation against session-fixation attacks - it switches from client-side to server-side sessions (in Redis) It doesn't prevent us from (re)-implementing a "remember_me" type of feature if that's considered useful by some. Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
This commit is contained in:
@ -1,5 +1,8 @@
|
|||||||
import flask
|
import flask
|
||||||
import flask_bootstrap
|
import flask_bootstrap
|
||||||
|
import redis
|
||||||
|
from flask_kvsession import KVSessionExtension
|
||||||
|
from simplekv.memory.redisstore import RedisStore
|
||||||
|
|
||||||
from mailu import utils, debug, models, manage, configuration
|
from mailu import utils, debug, models, manage, configuration
|
||||||
|
|
||||||
@ -17,6 +20,7 @@ def create_app_from_config(config):
|
|||||||
# Initialize application extensions
|
# Initialize application extensions
|
||||||
config.init_app(app)
|
config.init_app(app)
|
||||||
models.db.init_app(app)
|
models.db.init_app(app)
|
||||||
|
KVSessionExtension(RedisStore(redis.StrictRedis().from_url('redis://{0}/3'.format(config['REDIS_ADDRESS']))), app).cleanup_sessions(app)
|
||||||
utils.limiter.init_app(app)
|
utils.limiter.init_app(app)
|
||||||
utils.babel.init_app(app)
|
utils.babel.init_app(app)
|
||||||
utils.login.init_app(app)
|
utils.login.init_app(app)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from socrate import system
|
from socrate import system
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
@ -54,6 +55,7 @@ DEFAULT_CONFIG = {
|
|||||||
'RECAPTCHA_PRIVATE_KEY': '',
|
'RECAPTCHA_PRIVATE_KEY': '',
|
||||||
# Advanced settings
|
# Advanced settings
|
||||||
'LOG_LEVEL': 'WARNING',
|
'LOG_LEVEL': 'WARNING',
|
||||||
|
'SESSION_LIFETIME': 24,
|
||||||
'SESSION_COOKIE_SECURE': True,
|
'SESSION_COOKIE_SECURE': True,
|
||||||
'CREDENTIAL_ROUNDS': 12,
|
'CREDENTIAL_ROUNDS': 12,
|
||||||
# Host settings
|
# Host settings
|
||||||
@ -136,6 +138,8 @@ class ConfigManager(dict):
|
|||||||
self.config['QUOTA_STORAGE_URL'] = 'redis://{0}/1'.format(self.config['REDIS_ADDRESS'])
|
self.config['QUOTA_STORAGE_URL'] = 'redis://{0}/1'.format(self.config['REDIS_ADDRESS'])
|
||||||
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
||||||
self.config['SESSION_COOKIE_HTTPONLY'] = True
|
self.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||||
|
self.config['SESSION_KEY_BITS'] = 128
|
||||||
|
self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME']))
|
||||||
# update the app config itself
|
# update the app config itself
|
||||||
app.config = self
|
app.config = self
|
||||||
|
|
||||||
|
@ -46,6 +46,8 @@ class ConfirmationForm(flask_wtf.FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class LoginForm(flask_wtf.FlaskForm):
|
class LoginForm(flask_wtf.FlaskForm):
|
||||||
|
class Meta:
|
||||||
|
csrf = False
|
||||||
email = fields.StringField(_('E-mail'), [validators.Email()])
|
email = fields.StringField(_('E-mail'), [validators.Email()])
|
||||||
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
|
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
|
||||||
submit = fields.SubmitField(_('Sign in'))
|
submit = fields.SubmitField(_('Sign in'))
|
||||||
|
@ -17,6 +17,7 @@ def login():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = models.User.login(form.email.data, form.pw.data)
|
user = models.User.login(form.email.data, form.pw.data)
|
||||||
if user:
|
if user:
|
||||||
|
flask.session.regenerate()
|
||||||
flask_login.login_user(user)
|
flask_login.login_user(user)
|
||||||
endpoint = flask.request.args.get('next', '.index')
|
endpoint = flask.request.args.get('next', '.index')
|
||||||
return flask.redirect(flask.url_for(endpoint)
|
return flask.redirect(flask.url_for(endpoint)
|
||||||
@ -30,6 +31,7 @@ def login():
|
|||||||
@access.authenticated
|
@access.authenticated
|
||||||
def logout():
|
def logout():
|
||||||
flask_login.logout_user()
|
flask_login.logout_user()
|
||||||
|
flask.session.destroy()
|
||||||
return flask.redirect(flask.url_for('.index'))
|
return flask.redirect(flask.url_for('.index'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,6 +119,7 @@ def user_password(user_email):
|
|||||||
if form.pw.data != form.pw2.data:
|
if form.pw.data != form.pw2.data:
|
||||||
flask.flash('Passwords do not match', 'error')
|
flask.flash('Passwords do not match', 'error')
|
||||||
else:
|
else:
|
||||||
|
flask.session.regenerate()
|
||||||
user.set_password(form.pw.data)
|
user.set_password(form.pw.data)
|
||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
flask.flash('Password updated for %s' % user)
|
flask.flash('Password updated for %s' % user)
|
||||||
@ -186,6 +187,7 @@ def user_signup(domain_name=None):
|
|||||||
if domain.has_email(form.localpart.data):
|
if domain.has_email(form.localpart.data):
|
||||||
flask.flash('Email is already used', 'error')
|
flask.flash('Email is already used', 'error')
|
||||||
else:
|
else:
|
||||||
|
flask.session.regenerate()
|
||||||
user = models.User(domain=domain)
|
user = models.User(domain=domain)
|
||||||
form.populate_obj(user)
|
form.populate_obj(user)
|
||||||
user.set_password(form.pw.data)
|
user.set_password(form.pw.data)
|
||||||
|
@ -13,6 +13,7 @@ Flask==1.0.2
|
|||||||
Flask-Babel==0.12.2
|
Flask-Babel==0.12.2
|
||||||
Flask-Bootstrap==3.3.7.1
|
Flask-Bootstrap==3.3.7.1
|
||||||
Flask-DebugToolbar==0.10.1
|
Flask-DebugToolbar==0.10.1
|
||||||
|
Flask-KVSession==0.6.2
|
||||||
Flask-Limiter==1.0.1
|
Flask-Limiter==1.0.1
|
||||||
Flask-Login==0.4.1
|
Flask-Login==0.4.1
|
||||||
Flask-Migrate==2.4.0
|
Flask-Migrate==2.4.0
|
||||||
|
@ -3,6 +3,7 @@ Flask-Login
|
|||||||
Flask-SQLAlchemy
|
Flask-SQLAlchemy
|
||||||
Flask-bootstrap
|
Flask-bootstrap
|
||||||
Flask-Babel
|
Flask-Babel
|
||||||
|
Flask-KVSession
|
||||||
Flask-migrate
|
Flask-migrate
|
||||||
Flask-script
|
Flask-script
|
||||||
Flask-wtf
|
Flask-wtf
|
||||||
|
@ -149,6 +149,8 @@ The ``CREDENTIAL_ROUNDS`` (default: 12) setting is the number of rounds used by
|
|||||||
|
|
||||||
The ``SESSION_COOKIE_SECURE`` (default: True) setting controls the secure flag on the cookies of the administrative interface. It should only be turned off if you intend to access it over plain HTTP.
|
The ``SESSION_COOKIE_SECURE`` (default: True) setting controls the secure flag on the cookies of the administrative interface. It should only be turned off if you intend to access it over plain HTTP.
|
||||||
|
|
||||||
|
``SESSION_LIFETIME`` (default: 24) is the length in hours a session is valid for on the administrative interface.
|
||||||
|
|
||||||
The ``LOG_LEVEL`` setting is used by the python start-up scripts as a logging threshold.
|
The ``LOG_LEVEL`` setting is used by the python start-up scripts as a logging threshold.
|
||||||
Log messages equal or higher than this priority will be printed.
|
Log messages equal or higher than this priority will be printed.
|
||||||
Can be one of: CRITICAL, ERROR, WARNING, INFO, DEBUG or NOTSET.
|
Can be one of: CRITICAL, ERROR, WARNING, INFO, DEBUG or NOTSET.
|
||||||
|
1
towncrier/newsfragments/1783.misc
Normal file
1
towncrier/newsfragments/1783.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Switch from client side sessions (cookies) to server-side sessions (Redis). This simplies the security model a lot and allows for an easier recovery should a cookie ever land in the hands of an attacker.
|
Reference in New Issue
Block a user