CVE-2021-43777 (published 2021-11-24T16:15Z)

Redash is a package for data visualization and sharing. In Redash version 10.0 and prior, the implementation of Google Login (via OAuth) incorrectly uses the `state` parameter to pass the next URL to redirect the user to after login. The `state` parameter should be used for a Cross-Site Request Forgery (CSRF) token, not a static and easily predicted value. This vulnerability does not affect users who do not use Google Login for their instance of Redash. A patch in the `master` and `release/10.x.x` branches addresses this by replacing `Flask-Oauthlib` with `Authlib` which automatically provides and validates a CSRF token for the state variable. The new implementation stores the next URL on the user session object. As a workaround, one may disable Google Login to mitigate the vulnerability.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/getredash/redash
commit_details: repo_url: https://github.com/getredash/redash
author: Jesse
author_date: 2021-11-23 16:22:02-06:00
msg: Merge pull request from GHSA-vhc7-w7r8-8m34

* WIP: break the flask_oauthlib behavior

* Refactor google-oauth to use cryptographic state.

* Clean up comments

* Fix: tests didn't pass because of the scope issues.

Moved outside the create_blueprint method because this does not depend
on the Authlib object.

* Apply Arik's fixes. Tests pass.
repository: repo_url: https://github.com/getredash/redash
repo_name: getredash/redash
description: Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data.
repo_language: Python
owner: getredash
file_changes: filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -243,12 +243,13 @@ def logout_and_redirect_to_index():
 
 def init_app(app):
     from redash.authentication import (
-        google_oauth,
         saml_auth,
         remote_user_auth,
         ldap_auth,
     )
 
+    from redash.authentication.google_oauth import create_google_oauth_blueprint
+
     login_manager.init_app(app)
     login_manager.anonymous_user = models.AnonymousUser
     login_manager.REMEMBER_COOKIE_DURATION = settings.REMEMBER_COOKIE_DURATION
@@ -259,8 +260,9 @@ def init_app(app):
         app.permanent_session_lifetime = timedelta(seconds=settings.SESSION_EXPIRY_TIME)
 
     from redash.security import csrf
-    for auth in [google_oauth, saml_auth, remote_user_auth, ldap_auth]:
-        blueprint = auth.blueprint
+
+    # Authlib's flask oauth client requires a Flask app to initialize
+    for blueprint in [create_google_oauth_blueprint(app), saml_auth.blueprint, remote_user_auth.blueprint, ldap_auth.blueprint, ]:
         csrf.exempt(blueprint)
         app.register_blueprint(blueprint)
 

programming_language: Python
filename: google_oauth.py
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
 import logging
 import requests
 from flask import redirect, url_for, Blueprint, flash, request, session
