Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions flask_appbuilder/utils/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from fnmatch import fnmatch
import logging
from typing import Any, Callable
import unicodedata
Expand Down Expand Up @@ -27,11 +28,17 @@ def is_safe_redirect_url(url: str) -> bool:
if not url_info.scheme and url_info.netloc:
scheme = "http"
valid_schemes = ["http", "https"]
host_url = urlparse(request.host_url)
return (not url_info.netloc or url_info.netloc == host_url.netloc) and (
not scheme or scheme in valid_schemes

safe_hosts = current_app.config.get("SAFE_REDIRECT_HOSTS", [])
if not safe_hosts:
safe_hosts = [urlparse(request.host_url).netloc]

is_host_allowed = not url_info.netloc or any(
fnmatch(url_info.netloc, pattern) for pattern in safe_hosts
)

return is_host_allowed and (not scheme or scheme in valid_schemes)


def get_safe_redirect(url):
if url and is_safe_redirect_url(url):
Expand Down
2 changes: 2 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def browser_login(
password: str,
next_url: Optional[str] = None,
follow_redirects: bool = True,
headers: Optional[dict] = None,
) -> Response:
login_url = "/login/"
if next_url:
Expand All @@ -83,6 +84,7 @@ def browser_login(
login_url,
data=dict(username=username, password=password),
follow_redirects=follow_redirects,
headers=headers or {},
)

def assert_response(
Expand Down
75 changes: 75 additions & 0 deletions tests/security/test_mvc_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,81 @@ def test_db_login_invalid_no_netloc_with_scheme_next_url(self):
)
assert response.location == "/"

def test_login_next_url_spoofed_host_header_disallowed(self):
"""
Ensure a spoofed Host header does not allow redirection to an untrusted domain
"""
self.app.config["SAFE_REDIRECT_HOSTS"] = ["localhost"] # trusted dev host
self.browser_logout(self.client)

response = self.browser_login(
self.client,
USERNAME_ADMIN,
PASSWORD_ADMIN,
next_url="https://example.com/",
follow_redirects=False,
headers={"Host": "example.com"},
)

assert response.status_code == 302
assert response.location == "/"

def test_login_next_url_spoofed_host_header_allowed_config(self):
"""
Ensure a spoofed Host header does not allow redirection to an untrusted domain
"""
self.app.config["SAFE_REDIRECT_HOSTS"] = ["localhost"] # trusted dev host
self.browser_logout(self.client)

response = self.browser_login(
self.client,
USERNAME_ADMIN,
PASSWORD_ADMIN,
next_url="https://localhost/something",
follow_redirects=False,
headers={"Host": "example.com"},
)

assert response.status_code == 302
assert response.location == "https://localhost/something"

def test_login_next_url_allowed_config_wildcard(self):
"""
Ensure a spoofed Host header does not allow redirection to an untrusted domain
"""
self.app.config["SAFE_REDIRECT_HOSTS"] = ["*.localhost"] # trusted dev host
self.browser_logout(self.client)

response = self.browser_login(
self.client,
USERNAME_ADMIN,
PASSWORD_ADMIN,
next_url="https://example.localhost/something",
follow_redirects=False,
headers={"Host": "example.localhost"},
)

assert response.status_code == 302
assert response.location == "https://example.localhost/something"

def test_login_next_url_spoofed_host_header_allowed(self):
"""
Ensure a spoofed Host header does not allow redirection to an untrusted domain
"""
self.browser_logout(self.client)

response = self.browser_login(
self.client,
USERNAME_ADMIN,
PASSWORD_ADMIN,
next_url="https://example.com/",
follow_redirects=False,
headers={"Host": "example.com"},
)

assert response.status_code == 302
assert response.location == "https://example.com/"

def test_db_login_invalid_control_characters_next_url(self):
"""
Test Security invalid next URL with control characters
Expand Down