CVE-2021-43777 (Python, published 2021-11-24T16:15Z)
Description: value: 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 v3 base score: 6.1)
Weakness: CWE-601 \u2014 URL Redirection to Untrusted Site ('Open Redirect')
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.
Repository: https://github.com/getredash/redash
Files changed:
redash/authentication/__init__.py
redash/authentication/google_oauth.py
requirements.txt
Commit message: 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.

Vulnerable code:
        google_oauth,
    for auth in [google_oauth, saml_auth, remote_user_auth, ldap_auth]:
        blueprint = auth.blueprint
from flask_oauthlib.client import OAuth
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()
@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():
    callback = 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", 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,
        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 = request.args.get("state") or url_for(
        "redash.index", org_slug=org.slug
    )
    next_path = get_next_path(unsafe_next_path)
    return redirect(next_path)
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

Fixed code:
    from redash.authentication.google_oauth import create_google_oauth_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, ]:

from authlib.integrations.flask_client import OAuth
def create_google_oauth_blueprint(app):
    oauth = OAuth(app)
    logger = logging.getLogger("google_oauth")
    blueprint = Blueprint("google_oauth", __name__)
    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"},

    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()

    @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 blueprint
Authlib==0.15.5