From be6efd03c358fd4ec9508c2917e9b54e98793379 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 8 Mar 2024 14:12:57 +0000 Subject: [PATCH 1/4] Update .gitreview for stable/2024.1 Change-Id: I665c85c7307844a841ba8e9ef2286503c78b9e06 --- .gitreview | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitreview b/.gitreview index 8de12780..57f68119 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.opendev.org port=29418 project=openstack/keystonemiddleware.git +defaultbranch=stable/2024.1 From eb7fc0461a1832c641f5812118d92d12506c1c17 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 8 Mar 2024 14:12:58 +0000 Subject: [PATCH 2/4] Update TOX_CONSTRAINTS_FILE for stable/2024.1 Update the URL to the upper-constraints file to point to the redirect rule on releases.openstack.org so that anyone working on this branch will switch to the correct upper-constraints list automatically when the requirements repository branches. Until the requirements repository has as stable/2024.1 branch, tests will continue to use the upper-constraints list on master. Change-Id: I9d02834c84ea39b755be83ab13efdc1f79373dd6 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 96ad9556..c1d1e41c 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ setenv = OS_STDOUT_NOCAPTURE=False OS_STDERR_NOCAPTURE=False deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2024.1} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = From f58418f9fe654f7d75e030893002e05bcbb22332 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 31 Oct 2025 12:14:57 +0000 Subject: [PATCH 3/4] Update .gitreview for unmaintained/2024.1 Change-Id: Ie361f8bef773d2397e90306fe61cf6c6267bcc64 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/functions --- .gitreview | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitreview b/.gitreview index 57f68119..5f5e9b9d 100644 --- a/.gitreview +++ b/.gitreview @@ -2,4 +2,4 @@ host=review.opendev.org port=29418 project=openstack/keystonemiddleware.git -defaultbranch=stable/2024.1 +defaultbranch=unmaintained/2024.1 From 52acf50ca54b4db7f905f0d45736b6eb25ecf3b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Grasza Date: Thu, 8 Jan 2026 14:46:19 +0100 Subject: [PATCH 4/4] Fix privilege escalation via spoofed identity headers The external_oauth2_token middleware did not sanitize incoming authentication headers before processing OAuth 2.0 tokens. This allowed an attacker to send forged identity headers (e.g., X-Is-Admin-Project, X-Roles, X-User-Id) that would not be cleared by the middleware, potentially enabling privilege escalation. This fix adds a call to remove_auth_headers() at the start of request processing to sanitize all incoming identity headers, matching the secure behavior of the main auth_token middleware. Closes-Bug: #2129018 Change-Id: Idd4fe1d17a25b3064b31f454d9830242f345e018 (cherry picked from commit b473c0ed1467b70c74c8a82cb4d15ccf8424b27b) Signed-off-by: Jeremy Stanley Signed-off-by: Artem Goncharov --- keystonemiddleware/external_oauth2_token.py | 7 +- .../test_external_oauth2_token_middleware.py | 76 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/keystonemiddleware/external_oauth2_token.py b/keystonemiddleware/external_oauth2_token.py index c02cace6..32fd4e49 100644 --- a/keystonemiddleware/external_oauth2_token.py +++ b/keystonemiddleware/external_oauth2_token.py @@ -33,6 +33,7 @@ from keystonemiddleware._common import config from keystonemiddleware.auth_token import _cache +from keystonemiddleware.auth_token import _request from keystonemiddleware.exceptions import ConfigurationError from keystonemiddleware.exceptions import KeystoneMiddlewareException from keystonemiddleware.i18n import _ @@ -534,7 +535,7 @@ def _token_cache_factory(self): **cache_kwargs) return _cache.TokenCache(self._log, **cache_kwargs) - @webob.dec.wsgify() + @webob.dec.wsgify(RequestClass=_request._AuthTokenRequest) def __call__(self, req): """Handle incoming request.""" self.process_request(req) @@ -545,8 +546,10 @@ def process_request(self, request): """Process request. :param request: Incoming request - :type request: _request.AuthTokenRequest + :type request: _request._AuthTokenRequest """ + request.remove_auth_headers() + access_token = None if (request.authorization and request.authorization.authtype == 'Bearer'): diff --git a/keystonemiddleware/tests/unit/test_external_oauth2_token_middleware.py b/keystonemiddleware/tests/unit/test_external_oauth2_token_middleware.py index c67f79b7..fe907b13 100644 --- a/keystonemiddleware/tests/unit/test_external_oauth2_token_middleware.py +++ b/keystonemiddleware/tests/unit/test_external_oauth2_token_middleware.py @@ -1824,6 +1824,82 @@ def mock_resp(request, context): self.assertEqual(resp.headers.get('WWW-Authenticate'), 'Authorization OAuth 2.0 uri="%s"' % self._audience) + def test_spoofed_headers_are_sanitized(self): + """Test that spoofed identity headers are removed and replaced. + + This test verifies the fix for a privilege escalation vulnerability + where an attacker could send spoofed identity headers that would not + be cleared by the middleware, allowing unauthorized access. + """ + conf = copy.deepcopy(self._test_conf) + self.set_middleware(conf=conf) + + # Use non-admin roles in the token metadata + non_admin_roles = 'member,reader' + non_admin_metadata = copy.deepcopy(self._default_metadata) + non_admin_metadata['roles'] = non_admin_roles + + def mock_resp(request, context): + return self._introspect_response( + request, context, + auth_method=self._auth_method, + introspect_client_id=self._test_client_id, + introspect_client_secret=self._test_client_secret, + access_token=self._token, + active=True, + metadata=non_admin_metadata + ) + + self.requests_mock.post(self._introspect_endpoint, + json=mock_resp) + self.requests_mock.get(self._auth_url, + json=VERSION_LIST_v3, + status_code=300) + + # Attempt to spoof multiple identity headers + spoofed_headers = get_authorization_header(self._token) + spoofed_headers.update({ + 'X-Identity-Status': 'Confirmed', + 'X-Is-Admin-Project': 'true', + 'X-User-Id': 'spoofed_admin_user_id', + 'X-User-Name': 'spoofed_admin', + 'X-Roles': 'admin,superuser', + 'X-Project-Id': 'spoofed_project_id', + 'X-User-Domain-Id': 'spoofed_domain_id', + 'X-User-Domain-Name': 'spoofed_domain', + }) + + resp = self.call_middleware( + headers=spoofed_headers, + expected_status=200, + method='GET', path='/vnfpkgm/v1/vnf_packages', + environ={'wsgi.input': FakeWsgiInput(FakeSocket(None))} + ) + self.assertEqual(FakeApp.SUCCESS, resp.body) + + # Verify spoofed headers were replaced with actual token values + env = resp.request.environ + + # X-Is-Admin-Project should not be present (not the spoofed 'true') + # because the token has non-admin roles and the middleware only sets + # this header when is_admin is true + self.assertNotIn('HTTP_X_IS_ADMIN_PROJECT', env) + + # User info should match the token, not the spoofed values + self.assertEqual(self._user_id, env['HTTP_X_USER_ID']) + self.assertEqual(self._user_name, env['HTTP_X_USER_NAME']) + self.assertEqual(self._user_domain_id, env['HTTP_X_USER_DOMAIN_ID']) + self.assertEqual( + self._user_domain_name, + env['HTTP_X_USER_DOMAIN_NAME'] + ) + + # Roles should be from the token, not spoofed + self.assertEqual(non_admin_roles, env['HTTP_X_ROLES']) + + # Project info should match the token + self.assertEqual(self._project_id, env['HTTP_X_PROJECT_ID']) + class ExternalAuth2ProtocolTest(BaseExternalOauth2TokenMiddlewareTest):