diff --git a/SOURCES/0015-CVE-2023-5455.patch b/SOURCES/0015-CVE-2023-5455.patch new file mode 100644 index 0000000..e877c9d --- /dev/null +++ b/SOURCES/0015-CVE-2023-5455.patch @@ -0,0 +1,118 @@ +From 13778d88ca2ac73b729821bdea844172a18c0cb9 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Fri, 6 Oct 2023 20:16:29 +0000 +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 b7116469d73..198fc9e7dba 100644 +--- a/ipaserver/rpcserver.py ++++ b/ipaserver/rpcserver.py +@@ -156,6 +156,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. +@@ -331,9 +344,6 @@ class wsgi_dispatch(Executioner, HTTP_Status): + self.__apps[key] = app + + +- +- +- + class WSGIExecutioner(Executioner): + """ + Base class for execution backends with a WSGI application interface. +@@ -897,6 +907,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: +@@ -949,6 +962,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: +@@ -967,6 +983,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', +@@ -1015,6 +1034,9 @@ class login_password(Backend, KerberosSession): + + 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'): +@@ -1147,6 +1169,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'): +@@ -1364,6 +1389,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 diff --git a/SOURCES/0016-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch b/SOURCES/0016-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch new file mode 100644 index 0000000..62b1707 --- /dev/null +++ b/SOURCES/0016-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch @@ -0,0 +1,262 @@ +From a847e2483b4c4832ee5129901da169f4eb0d1392 Mon Sep 17 00:00:00 2001 +From: Julien Rische +Date: Mon, 9 Oct 2023 15:47:03 +0200 +Subject: [PATCH] ipa-kdb: Detect and block Bronze-Bit attacks + +The C8S/RHEL8 version of FreeIPA is vulnerable to the Bronze-Bit attack +because it does not implement PAC ticket signature to protect the +"forwardable" flag. However, it does implement the PAC extended KDC +signature, which protects against PAC spoofing. + +Based on information available in the PAC and the +"ok-to-auth-as-delegate" attribute in the database. It is possible to +detect and reject requests where the "forwardable" flag was flipped by +the attacker in the evidence ticket. +--- + daemons/ipa-kdb/ipa_kdb.h | 13 +++ + daemons/ipa-kdb/ipa_kdb_kdcpolicy.c | 6 + + daemons/ipa-kdb/ipa_kdb_mspac.c | 173 ++++++++++++++++++++++++++++ + ipaserver/install/server/install.py | 8 ++ + 4 files changed, 200 insertions(+) + +diff --git a/daemons/ipa-kdb/ipa_kdb.h b/daemons/ipa-kdb/ipa_kdb.h +index 7aa5be4948e..02b2cb6313e 100644 +--- a/daemons/ipa-kdb/ipa_kdb.h ++++ b/daemons/ipa-kdb/ipa_kdb.h +@@ -367,6 +367,19 @@ krb5_error_code ipadb_is_princ_from_trusted_realm(krb5_context kcontext, + const char *test_realm, size_t size, + char **trusted_realm); + ++/* Try to detect a Bronze-Bit attack based on the content of the request and ++ * data from the KDB. ++ * ++ * context krb5 context ++ * request KDB request ++ * detected Set to "true" if a bronze bit attack is detected and the ++ * pointer is not NULL. Remains unset otherwise. ++ * status If the call fails and the pointer is not NULL, set it with a ++ * message describing the cause of the failure. */ ++krb5_error_code ++ipadb_check_for_bronze_bit_attack(krb5_context context, krb5_kdc_req *request, ++ bool *detected, const char **status); ++ + /* DELEGATION CHECKS */ + + krb5_error_code ipadb_check_allowed_to_delegate(krb5_context kcontext, +diff --git a/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c b/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c +index f2804c9b23a..1032dff0b5c 100644 +--- a/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c ++++ b/daemons/ipa-kdb/ipa_kdb_kdcpolicy.c +@@ -185,6 +185,12 @@ ipa_kdcpolicy_check_tgs(krb5_context context, krb5_kdcpolicy_moddata moddata, + const char **status, krb5_deltat *lifetime_out, + krb5_deltat *renew_lifetime_out) + { ++ krb5_error_code kerr; ++ ++ kerr = ipadb_check_for_bronze_bit_attack(context, request, NULL, status); ++ if (kerr) ++ return KRB5KDC_ERR_POLICY; ++ + *status = NULL; + *lifetime_out = 0; + *renew_lifetime_out = 0; +diff --git a/daemons/ipa-kdb/ipa_kdb_mspac.c b/daemons/ipa-kdb/ipa_kdb_mspac.c +index 83cb9914d23..b4e22d4316a 100644 +--- a/daemons/ipa-kdb/ipa_kdb_mspac.c ++++ b/daemons/ipa-kdb/ipa_kdb_mspac.c +@@ -3298,3 +3298,176 @@ krb5_error_code ipadb_is_princ_from_trusted_realm(krb5_context kcontext, + + return KRB5_KDB_NOENTRY; + } ++ ++krb5_error_code ++ipadb_check_for_bronze_bit_attack(krb5_context context, krb5_kdc_req *request, ++ bool *detected, const char **status) ++{ ++ krb5_error_code kerr; ++ const char *st = NULL; ++ size_t i, j; ++ krb5_ticket *evidence_tkt; ++ krb5_authdata **authdata, **ifrel = NULL; ++ krb5_pac pac = NULL; ++ TALLOC_CTX *tmpctx = NULL; ++ krb5_data fullsign = { 0, 0, NULL }, linfo_blob = { 0, 0, NULL }; ++ DATA_BLOB linfo_data; ++ struct PAC_LOGON_INFO_CTR linfo; ++ enum ndr_err_code ndr_err; ++ struct dom_sid asserted_identity_sid; ++ bool evtkt_is_s4u2self = false; ++ krb5_db_entry *proxy_entry = NULL; ++ ++ /* If no additional ticket, this is not a constrained delegateion request. ++ * Skip checks. */ ++ if (!(request->kdc_options & KDC_OPT_CNAME_IN_ADDL_TKT)) { ++ kerr = 0; ++ goto end; ++ } ++ ++ evidence_tkt = request->second_ticket[0]; ++ ++ /* No need to check the Forwardable flag. If it was not set, this request ++ * would have failed earlier. */ ++ ++ /* We only support general constrained delegation (not RBCD), which is not ++ * available for cross-realms. */ ++ if (!krb5_realm_compare(context, evidence_tkt->server, request->server)) { ++ st = "S4U2PROXY_NOT_SUPPORTED_FOR_CROSS_REALMS"; ++ kerr = ENOTSUP; ++ goto end; ++ } ++ ++ authdata = evidence_tkt->enc_part2->authorization_data; ++ ++ /* Search for the PAC. */ ++ for (i = 0; authdata != NULL && authdata[i] != NULL; i++) { ++ if (authdata[i]->ad_type != KRB5_AUTHDATA_IF_RELEVANT) ++ continue; ++ ++ kerr = krb5_decode_authdata_container(context, ++ KRB5_AUTHDATA_IF_RELEVANT, ++ authdata[i], &ifrel); ++ if (kerr) { ++ st = "S4U2PROXY_CANNOT_DECODE_EVIDENCE_TKT_AUTHDATA"; ++ goto end; ++ } ++ ++ for (j = 0; ifrel[j] != NULL; j++) { ++ if (ifrel[j]->ad_type == KRB5_AUTHDATA_WIN2K_PAC) ++ break; ++ } ++ if (ifrel[j] != NULL) ++ break; ++ ++ krb5_free_authdata(context, ifrel); ++ ifrel = NULL; ++ } ++ ++ if (ifrel == NULL) { ++ st = "S4U2PROXY_EVIDENCE_TKT_WITHOUT_PAC"; ++ kerr = ENOENT; ++ goto end; ++ } ++ ++ /* Parse the PAC. */ ++ kerr = krb5_pac_parse(context, ifrel[j]->contents, ifrel[j]->length, &pac); ++ if (kerr) { ++ st = "S4U2PROXY_CANNOT_DECODE_EVICENCE_TKT_PAC"; ++ goto end; ++ } ++ ++ /* Check that the PAC extanded KDC signature is present. If it is, it was ++ * already tested. ++ * If absent, the context of the PAC cannot be trusted. */ ++ kerr = krb5_pac_get_buffer(context, pac, KRB5_PAC_FULL_CHECKSUM, &fullsign); ++ if (kerr) { ++ st = "S4U2PROXY_MISSING_EXTENDED_KDC_SIGN_IN_EVIDENCE_TKT_PAC"; ++ goto end; ++ } ++ ++ /* Get the PAC Logon Info. */ ++ kerr = krb5_pac_get_buffer(context, pac, KRB5_PAC_LOGON_INFO, &linfo_blob); ++ if (kerr) { ++ st = "S4U2PROXY_NO_PAC_LOGON_INFO_IN_EVIDENCE_TKT"; ++ goto end; ++ } ++ ++ /* Parse the PAC Logon Info. */ ++ tmpctx = talloc_new(NULL); ++ if (!tmpctx) { ++ st = "OUT_OF_MEMORY"; ++ kerr = ENOMEM; ++ goto end; ++ } ++ ++ linfo_data.length = linfo_blob.length; ++ linfo_data.data = (uint8_t *)linfo_blob.data; ++ ndr_err = ndr_pull_union_blob(&linfo_data, tmpctx, &linfo, ++ PAC_TYPE_LOGON_INFO, ++ (ndr_pull_flags_fn_t)ndr_pull_PAC_INFO); ++ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { ++ st = "S4U2PROXY_CANNOT_PARSE_ENVIDENCE_TKT_PAC_LOGON_INFO"; ++ kerr = EINVAL; ++ goto end; ++ } ++ ++ /* Check that the extra SIDs array is not empty. */ ++ if (linfo.info->info3.sidcount == 0) { ++ st = "S4U2PROXY_NO_EXTRA_SID"; ++ kerr = ENOENT; ++ goto end; ++ } ++ ++ /* Search for the S-1-18-2 domain SID, which indicates the ticket was ++ * obtained using S4U2Self */ ++ kerr = ipadb_string_to_sid("S-1-18-2", &asserted_identity_sid); ++ if (kerr) { ++ st = "S4U2PROXY_CANNOT_CREATE_ASSERTED_IDENTITY_SID"; ++ goto end; ++ } ++ ++ for (i = 0; i < linfo.info->info3.sidcount; i++) { ++ if (dom_sid_check(&asserted_identity_sid, ++ linfo.info->info3.sids[0].sid, true)) { ++ evtkt_is_s4u2self = true; ++ break; ++ } ++ } ++ ++ /* If the ticket was obtained using S4U2Self, the proxy principal entry must ++ * have the "ok_to_auth_as_delegate" attribute set to true. */ ++ if (evtkt_is_s4u2self) { ++ kerr = ipadb_get_principal(context, evidence_tkt->server, 0, ++ &proxy_entry); ++ if (kerr) { ++ st = "S4U2PROXY_CANNOT_FIND_PROXY_PRINCIPAL"; ++ goto end; ++ } ++ ++ if (!(proxy_entry->attributes & KRB5_KDB_OK_TO_AUTH_AS_DELEGATE)) { ++ /* This evidence ticket cannot be forwardable given the privileges ++ * of the proxy principal. ++ * This is a Bronze Bit attack. */ ++ if (detected) ++ *detected = true; ++ st = "S4U2PROXY_BRONZE_BIT_ATTACK_DETECTED"; ++ kerr = EBADE; ++ goto end; ++ } ++ } ++ ++ kerr = 0; ++ ++end: ++ if (st && status) ++ *status = st; ++ ++ krb5_free_authdata(context, ifrel); ++ krb5_pac_free(context, pac); ++ krb5_free_data_contents(context, &linfo_blob); ++ krb5_free_data_contents(context, &fullsign); ++ talloc_free(tmpctx); ++ ipadb_free_principal(context, proxy_entry); ++ return kerr; ++} +diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py +index 4e4076410f1..bfbb83bcbfa 100644 +--- a/ipaserver/install/server/install.py ++++ b/ipaserver/install/server/install.py +@@ -978,6 +978,14 @@ def install(installer): + # Set the admin user kerberos password + ds.change_admin_password(admin_password) + ++ # Force KDC to refresh the cached value of ipaKrbAuthzData by restarting. ++ # ipaKrbAuthzData has to be set with "MS-PAC" to trigger PAC generation, ++ # which is required to handle S4U2Proxy with the Bronze-Bit fix. ++ # Not doing so would cause API malfunction for around a minute, which is ++ # long enough to cause the hereafter client installation to fail. ++ service.print_msg("Restarting the KDC") ++ krb.restart() ++ + # Call client install script + service.print_msg("Configuring client side components") + try: diff --git a/SOURCES/0017-Integration-tests-for-verifying-Referer.patch b/SOURCES/0017-Integration-tests-for-verifying-Referer.patch new file mode 100644 index 0000000..0a0ddc0 --- /dev/null +++ b/SOURCES/0017-Integration-tests-for-verifying-Referer.patch @@ -0,0 +1,356 @@ +From 86b073a7f03ba0edf4dd91f85b96c89107e9e673 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Thu, 12 Oct 2023 20:34:01 +0000 +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 6cd034a7196..8924798fc93 100644 +--- a/ipatests/test_ipaserver/httptest.py ++++ b/ipatests/test_ipaserver/httptest.py +@@ -36,7 +36,7 @@ class Unauthorized_HTTP_test: + content_type = 'application/x-www-form-urlencoded' + accept_language = 'en-us' + +- def send_request(self, method='POST', params=None): ++ def send_request(self, method='POST', params=None, host=None): + """ + Send a request to HTTP server + +@@ -45,7 +45,10 @@ class Unauthorized_HTTP_test: + if params is not None: + if self.content_type == 'application/x-www-form-urlencoded': + 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, + 'Accept-Language': self.accept_language, +diff --git a/ipatests/test_ipaserver/test_changepw.py b/ipatests/test_ipaserver/test_changepw.py +index c3a47ab265f..df38ddb3d9e 100644 +--- a/ipatests/test_ipaserver/test_changepw.py ++++ b/ipatests/test_ipaserver/test_changepw.py +@@ -53,10 +53,11 @@ class test_changepw(XMLRPC_test, Unauthorized_HTTP_test): + + request.addfinalizer(fin) + +- 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): +@@ -89,6 +90,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 00000000000..9425cb7977f +--- /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 00000000000..4eade8bbaf3 +--- /dev/null ++++ b/ipatests/test_ipaserver/test_referer.py +@@ -0,0 +1,136 @@ ++# 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='HOTP', description='testotp', ++ ipatokenotpalgorithm='sha512', ipatokenowner=testuser, ++ ipatokenotpdigits='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) ++ ++ def test_i18n_messages_valid(self): ++ # i18n_messages requires a valid JSON request and we send ++ # nothing. If we get a 500 error then it got past the ++ # referer check. ++ self.app_uri = "/ipa/i18n_messages" ++ response = self._request() ++ assert_equal(response.status, 500, 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 929c3e899c3..61af0c40d07 100644 +--- a/ipatests/util.py ++++ b/ipatests/util.py +@@ -163,12 +163,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): diff --git a/SPECS/ipa.spec b/SPECS/ipa.spec index 8b180ab..a56d6ff 100644 --- a/SPECS/ipa.spec +++ b/SPECS/ipa.spec @@ -189,7 +189,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 9%{?rc_version:.%rc_version}%{?dist}.alma.1 +Release: 11%{?rc_version:.%rc_version}%{?dist}.alma.1 Summary: The Identity, Policy and Audit system License: GPLv3+ @@ -226,6 +226,12 @@ Patch0013: 0013-Installer-activate-nss-and-pam-services-in-sssd.conf_rhbz#2 # Patches were taken from: # https://gitlab.com/redhat/centos-stream/rpms/ipa/-/commit/5d0ca0e625aea2553a39ae3e56174285cb123f13 Patch0014: 0014-ipa-kdb-Make-AD-SIGNEDPATH-optional-with-krb5-DAL-8.patch +# https://github.com/freeipa/freeipa/commit/13778d88ca2ac73b729821bdea844172a18c0cb9 +Patch0015: 0015-CVE-2023-5455.patch +# https://github.com/freeipa/freeipa/commit/a847e2483b4c4832ee5129901da169f4eb0d1392 +Patch0016: 0016-ipa-kdb-Detect-and-block-Bronze-Bit-attacks.patch +# https://github.com/freeipa/freeipa/commit/86b073a7f03ba0edf4dd91f85b96c89107e9e673 +Patch0017: 0017-Integration-tests-for-verifying-Referer.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch Patch1002: 1002-Revert-freeipa.spec-depend-on-bind-dnssec-utils.patch @@ -1741,6 +1747,11 @@ fi %endif %changelog +* Mon Jan 15 2024 Eduard Abdullin - 4.9.12-11.alma.1 +- CVE-2023-5455 +- ipa-kdb: Detect and block Bronze-Bit attacks +- Integration tests for verifying Referer header in the UI + * Wed Nov 14 2023 Eduard Abdullin - 4.9.12-9.alma.1 - ipa-kdb: Make AD-SIGNEDPATH optional with krb5 DAL 8 and older