diff --git a/docs/docs/security/security.mdx b/docs/docs/security/security.mdx index 5934af51df0..870992fb930 100644 --- a/docs/docs/security/security.mdx +++ b/docs/docs/security/security.mdx @@ -6,7 +6,7 @@ sidebar_position: 1 ### Roles -Security in Superset is handled by Flask AppBuilder (FAB), an application development framework +Authentication and authorization in Superset is handled by Flask AppBuilder (FAB), an application development framework built on top of Flask. FAB provides authentication, user management, permissions and roles. Please read its [Security documentation](https://flask-appbuilder.readthedocs.io/en/latest/security.html). @@ -157,6 +157,34 @@ HTTPS if the cookie is marked “secure”. The application must be served over `PERMANENT_SESSION_LIFETIME`: (default: "31 days") The lifetime of a permanent session as a `datetime.timedelta` object. +#### Switching to server side sessions + +Server side sessions offer benefits over client side sessions on security and performance. +By enabling server side sessions, the session data is stored server side and only a session ID +is sent to the client. When a user logs in, a session is created server side and the session ID +is sent to the client in a cookie. The client will send the session ID with each request and the +server will use it to retrieve the session data. +On logout, the session is destroyed server side and the session cookie is deleted on the client side. +This reduces the risk for replay attacks and session hijacking. + +Superset uses [Flask-Session](https://flask-session.readthedocs.io/en/latest/) to manage server side sessions. +To enable this extension you have to set: + +```python +SESSION_SERVER_SIDE = True +``` + +Flask-Session offers multiple backend session interfaces for Flask, here's an example for Redis: + +```python +from redis import Redis + +SESSION_TYPE = "redis" +SESSION_REDIS = Redis(host="redis", port=6379, db=0) +# sign the session cookie sid +SESSION_USE_SIGNER = True +``` + ### Content Security Policy (CSP) Superset uses the [Talisman](https://pypi.org/project/flask-talisman/) extension to enable implementation of a @@ -177,8 +205,8 @@ It's extremely important to correctly configure a Content Security Policy when d prevent many types of attacks. Superset provides two variables in `config.py` for deploying a CSP: - `TALISMAN_ENABLED` defaults to `True`; set this to `False` in order to disable CSP -- `TALISMAN_CONFIG` holds the actual the policy definition (*see example below*) as well as any -other arguments to be passed to Talisman. +- `TALISMAN_CONFIG` holds the actual the policy definition (_see example below_) as well as any + other arguments to be passed to Talisman. When running in production mode, Superset will check at startup for the presence of a CSP. If one is not found, it will issue a warning with the security risks. For environments @@ -187,15 +215,15 @@ this warning using the `CONTENT_SECURITY_POLICY_WARNING` key in `config.py`. #### CSP Requirements -* Superset needs the `style-src unsafe-inline` CSP directive in order to operate. +- Superset needs the `style-src unsafe-inline` CSP directive in order to operate. ``` style-src 'self' 'unsafe-inline' ``` -* Only scripts marked with a [nonce](https://content-security-policy.com/nonce/) can be loaded and executed. -Nonce is a random string automatically generated by Talisman on each page load. -You can get current nonce value by calling jinja macro `csp_nonce()`. +- Only scripts marked with a [nonce](https://content-security-policy.com/nonce/) can be loaded and executed. + Nonce is a random string automatically generated by Talisman on each page load. + You can get current nonce value by calling jinja macro `csp_nonce()`. ``` ``` -- Some dashboards load images using data URIs and require `data:` in their `img-src` +* Some dashboards load images using data URIs and require `data:` in their `img-src` ``` img-src 'self' data: ``` -- MapBox charts use workers and need to connect to MapBox servers in addition to the Superset origin +* MapBox charts use workers and need to connect to MapBox servers in addition to the Superset origin ``` worker-src 'self' blob: connect-src 'self' https://api.mapbox.com https://events.mapbox.com ``` -* Other CSP directives default to `'self'` to limit content to the same origin as the Superset server. +- Other CSP directives default to `'self'` to limit content to the same origin as the Superset server. In order to adjust provided CSP configuration to your needs, follow the instructions and examples provided in [Content Security Policy Reference](https://content-security-policy.com/) - #### Other Talisman security considerations Setting `TALISMAN_ENABLED = True` will invoke Talisman's protection with its default arguments, @@ -231,7 +258,7 @@ These generally improve security, but administrators should be aware of their ex In particular, the option of `force_https = True` (`False` by default) may break Superset's Alerts & Reports if workers are configured to access charts via a `WEBDRIVER_BASEURL` beginning -with `http://`. As long as a Superset deployment enforces https upstream, e.g., +with `http://`. As long as a Superset deployment enforces https upstream, e.g., through a loader balancer or application gateway, it should be acceptable to keep this option disabled. Otherwise, you may want to enable `force_https` like this: diff --git a/requirements/base.txt b/requirements/base.txt index e4cb6eb58fb..b60c3513246 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -87,6 +87,7 @@ flask==2.2.5 # flask-limiter # flask-login # flask-migrate + # flask-session # flask-sqlalchemy # flask-wtf flask-appbuilder==4.3.10 @@ -107,6 +108,8 @@ flask-login==0.6.0 # flask-appbuilder flask-migrate==3.1.0 # via apache-superset +flask-session==0.5.0 + # via apache-superset flask-sqlalchemy==2.5.1 # via # flask-appbuilder diff --git a/requirements/testing.txt b/requirements/testing.txt index 95278b3ee86..f25be8a1ac5 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -117,6 +117,8 @@ protobuf==4.23.0 # proto-plus pydata-google-auth==1.7.0 # via pandas-gbq +pyee==9.0.4 + # via playwright pyfakefs==5.2.2 # via -r requirements/testing.in pyhive[presto]==0.6.5 diff --git a/setup.py b/setup.py index 89cd3f51f6d..2790d120340 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ setup( "flask-talisman>=1.0.0, <2.0", "flask-login>=0.6.0, < 1.0", "flask-migrate>=3.1.0, <4.0", + "flask-session>=0.4.0, <1.0", "flask-wtf>=1.1.0, <2.0", "func_timeout", "geopy", diff --git a/superset/config.py b/superset/config.py index d62136a0004..6454ba6b7da 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1462,6 +1462,18 @@ TALISMAN_DEV_CONFIG = { SESSION_COOKIE_HTTPONLY = True # Prevent cookie from being read by frontend JS? SESSION_COOKIE_SECURE = False # Prevent cookie from being transmitted over non-tls? SESSION_COOKIE_SAMESITE: Literal["None", "Lax", "Strict"] | None = "Lax" +# Whether to use server side sessions from flask-session or Flask secure cookies +SESSION_SERVER_SIDE = False +# Example config using Redis as the backend for server side sessions +# from flask_session import RedisSessionInterface +# +# SESSION_SERVER_SIDE = True +# SESSION_USE_SIGNER = True +# SESSION_TYPE = "redis" +# SESSION_REDIS = Redis(host="localhost", port=6379, db=0) +# +# Other possible config options and backends: +# # https://flask-session.readthedocs.io/en/latest/config.html # Cache static resources. SEND_FILE_MAX_AGE_DEFAULT = int(timedelta(days=365).total_seconds()) diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 326b7b47e70..a54adce7ba5 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -27,6 +27,7 @@ from flask import Flask, redirect from flask_appbuilder import expose, IndexView from flask_babel import gettext as __, lazy_gettext as _ from flask_compress import Compress +from flask_session import Session from werkzeug.middleware.proxy_fix import ProxyFix from superset.constants import CHANGE_ME_SECRET_KEY @@ -474,6 +475,10 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods logger.error("Refusing to start due to insecure SECRET_KEY") sys.exit(1) + def configure_session(self) -> None: + if self.config["SESSION_SERVER_SIDE"]: + Session(self.superset_app) + def init_app(self) -> None: """ Main entry point which will delegate to other methods in @@ -481,6 +486,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods """ self.pre_init() self.check_secret_key() + self.configure_session() # Configuration of logging must be done first to apply the formatter properly self.configure_logging() # Configuration of feature_flags must be done first to allow init features