- CVE-2023-5455

- ipa-kdb: Detect and block Bronze-Bit attacks
This commit is contained in:
eabdullin 2024-01-15 15:06:33 +03:00
parent 697efbdf54
commit e82ab6efc0
4 changed files with 748 additions and 1 deletions

View File

@ -0,0 +1,118 @@
From 13778d88ca2ac73b729821bdea844172a18c0cb9 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
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 <rcritten@redhat.com>
---
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 = """<html>
</html>"""
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

View File

@ -0,0 +1,262 @@
From a847e2483b4c4832ee5129901da169f4eb0d1392 Mon Sep 17 00:00:00 2001
From: Julien Rische <jrische@redhat.com>
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:

View File

@ -0,0 +1,356 @@
From 86b073a7f03ba0edf4dd91f85b96c89107e9e673 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
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 <rcritten@redhat.com>
---
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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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):

View File

@ -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 <eabdullin@almalinux.org> - 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 <eabdullin@almalinux.org> - 4.9.12-9.alma.1
- ipa-kdb: Make AD-SIGNEDPATH optional with krb5 DAL 8 and older