-from flask_oauthlib.client import OAuth
+
 
 from redash import models, settings
 from redash.authentication import (
@@ -11,42 +11,7 @@ from redash.authentication import (
 )
 from redash.authentication.org_resolving import current_org
 
-logger = logging.getLogger("google_oauth")
-
-oauth = OAuth()
-blueprint = Blueprint("google_oauth", __name__)
-
-
-def google_remote_app():
-    if "google" not in oauth.remote_apps:
-        oauth.remote_app(
-            "google",
-            base_url="https://www.google.com/accounts/",
-            authorize_url="https://accounts.google.com/o/oauth2/auth?prompt=select_account+consent",
-            request_token_url=None,
-            request_token_params={
-                "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
-            },
-            access_token_url="https://accounts.google.com/o/oauth2/token",
-            access_token_method="POST",
-            consumer_key=settings.GOOGLE_CLIENT_ID,
-            consumer_secret=settings.GOOGLE_CLIENT_SECRET,
-        )
-
-    return oauth.google
-
-
-def get_user_profile(access_token):
-    headers = {"Authorization": "OAuth {}".format(access_token)}
-    response = requests.get(
-        "https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
-    )
-
-    if response.status_code == 401:
-        logger.warning("Failed getting user profile (response code 401).")
-        return None
-
-    return response.json()
+from authlib.integrations.flask_client import OAuth
 
 
 def verify_profile(org, profile):
@@ -65,60 +30,102 @@ def verify_profile(org, profile):
     return False
 
 
-@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
-def org_login(org_slug):
-    session["org_slug"] = current_org.slug
-    return redirect(url_for(".authorize", next=request.args.get("next", None)))
+def create_google_oauth_blueprint(app):
+    oauth = OAuth(app)
 
+    logger = logging.getLogger("google_oauth")
+    blueprint = Blueprint("google_oauth", __name__)
 
-@blueprint.route("/oauth/google", endpoint="authorize")
-def login():
-    callback = url_for(".callback", _external=True)
-    next_path = request.args.get(
-        "next", url_for("redash.index", org_slug=session.get("org_slug"))
+    CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
+    oauth = OAuth(app)
+    oauth.register(
+        name="google",
+        server_metadata_url=CONF_URL,
+        client_kwargs={"scope": "openid email profile"},
     )
-    logger.debug("Callback url: %s", callback)
-    logger.debug("Next is: %s", next_path)
-    return google_remote_app().authorize(callback=callback, state=next_path)
-
-
-@blueprint.route("/oauth/google_callback", endpoint="callback")
-def authorized():
-    resp = google_remote_app().authorized_response()
-    access_token = resp["access_token"]
-
-    if access_token is None:
-        logger.warning("Access token missing in call back request.")
-        flash("Validation error. Please retry.")
-        return redirect(url_for("redash.login"))
-
-    profile = get_user_profile(access_token)
-    if profile is None:
-        flash("Validation error. Please retry.")
-        return redirect(url_for("redash.login"))
-
-    if "org_slug" in session:
-        org = models.Organization.get_by_slug(session.pop("org_slug"))
-    else:
-        org = current_org
-
-    if not verify_profile(org, profile):
-        logger.warning(
-            "User tried to login with unauthorized domain name: %s (org: %s)",
-            profile["email"],
-            org,
+
+    def get_user_profile(access_token):
+        headers = {"Authorization": "OAuth {}".format(access_token)}
+        response = requests.get(
+            "https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
         )
-        flash("Your Google Apps account ({}) isn't allowed.".format(profile["email"]))
-        return redirect(url_for("redash.login", org_slug=org.slug))
 
-    picture_url = "%s?sz=40" % profile["picture"]
-    user = create_and_login_user(org, profile["name"], profile["email"], picture_url)
-    if user is None:
-        return logout_and_redirect_to_index()
+        if response.status_code == 401:
+            logger.warning("Failed getting user profile (response code 401).")
+            return None
 
-    unsafe_next_path = request.args.get("state") or url_for(
-        "redash.index", org_slug=org.slug
-    )
-    next_path = get_next_path(unsafe_next_path)
+        return response.json()
+
+    @blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
+    def org_login(org_slug):
+        session["org_slug"] = current_org.slug
+        return redirect(url_for(".authorize", next=request.args.get("next", None)))
+
+    @blueprint.route("/oauth/google", endpoint="authorize")
+    def login():
+
+        redirect_uri = url_for(".callback", _external=True)
+
+        next_path = request.args.get(
+            "next", url_for("redash.index", org_slug=session.get("org_slug"))
+        )
+        logger.debug("Callback url: %s", redirect_uri)
+        logger.debug("Next is: %s", next_path)
+
+        session["next_url"] = next_path
+
+        return oauth.google.authorize_redirect(redirect_uri)
+
+    @blueprint.route("/oauth/google_callback", endpoint="callback")
+    def authorized():
+
+        logger.debug("Authorized user inbound")
+
+        resp = oauth.google.authorize_access_token()
+        user = resp.get("userinfo")
+        if user:
+            session["user"] = user
+
+        access_token = resp["access_token"]
+
+        if access_token is None:
+            logger.warning("Access token missing in call back request.")
+            flash("Validation error. Please retry.")
+            return redirect(url_for("redash.login"))
+
+        profile = get_user_profile(access_token)
+        if profile is None:
+            flash("Validation error. Please retry.")
+            return redirect(url_for("redash.login"))
+
+        if "org_slug" in session:
+            org = models.Organization.get_by_slug(session.pop("org_slug"))
+        else:
+            org = current_org
+
+        if not verify_profile(org, profile):
+            logger.warning(
+                "User tried to login with unauthorized domain name: %s (org: %s)",
+                profile["email"],
+                org,
+            )
+            flash(
+                "Your Google Apps account ({}) isn't allowed.".format(profile["email"])
+            )
+            return redirect(url_for("redash.login", org_slug=org.slug))
+
+        picture_url = "%s?sz=40" % profile["picture"]
+        user = create_and_login_user(
+            org, profile["name"], profile["email"], picture_url
+        )
+        if user is None:
+            return logout_and_redirect_to_index()
+
+        unsafe_next_path = session.get("next_url") or url_for(
+            "redash.index", org_slug=org.slug
+        )
+        next_path = get_next_path(unsafe_next_path)
+
+        return redirect(next_path)
 
-    return redirect(next_path)
+    return blueprint

programming_language: Python
filename: requirements.txt
change_type: ModificationType.MODIFY
diff: @@ -8,9 +8,6 @@ httplib2==0.14.0
 wtforms==2.2.1
 Flask-RESTful==0.3.7
 Flask-Login==0.4.1
-Flask-OAuthLib==0.9.5
-# pin this until https://github.com/lepture/flask-oauthlib/pull/388 is released
-requests-oauthlib>=0.6.2,<1.2.0
 Flask-SQLAlchemy==2.4.1
 Flask-Migrate==2.5.2
 flask-mail==0.9.1
@@ -67,3 +64,4 @@ werkzeug==0.16.1
 # Uncomment the requirement for ldap3 if using ldap.
 # It is not included by default because of the GPL license conflict.
 # ldap3==2.2.4
+Authlib==0.15.5
\ No newline at end of file

programming_language: Batchfile