diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 89515862..326b851d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ This document describes changes between each past release. Breaking changes ---------------- +- Enable session cookie security by default (#845) - Drop support for Python 2 (#483) - Drop support for Python 3.5 (#571) - Drop support for MySQL (#743) @@ -25,6 +26,7 @@ Security - Add CSRF validation on destructive actions (#796) - Ask for private code to delete project or project history (#796) +- Add headers to mitigate Clickjacking, XSS, and other attacks: `X-Frame-Options`, `X-XSS-Protection`, `X-Content-Type-Options`, `Content-Security-Policy`, `Referrer-Policy` (#845) Added ----- diff --git a/conf/entrypoint.sh b/conf/entrypoint.sh index dfe5e12c..e66ad7c8 100755 --- a/conf/entrypoint.sh +++ b/conf/entrypoint.sh @@ -21,6 +21,7 @@ ADMIN_PASSWORD = '$ADMIN_PASSWORD' ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD BABEL_DEFAULT_TIMEZONE = "$BABEL_DEFAULT_TIMEZONE" +SESSION_COOKIE_SECURE = $SESSION_COOKIE_SECURE EOF # Start gunicorn without forking diff --git a/docs/configuration.rst b/docs/configuration.rst index 29ab34f7..d29ef9f1 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -64,6 +64,21 @@ of the secret key could easily access any project and bypass the private code ve - **Production value:** `ihatemoney conf-example ihatemoney.cfg` sets it to something random, which is good. +`SESSION_COOKIE_SECURE` +----------------------- + +A boolean that controls whether the session cookie will be marked "secure". +If this is the case, browsers will refuse to send the session cookie over plain HTTP. + +- **Default value:** ``True`` +- **Production value:** ``True`` if you run your service over HTTPS, ``False`` if you run + your service over plain HTTP. + +Note: this setting is actually interpreted by Flask, see the +`Flask documentation`_ for details. + +.. _Flask documentation: https://flask.palletsprojects.com/en/2.0.x/config/#SESSION_COOKIE_SECURE + `MAIL_DEFAULT_SENDER` --------------------- diff --git a/docs/contributing.rst b/docs/contributing.rst index 80174b76..5d597b25 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -104,12 +104,20 @@ You can create a ``settings.cfg`` file, with the following content:: DEBUG = True SQLACHEMY_ECHO = DEBUG -You can also set the `TESTING` flag to `True` so no mails are sent -(and no exception is raised) while you're on development mode. Then before running the application, declare its path with :: export IHATEMONEY_SETTINGS_FILE_PATH="$(pwd)/settings.cfg" +You can also set the ``TESTING`` flag to ``True`` so no mails are sent +(and no exception is raised) while you're on development mode. + +In some cases, you may need to disable secure cookies by setting +``SESSION_COOKIE_SECURE`` to ``False``. This is needed if you +access your dev server over the network: with the default value +of ``SESSION_COOKIE_SECURE``, the browser will refuse to send +the session cookie over insecure HTTP, so many features of Ihatemoney +won't work (project login, language change, etc). + .. _contributing-developer: Contributing as a developer diff --git a/docs/upgrade.rst b/docs/upgrade.rst index ec846324..53185413 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -65,6 +65,17 @@ If so, pick the ``pip`` commands to use in the relevant section(s) of Then follow :ref:`general-procedure` from step 1. in order to complete the update. +Disable session cookie security if running over plain HTTP +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. note:: If you are running Ihatemoney over HTTPS, no special action is required. + +Session cookies are now marked "secure" by default to increase security. + +If you run Ihatemoney over plain HTTP, you need to explicitly disable this security +feature by setting ``SESSION_COOKIE_SECURE`` to ``False``, see :ref:`configuration`. + + Switch to MariaDB >= 10.3.2 instead of MySQL ++++++++++++++++++++++++++++++++++++++++++++ diff --git a/ihatemoney/conf-templates/ihatemoney.cfg.j2 b/ihatemoney/conf-templates/ihatemoney.cfg.j2 index 0188c6b1..13a8e9f5 100644 --- a/ihatemoney/conf-templates/ihatemoney.cfg.j2 +++ b/ihatemoney/conf-templates/ihatemoney.cfg.j2 @@ -38,3 +38,7 @@ ACTIVATE_ADMIN_DASHBOARD = False # You can change the timezone used to display time. By default it will be #derived from the server OS. #BABEL_DEFAULT_TIMEZONE = "Europe/Paris" + +# Enable secure cookies. Requires HTTPS. Disable if you run your ihatemoney +# service over plain HTTP. +SESSION_COOKIE_SECURE = True diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py index 9050bbeb..96795a01 100644 --- a/ihatemoney/default_settings.py +++ b/ihatemoney/default_settings.py @@ -8,6 +8,7 @@ ACTIVATE_DEMO_PROJECT = True ADMIN_PASSWORD = "" ALLOW_PUBLIC_PROJECT_CREATION = True ACTIVATE_ADMIN_DASHBOARD = False +SESSION_COOKIE_SECURE = True SUPPORTED_LANGUAGES = [ "de", "el", diff --git a/ihatemoney/run.py b/ihatemoney/run.py index c8fc5b25..cea6f93e 100644 --- a/ihatemoney/run.py +++ b/ihatemoney/run.py @@ -7,6 +7,7 @@ from flask import Flask, g, render_template, request, session from flask_babel import Babel, format_currency from flask_mail import Mail from flask_migrate import Migrate, stamp, upgrade +from flask_talisman import Talisman from jinja2 import pass_context from markupsafe import Markup import pytz @@ -126,6 +127,24 @@ def create_app( instance_relative_config=instance_relative_config, ) + # If we need to load external JS/CSS/image resources, it needs to be added here, see + # https://github.com/wntrblm/flask-talisman#content-security-policy + csp = { + "default-src": ["'self'"], + # We have several inline javascript scripts :( + "script-src": ["'self'", "'unsafe-inline'"], + "object-src": "'none'", + } + + Talisman( + app, + # Forcing HTTPS is the job of a reverse proxy + force_https=False, + # This is handled separately through the SESSION_COOKIE_SECURE Flask setting + session_cookie_secure=False, + content_security_policy=csp, + ) + # If a configuration object is passed, use it. Otherwise try to find one. load_configuration(app, configuration) app.wsgi_app = PrefixedWSGI(app) diff --git a/setup.cfg b/setup.cfg index 3bbf6f27..f58c1619 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ install_requires = Flask-Migrate>=2.5.3,<4 # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b) Flask-RESTful>=0.3.9,<1 Flask-SQLAlchemy>=2.4,<3 + Flask-Talisman>=0.8,<1 Flask-WTF>=0.14.3,<1 WTForms>=2.3.1,<2.4 Flask>=2,<3