From ac942c01718606624c0fab68e729b0fae9c6609e Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Jan 10 2024 12:33:02 +0000 Subject: import ipa-4.6.8-5.el7_9.16 --- diff --git a/README.debrand b/README.debrand deleted file mode 100644 index 01c46d2..0000000 --- a/README.debrand +++ /dev/null @@ -1,2 +0,0 @@ -Warning: This package was configured for automatic debranding, but the changes -failed to apply. diff --git a/SOURCES/0042-Check-the-HTTP-Referer-header-on-all-requests.patch b/SOURCES/0042-Check-the-HTTP-Referer-header-on-all-requests.patch new file mode 100644 index 0000000..82fd853 --- /dev/null +++ b/SOURCES/0042-Check-the-HTTP-Referer-header-on-all-requests.patch @@ -0,0 +1,121 @@ +From 2b9692621b0ee27def37c597621ea6a20c757ca8 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 21 Nov 2023 09:37:25 +0100 +Subject: [PATCH] Check the HTTP Referer header on all requests + +The referer was only checked in WSGIExecutioner classes: + + - jsonserver + - KerberosWSGIExecutioner + - xmlserver + - jsonserver_kerb + +This left /i18n_messages, /session/login_kerberos, +/session/login_x509, /session/login_password, +/session/change_password and /session/sync_token unprotected +against CSRF attacks. + +CVE-2023-5455 + +Signed-off-by: Rob Crittenden +--- + ipaserver/rpcserver.py | 34 +++++++++++++++++++++++++++++++--- + 1 file changed, 31 insertions(+), 3 deletions(-) + +diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py +index 3c831a55888ba723fc52c988593c364fb7883b7b..91bec2133479a9b4bd1311e6be800d982cf855f3 100644 +--- a/ipaserver/rpcserver.py ++++ b/ipaserver/rpcserver.py +@@ -136,6 +136,19 @@ _success_template = """ + """ + + class HTTP_Status(plugable.Plugin): ++ def check_referer(self, environ): ++ if "HTTP_REFERER" not in environ: ++ logger.error("Rejecting request with missing Referer") ++ return False ++ if (not environ["HTTP_REFERER"].startswith( ++ "https://%s/ipa" % self.api.env.host) ++ and not self.env.in_tree): ++ logger.error("Rejecting request with bad Referer %s", ++ environ["HTTP_REFERER"]) ++ return False ++ logger.debug("Valid Referer %s", environ["HTTP_REFERER"]) ++ return True ++ + def not_found(self, environ, start_response, url, message): + """ + Return a 404 Not Found error. +@@ -297,9 +310,6 @@ class wsgi_dispatch(Executioner, HTTP_Status): + self.__apps[key] = app + + +- +- +- + class WSGIExecutioner(Executioner): + """ + Base class for execution backends with a WSGI application interface. +@@ -803,6 +813,9 @@ class jsonserver_session(jsonserver, KerberosSession): + + logger.debug('WSGI jsonserver_session.__call__:') + ++ if not self.check_referer(environ): ++ return self.bad_request(environ, start_response, 'denied') ++ + # Redirect to login if no Kerberos credentials + ccache_name = self.get_environ_creds(environ) + if ccache_name is None: +@@ -843,6 +856,9 @@ class KerberosLogin(Backend, KerberosSession): + def __call__(self, environ, start_response): + logger.debug('WSGI KerberosLogin.__call__:') + ++ if not self.check_referer(environ): ++ return self.bad_request(environ, start_response, 'denied') ++ + # Redirect to login if no Kerberos credentials + user_ccache_name = self.get_environ_creds(environ) + if user_ccache_name is None: +@@ -861,6 +877,9 @@ class login_x509(KerberosLogin): + def __call__(self, environ, start_response): + logger.debug('WSGI login_x509.__call__:') + ++ if not self.check_referer(environ): ++ return self.bad_request(environ, start_response, 'denied') ++ + if 'KRB5CCNAME' not in environ: + return self.unauthorized( + environ, start_response, 'KRB5CCNAME not set', +@@ -881,6 +900,9 @@ class login_password(Backend, KerberosSession): + def __call__(self, environ, start_response): + logger.debug('WSGI login_password.__call__:') + ++ if not self.check_referer(environ): ++ return self.bad_request(environ, start_response, 'denied') ++ + # Get the user and password parameters from the request + content_type = environ.get('CONTENT_TYPE', '').lower() + if not content_type.startswith('application/x-www-form-urlencoded'): +@@ -1018,6 +1040,9 @@ class change_password(Backend, HTTP_Status): + def __call__(self, environ, start_response): + logger.info('WSGI change_password.__call__:') + ++ if not self.check_referer(environ): ++ return self.bad_request(environ, start_response, 'denied') ++ + # Get the user and password parameters from the request + content_type = environ.get('CONTENT_TYPE', '').lower() + if not content_type.startswith('application/x-www-form-urlencoded'): +@@ -1231,6 +1256,9 @@ class xmlserver_session(xmlserver, KerberosSession): + + logger.debug('WSGI xmlserver_session.__call__:') + ++ if not self.check_referer(environ): ++ return self.bad_request(environ, start_response, 'denied') ++ + ccache_name = environ.get('KRB5CCNAME') + + # Redirect to /ipa/xml if no Kerberos credentials +-- +2.26.3 + diff --git a/SOURCES/0043-Integration-tests-for-verifying-Referer-header-in-th.patch b/SOURCES/0043-Integration-tests-for-verifying-Referer-header-in-th.patch new file mode 100644 index 0000000..47b84ff --- /dev/null +++ b/SOURCES/0043-Integration-tests-for-verifying-Referer-header-in-th.patch @@ -0,0 +1,351 @@ +From 7bd6835819d7d6b158f307939da1e618f860c42a Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 21 Nov 2023 09:49:14 +0100 +Subject: [PATCH] Integration tests for verifying Referer header in the UI + +Validate that the change_password and login_password endpoints +verify the HTTP Referer header. There is some overlap in the +tests: belt and suspenders. + +All endpoints except session/login_x509 are covered, sometimes +having to rely on expected bad results (see the i18n endpoint). + +session/login_x509 is not tested yet as it requires significant +additional setup in order to associate a user certificate with +a user entry, etc. + +This can be manually verified by modifying /etc/httpd/conf.d/ipa.conf +and adding: + +Satisfy Any +Require all granted + +Then comment out Auth and SSLVerify, etc. and restart httpd. + +With a valid Referer will fail with a 401 and log that there is no +KRB5CCNAME. This comes after the referer check. + +With an invalid Referer it will fail with a 400 Bad Request as +expected. + +CVE-2023-5455 + +Signed-off-by: Rob Crittenden +--- + ipatests/test_ipaserver/httptest.py | 7 +- + ipatests/test_ipaserver/test_changepw.py | 12 +- + .../test_ipaserver/test_login_password.py | 88 ++++++++++++ + ipatests/test_ipaserver/test_referer.py | 136 ++++++++++++++++++ + ipatests/util.py | 4 +- + 5 files changed, 242 insertions(+), 5 deletions(-) + create mode 100644 ipatests/test_ipaserver/test_login_password.py + create mode 100644 ipatests/test_ipaserver/test_referer.py + +diff --git a/ipatests/test_ipaserver/httptest.py b/ipatests/test_ipaserver/httptest.py +index 13003bc85c2e1fee5cb78e1c9fc50d39e6e88eb1..01c2abae890af6cc3e36fbfec25f6b4665865c87 100644 +--- a/ipatests/test_ipaserver/httptest.py ++++ b/ipatests/test_ipaserver/httptest.py +@@ -34,7 +34,7 @@ class Unauthorized_HTTP_test(object): + cacert = api.env.tls_ca_cert + content_type = 'application/x-www-form-urlencoded' + +- def send_request(self, method='POST', params=None): ++ def send_request(self, method='POST', params=None, host=None): + """ + Send a request to HTTP server + +@@ -44,7 +44,10 @@ class Unauthorized_HTTP_test(object): + # urlencode *can* take two arguments + # pylint: disable=too-many-function-args + params = urllib.parse.urlencode(params, True) +- url = 'https://' + self.host + self.app_uri ++ if host: ++ url = 'https://' + host + self.app_uri ++ else: ++ url = 'https://' + self.host + self.app_uri + + headers = {'Content-Type' : self.content_type, + 'Referer' : url} +diff --git a/ipatests/test_ipaserver/test_changepw.py b/ipatests/test_ipaserver/test_changepw.py +index 3f8331266889a209b62f97a0eed40c51f7aa3475..64a2aa533a31b0d655a0028a2e008e8559f9b4cd 100644 +--- a/ipatests/test_ipaserver/test_changepw.py ++++ b/ipatests/test_ipaserver/test_changepw.py +@@ -52,10 +52,11 @@ class test_changepw(XMLRPC_test, Unauthorized_HTTP_test): + except errors.NotFound: + pass + +- def _changepw(self, user, old_password, new_password): ++ def _changepw(self, user, old_password, new_password, host=None): + return self.send_request(params={'user': str(user), + 'old_password' : str(old_password), + 'new_password' : str(new_password)}, ++ host=host + ) + + def _checkpw(self, user, password): +@@ -88,6 +89,15 @@ class test_changepw(XMLRPC_test, Unauthorized_HTTP_test): + # make sure that password is NOT changed + self._checkpw(testuser, old_password) + ++ def test_invalid_referer(self): ++ response = self._changepw(testuser, old_password, new_password, ++ 'attacker.test') ++ ++ assert_equal(response.status, 400) ++ ++ # make sure that password is NOT changed ++ self._checkpw(testuser, old_password) ++ + def test_pwpolicy_error(self): + response = self._changepw(testuser, old_password, '1') + +diff --git a/ipatests/test_ipaserver/test_login_password.py b/ipatests/test_ipaserver/test_login_password.py +new file mode 100644 +index 0000000000000000000000000000000000000000..9425cb7977fbc87210bf91464e0257830a938baf +--- /dev/null ++++ b/ipatests/test_ipaserver/test_login_password.py +@@ -0,0 +1,88 @@ ++# Copyright (C) 2023 Red Hat ++# see file 'COPYING' for use and warranty information ++# ++# This program is free software; you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation, either version 3 of the License, or ++# (at your option) any later version. ++# ++# This program is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with this program. If not, see . ++ ++import os ++import pytest ++import uuid ++ ++from ipatests.test_ipaserver.httptest import Unauthorized_HTTP_test ++from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test ++from ipatests.util import assert_equal ++from ipalib import api, errors ++from ipapython.ipautil import run ++ ++testuser = u'tuser' ++password = u'password' ++ ++ ++@pytest.mark.tier1 ++class test_login_password(XMLRPC_test, Unauthorized_HTTP_test): ++ app_uri = '/ipa/session/login_password' ++ ++ @pytest.fixture(autouse=True) ++ def login_setup(self, request): ++ ccache = os.path.join('/tmp', str(uuid.uuid4())) ++ try: ++ api.Command['user_add'](uid=testuser, givenname=u'Test', sn=u'User') ++ api.Command['passwd'](testuser, password=password) ++ run(['kinit', testuser], stdin='{0}\n{0}\n{0}\n'.format(password), ++ env={"KRB5CCNAME": ccache}) ++ except errors.ExecutionError as e: ++ pytest.skip( ++ 'Cannot set up test user: %s' % e ++ ) ++ ++ def fin(): ++ try: ++ api.Command['user_del']([testuser]) ++ except errors.NotFound: ++ pass ++ os.unlink(ccache) ++ ++ request.addfinalizer(fin) ++ ++ def _login(self, user, password, host=None): ++ return self.send_request(params={'user': str(user), ++ 'password' : str(password)}, ++ host=host) ++ ++ def test_bad_options(self): ++ for params in ( ++ None, # no params ++ {"user": "foo"}, # missing options ++ {"user": "foo", "password": ""}, # empty option ++ ): ++ response = self.send_request(params=params) ++ assert_equal(response.status, 400) ++ assert_equal(response.reason, 'Bad Request') ++ ++ def test_invalid_auth(self): ++ response = self._login(testuser, 'wrongpassword') ++ ++ assert_equal(response.status, 401) ++ assert_equal(response.getheader('X-IPA-Rejection-Reason'), ++ 'invalid-password') ++ ++ def test_invalid_referer(self): ++ response = self._login(testuser, password, 'attacker.test') ++ ++ assert_equal(response.status, 400) ++ ++ def test_success(self): ++ response = self._login(testuser, password) ++ ++ assert_equal(response.status, 200) ++ assert response.getheader('X-IPA-Rejection-Reason') is None +diff --git a/ipatests/test_ipaserver/test_referer.py b/ipatests/test_ipaserver/test_referer.py +new file mode 100644 +index 0000000000000000000000000000000000000000..4eade8bbaf304c48bf71c16892858d899b43cf88 +--- /dev/null ++++ b/ipatests/test_ipaserver/test_referer.py +@@ -0,0 +1,128 @@ ++# Copyright (C) 2023 Red Hat ++# see file 'COPYING' for use and warranty information ++# ++# This program is free software; you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation, either version 3 of the License, or ++# (at your option) any later version. ++# ++# This program is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty of ++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++# GNU General Public License for more details. ++# ++# You should have received a copy of the GNU General Public License ++# along with this program. If not, see . ++ ++import os ++import pytest ++import uuid ++ ++from ipatests.test_ipaserver.httptest import Unauthorized_HTTP_test ++from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test ++from ipatests.util import assert_equal ++from ipalib import api, errors ++from ipapython.ipautil import run ++ ++testuser = u'tuser' ++password = u'password' ++ ++ ++@pytest.mark.tier1 ++class test_referer(XMLRPC_test, Unauthorized_HTTP_test): ++ ++ @pytest.fixture(autouse=True) ++ def login_setup(self, request): ++ ccache = os.path.join('/tmp', str(uuid.uuid4())) ++ tokenid = None ++ try: ++ api.Command['user_add'](uid=testuser, givenname=u'Test', sn=u'User') ++ api.Command['passwd'](testuser, password=password) ++ run(['kinit', testuser], stdin='{0}\n{0}\n{0}\n'.format(password), ++ env={"KRB5CCNAME": ccache}) ++ result = api.Command["otptoken_add"]( ++ type=u'HOTP', description=u'testotp', ++ ipatokenotpalgorithm=u'sha512', ipatokenowner=testuser, ++ ipatokenotpdigits=u'6') ++ tokenid = result['result']['ipatokenuniqueid'][0] ++ except errors.ExecutionError as e: ++ pytest.skip( ++ 'Cannot set up test user: %s' % e ++ ) ++ ++ def fin(): ++ try: ++ api.Command['user_del']([testuser]) ++ api.Command['otptoken_del']([tokenid]) ++ except errors.NotFound: ++ pass ++ os.unlink(ccache) ++ ++ request.addfinalizer(fin) ++ ++ def _request(self, params={}, host=None): ++ # implicit is that self.app_uri is set to the appropriate value ++ return self.send_request(params=params, host=host) ++ ++ def test_login_password_valid(self): ++ """Valid authentication of a user""" ++ self.app_uri = "/ipa/session/login_password" ++ response = self._request( ++ params={'user': 'tuser', 'password': password}) ++ assert_equal(response.status, 200, self.app_uri) ++ ++ def test_change_password_valid(self): ++ """This actually changes the user password""" ++ self.app_uri = "/ipa/session/change_password" ++ response = self._request( ++ params={'user': 'tuser', ++ 'old_password': password, ++ 'new_password': 'new_password'} ++ ) ++ assert_equal(response.status, 200, self.app_uri) ++ ++ def test_sync_token_valid(self): ++ """We aren't testing that sync works, just that we can get there""" ++ self.app_uri = "/ipa/session/sync_token" ++ response = self._request( ++ params={'user': 'tuser', ++ 'first_code': '1234', ++ 'second_code': '5678', ++ 'password': 'password'}) ++ assert_equal(response.status, 200, self.app_uri) ++ ++ # /ipa/session/login_x509 is not tested yet as it requires ++ # significant additional setup. ++ # This can be manually verified by adding ++ # Satisfy Any and Require all granted to the configuration ++ # section and comment out all Auth directives. The request ++ # will fail and log that there is no KRB5CCNAME which comes ++ # after the referer check. ++ ++ def test_endpoints_auth_required(self): ++ """Test endpoints that require pre-authorization which will ++ fail before we even get to the Referer check ++ """ ++ self.endpoints = { ++ "/ipa/xml", ++ "/ipa/session/login_kerberos", ++ "/ipa/session/json", ++ "/ipa/session/xml" ++ } ++ for self.app_uri in self.endpoints: ++ response = self._request(host="attacker.test") ++ ++ # referer is checked after auth ++ assert_equal(response.status, 401, self.app_uri) ++ ++ def notest_endpoints_invalid(self): ++ """Pass in a bad Referer, expect a 400 Bad Request""" ++ self.endpoints = { ++ "/ipa/session/login_password", ++ "/ipa/session/change_password", ++ "/ipa/session/sync_token", ++ } ++ for self.app_uri in self.endpoints: ++ response = self._request(host="attacker.test") ++ ++ assert_equal(response.status, 400, self.app_uri) +diff --git a/ipatests/util.py b/ipatests/util.py +index 6d1e78260089e65d5d2dd44a5a312021c2fdd781..bde93b6c5c96c2f996462683036dbbb549f914f2 100644 +--- a/ipatests/util.py ++++ b/ipatests/util.py +@@ -169,12 +169,12 @@ class ExceptionNotRaised(Exception): + return self.msg % self.expected.__name__ + + +-def assert_equal(val1, val2): ++def assert_equal(val1, val2, msg=''): + """ + Assert ``val1`` and ``val2`` are the same type and of equal value. + """ + assert type(val1) is type(val2), '%r != %r' % (val1, val2) +- assert val1 == val2, '%r != %r' % (val1, val2) ++ assert val1 == val2, '%r != %r %r' % (val1, val2, msg) + + + def assert_not_equal(val1, val2): +-- +2.26.3 + diff --git a/SPECS/ipa.spec b/SPECS/ipa.spec index 6d0d521..dcedc8a 100644 --- a/SPECS/ipa.spec +++ b/SPECS/ipa.spec @@ -25,6 +25,7 @@ %if 0%{?rhel} %global with_python3 0 +%define __python %{__python2} %else %global with_python3 1 %endif @@ -103,7 +104,7 @@ Name: ipa Version: %{IPA_VERSION} -Release: 5%{?dist}.15 +Release: 5%{?dist}.16 Summary: The Identity, Policy and Audit system Group: System Environment/Base @@ -111,9 +112,9 @@ License: GPLv3+ URL: http://www.freeipa.org/ Source0: https://releases.pagure.org/freeipa/freeipa-%{version}.tar.gz # RHEL spec file only: START: Change branding to IPA and Identity Management -#Source1: header-logo.png -#Source2: login-screen-background.jpg -#Source4: product-name.png +Source1: header-logo.png +Source2: login-screen-background.jpg +Source4: product-name.png # RHEL spec file only: END: Change branding to IPA and Identity Management BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) @@ -159,6 +160,8 @@ Patch0038: 0038-Move-client-certificate-request-after-krb5.conf-is-c.patch Patch0039: 0039-server-install-remove-error-log-about-missing-bkup-f.patch Patch0040: 0040-ipaserver-deepcopy-objectclasses-list-from-IPA-confi.patch Patch0041: 0041-Fix-memory-leak-in-the-OTP-last-token-plugin.patch +Patch0042: 0042-Check-the-HTTP-Referer-header-on-all-requests.patch +Patch0043: 0043-Integration-tests-for-verifying-Referer-header-in-th.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch Patch1002: 1002-Package-copy-schema-to-ca.py.patch Patch1003: 1003-Revert-Increased-mod_wsgi-socket-timeout.patch @@ -415,10 +418,7 @@ Requires: oddjob Requires: gssproxy >= 0.7.0-2 # 1.15.2: FindByNameAndCertificate (https://pagure.io/SSSD/sssd/issue/3050) Requires: sssd-dbus >= 1.15.2 - -%if 0%{?centos} == 0 Requires: system-logos >= 70.7.0 -%endif Provides: %{alt_name}-server = %{version} Conflicts: %{alt_name}-server @@ -975,9 +975,9 @@ cp -r %{_builddir}/freeipa-%{version} %{_builddir}/freeipa-%{version}-python3 # with_python3 # RHEL spec file only: START: Change branding to IPA and Identity Management -#cp %SOURCE1 install/ui/images/header-logo.png -#cp %SOURCE2 install/ui/images/login-screen-background.jpg -#cp %SOURCE4 install/ui/images/product-name.png +cp %SOURCE1 install/ui/images/header-logo.png +cp %SOURCE2 install/ui/images/login-screen-background.jpg +cp %SOURCE4 install/ui/images/product-name.png # RHEL spec file only: END: Change branding to IPA and Identity Management @@ -1001,8 +1001,7 @@ find \ %configure --with-vendor-suffix=-%{release} \ %{enable_server_option} \ %{with_ipatests_option} \ - %{linter_options} \ - --with-ipaplatform=rhel + %{linter_options} %make_build @@ -1023,8 +1022,7 @@ find \ %configure --with-vendor-suffix=-%{release} \ %{enable_server_option} \ %{with_ipatests_option} \ - %{linter_options} \ - --with-ipaplatform=rhel + %{linter_options} popd %endif # with_python3 @@ -1111,11 +1109,9 @@ ln -s %{_bindir}/ipa-test-task-%{python2_version} %{buildroot}%{_bindir}/ipa-tes # remove files which are useful only for make uninstall find %{buildroot} -wholename '*/site-packages/*/install_files.txt' -exec rm {} \; -%if 0%{?centos} == 0 # RHEL spec file only: START: Replace login-screen-logo.png with a symlink ln -sf %{_datadir}/pixmaps/fedora-gdm-logo.png %{buildroot}%{_usr}/share/ipa/ui/images/login-screen-logo.png # RHEL spec file only: END: Replace login-screen-logo.png with a symlink -%endif %find_lang %{gettext_domain} @@ -1772,6 +1768,9 @@ fi %changelog +* Tue Nov 21 2023 Florence Blanc-Renaud - 4.6.8-5.el7_9.16 +- Resolves: RHEL-12570 ipa: Invalid CSRF protection + * Thu Aug 03 2023 Florence Blanc-Renaud - 4.6.8-5.el7_9.15 - Resolves: 2209636 libipa_otp_lasttoken plugin memory leak