diff --git a/tests/integration_tests/security/api_tests.py b/tests/integration_tests/security/api_tests.py index 24e9acc2b69..7512507cf18 100644 --- a/tests/integration_tests/security/api_tests.py +++ b/tests/integration_tests/security/api_tests.py @@ -30,6 +30,7 @@ from superset.utils import json from tests.conftest import with_config from tests.integration_tests.base_tests import SupersetTestCase from tests.integration_tests.constants import ADMIN_USERNAME, GAMMA_USERNAME +from tests.integration_tests.test_app import app from tests.integration_tests.fixtures.birth_names_dashboard import ( load_birth_names_dashboard_with_slices, # noqa: F401 load_birth_names_data, # noqa: F401 @@ -402,3 +403,42 @@ class TestSecurityRolesApi(SupersetTestCase): assert sorted(role2_api["user_ids"]) == role2_expected["user_ids"] assert sorted(role2_api["permission_ids"]) == role2_expected["permission_ids"] assert role2_api["group_ids"] == role2_expected["group_ids"] + + +class TestLogoutSessionInvalidation(SupersetTestCase): + """Regression for #24713: a session cookie captured pre-logout must not grant + access after the user logs out. The original report describes copying the + session cookie out, calling /logout/, and successfully reusing the cookie in + a second browser to bypass authentication.""" + + def test_session_cookie_invalidated_after_logout(self): + self.login(ADMIN_USERNAME) + + resp_authed = self.client.get("api/v1/dashboard/", follow_redirects=False) + assert resp_authed.status_code == 200, ( + f"Login did not yield an authenticated session " + f"(got {resp_authed.status_code})" + ) + + # Werkzeug 2.3+ exposes the test client's cookies on `_cookies` as a + # mapping keyed by (domain, path, key). Snapshot the session cookie + # value — this is what a malicious actor would copy out of a browser. + captured = None + for cookie in self.client._cookies.values(): + if cookie.key == "session": + captured = cookie.value + break + assert captured, "expected a session cookie after login" + + self.client.get("/logout/", follow_redirects=True) + + # Replay the captured cookie in a fresh client (simulates importing + # the cookie into a second browser). + replay_client = app.test_client() + replay_client.set_cookie("session", captured, domain="localhost") + + resp_replay = replay_client.get("api/v1/dashboard/", follow_redirects=False) + assert resp_replay.status_code != 200, ( + f"Captured session cookie was still accepted after logout " + f"(status={resp_replay.status_code}); see issue #24713" + )