diff --git a/flask_oidc/views.py b/flask_oidc/views.py index 0e87ae3..e0c49ce 100644 --- a/flask_oidc/views.py +++ b/flask_oidc/views.py @@ -6,6 +6,7 @@ import logging import warnings +import re from urllib.parse import urlparse from authlib.integrations.base_client.errors import OAuthError @@ -34,6 +35,30 @@ auth_routes = Blueprint("oidc_auth", __name__) +def validate_return_url(next, url_root): + if next == url_root: + return next + if not re.match(r"^[a-zA-Z0-9:\/.\-@%?!&+#_=*~']{2,256}$", next): + logger.debug("The redirect url you provided contains invalid characters") + return url_root + + temp_url = next + if not next.startswith(('http://', 'https://')): + # add a scheme for urlparse + temp_url = 'http://' + next + + parsed_url = urlparse(temp_url) + parsed_root = urlparse(url_root) + if not parsed_url.netloc and parsed_url.path.startswith('/'): + # this is a valid relative url + return next + if parsed_url.netloc == parsed_root.netloc: + # netloc should match for valid absolute urls + return next + logger.debug("The redirect url you provided is invalid") + return url_root + + @auth_routes.route("/login", endpoint="login") def login_view(): if current_app.config["OIDC_OVERWRITE_REDIRECT_URI"]: @@ -44,7 +69,8 @@ def login_view(): ) else: redirect_uri = url_for("oidc_auth.authorize", _external=True) - session["next"] = request.args.get("next", request.url_root) + next = request.args.get("next", request.url_root) + session["next"] = validate_return_url(next, request.url_root) before_login_redirect.send( g._oidc_auth, redirect_uri=redirect_uri, @@ -99,7 +125,8 @@ def logout_view(): flash("Your session expired, please reconnect.") else: flash("You were successfully logged out.") - return_to = request.args.get("next", request.url_root) + next = request.args.get("next", request.url_root) + return_to = validate_return_url(next, request.url_root) after_logout.send(g._oidc_auth, reason=reason, return_to=return_to) return redirect(return_to) diff --git a/tests/test_views.py b/tests/test_views.py index 51b816f..1958aa4 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -17,6 +17,7 @@ before_authorize, before_logout, ) +from flask_oidc.views import validate_return_url HAS_MULTIPLE_CONTEXT_MANAGERS = sys.hexversion >= 0x030900F0 # 3.9.0 @@ -132,3 +133,22 @@ def test_oidc_callback_route(test_app, client, dummy_token): resp = client.get("/oidc_callback?state=dummy-state&code=dummy-code") assert resp.status_code == 302 assert resp.location == "/authorize?state=dummy-state&code=dummy-code" + + +def test_logout_return_url_invalid(client, dummy_token): + with client.session_transaction() as session: + session["oidc_auth_token"] = dummy_token + session["oidc_auth_profile"] = {"nickname": "dummy"} + response = client.get("/logout?next=https://www.google.com") + assert response.status_code == 302 + assert response.location == "http://localhost/" + + +def test_validate_return_url(): + url_root = "http://localhost/" + valid = ["/test/url", "http://localhost/", "http://localhost/test/url"] + invalid = ["test/url", "http://localhost1/", "https://www.google.com", "../../test"] + for valid_url in valid: + assert validate_return_url(valid_url, url_root) == valid_url + for invalid_url in invalid: + assert validate_return_url(invalid_url, url_root) == url_root