CVE-2021-23393 (published 2021-06-11T00:15Z)

This affects the package Flask-Unchained before 0.9.0. When using the the _validate_redirect_url function, it is possible to bypass URL validation and redirect a user to an arbitrary URL by providing multiple back slashes such as \\\\\\evil.com/path. This vulnerability is only exploitable if an alternative WSGI server other than Werkzeug is used, or the default behaviour of Werkzeug is modified using 'autocorrect_location_header=False.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 5.4, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/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/briancappello/flask-unchained
commit_details: repo_url: https://github.com/briancappello/flask-unchained
author: Brian Cappello
author_date: 2021-05-27 14:18:41-04:00
msg: add tests for validate_redirect_url
repository: repo_url: https://github.com/briancappello/flask-unchained
repo_name: briancappello/flask-unchained
description: The quickest and easiest way to build large web apps and APIs with Flask
repo_language: Python
owner: briancappello
file_changes: filename: utils.py
change_type: ModificationType.MODIFY
diff: @@ -185,7 +185,7 @@ def method_name_to_url(method_name) -> str:
 
 def encode_non_url_reserved_characters(url):
     # safe url reserved characters: https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
-    return urlquote(url, safe=":/?#[]@!$&'()*+,;=")
+    return urlquote(url, safe=":/?#[]@!$&'()*+,;=<>")
 
 
 # modified from flask_security.utils.get_post_action_redirect

programming_language: Python
filename: requirements.txt
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@ flask_migrate==2.7.0
 flask-oauthlib==0.9.5
 flask-principal==0.4.0
 flask-session==0.3.2
+flask-sqlalchemy==2.5.1
 flask-sqlalchemy-unchained==0.7.4
 flask_wtf==0.14.3
 graphene==2.1.8

programming_language: Shell
filename: test_utils.py
change_type: ModificationType.MODIFY
diff: @@ -190,6 +190,9 @@ class TestValidateRedirectUrl:
     def test_it_fails_on_garbage(self):
         assert _validate_redirect_url(None) is False
         assert _validate_redirect_url(' ') is False
+        assert _validate_redirect_url('///evil.com') is False
+        assert _validate_redirect_url('\\\\\\evil.com') is False
+        assert _validate_redirect_url('\x00evil.com') is False
 
     def test_it_fails_with_invalid_netloc(self, app, monkeypatch):
         with app.test_request_context():
@@ -197,17 +200,30 @@ class TestValidateRedirectUrl:
             assert _validate_redirect_url('http://fail.com') is False
             monkeypatch.undo()
 
-    @pytest.mark.options(EXTERNAL_SERVER_NAME='works.com')
+    def test_it_requires_same_scheme(self, app, monkeypatch):
+        with app.test_request_context():
+            monkeypatch.setattr('flask.request.host_url', 'https://example.com')
+            assert _validate_redirect_url('http://example.com/foo') is False
+            monkeypatch.undo()
+
+    @pytest.mark.options(EXTERNAL_SERVER_NAME='http://works.com')
     def test_it_works_with_external_server_name(self, app, monkeypatch):
         with app.test_request_context():
             monkeypatch.setattr('flask.request.host_url', 'http://example.com')
             assert _validate_redirect_url('http://works.com') is True
             monkeypatch.undo()
 
+    @pytest.mark.options(EXTERNAL_SERVER_NAME='https://works.com')
+    def test_it_requires_same_external_server_name_scheme(self, app, monkeypatch):
+        with app.test_request_context():
+            monkeypatch.setattr('flask.request.host_url', 'http://example.com')
+            assert _validate_redirect_url('http://works.com') is False
+            monkeypatch.undo()
+
     def test_it_works_with_explicit_external_host(self, app, monkeypatch):
         with app.test_request_context():
             monkeypatch.setattr('flask.request.host_url', 'http://example.com')
             result = _validate_redirect_url('http://works.com',
-                                            _external_host='works.com')
+                                            _external_host='http://works.com')
             assert result is True
             monkeypatch.undo()

programming_language: Python