diff --git a/.werks/16218 b/.werks/16218
new file mode 100644
index 00000000000..42bb703b276
--- /dev/null
+++ b/.werks/16218
@@ -0,0 +1,21 @@
+Title: Fix 2FA bypass via RestAPI
+Class: security
+Compatible: compat
+Component: wato
+Date: 1725874171
+Edition: cre
+Level: 1
+Version: 2.2.0p34
+
+Previous to this Werk the RestAPI did not properly check if a user that is supposed to authenticated with multiple factors indeed authenticated fully.
+
+This issue was found during internal review.
+
+Affected Versions:
+
+LI: 2.3.0
+LI: 2.2.0
+
+Vulnerability Management:
+
+We have rated the issue with a CVSS Score of 9.2 High (CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
) and assigned CVE-2024-8606
.
diff --git a/cmk/gui/session.py b/cmk/gui/session.py
index 049bb170ac2..0f84e04355d 100644
--- a/cmk/gui/session.py
+++ b/cmk/gui/session.py
@@ -173,6 +173,14 @@ def persist(self) -> None:
def invalidate(self) -> None:
self.session_info.logged_out = True
+ def two_factor_pending(self) -> bool:
+ if isinstance(self.user, (LoggedInNobody, LoggedInSuperUser)):
+ return False
+ return (
+ userdb.is_two_factor_login_enabled(self.user.ident)
+ and not self.session_info.two_factor_completed
+ )
+
class FileBasedSession(SessionInterface):
"""A "session" which loads its information from a .mk file
diff --git a/cmk/gui/wsgi/applications/rest_api.py b/cmk/gui/wsgi/applications/rest_api.py
index 02fdc0b9de1..8536a0a9531 100644
--- a/cmk/gui/wsgi/applications/rest_api.py
+++ b/cmk/gui/wsgi/applications/rest_api.py
@@ -322,6 +322,8 @@ def ensure_authenticated(persist: bool = True) -> typing.Iterator[None]:
if session.session.exc:
raise session.session.exc
raise MKAuthException("You need to be logged in to access this resource.")
+ if session.session.two_factor_pending():
+ raise MKAuthException("Two-factor authentication required.")
yield
diff --git a/cmk/gui/wsgi/applications/utils.py b/cmk/gui/wsgi/applications/utils.py
index bf7c286cbca..145a43b8f8b 100644
--- a/cmk/gui/wsgi/applications/utils.py
+++ b/cmk/gui/wsgi/applications/utils.py
@@ -68,11 +68,7 @@ def _call_auth() -> Response:
"user_webauthn_login_complete",
)
- if (
- not two_factor_ok
- and userdb.is_two_factor_login_enabled(user_id)
- and not session.session_info.two_factor_completed
- ):
+ if not two_factor_ok and session.two_factor_pending():
raise HTTPRedirect(
"user_login_two_factor.py?_origtarget=%s" % urlencode(makeuri(request, []))
)
diff --git a/tests/integration/cmk/gui/test_login.py b/tests/integration/cmk/gui/test_login.py
index 3c095a2bef9..be59d6828e7 100644
--- a/tests/integration/cmk/gui/test_login.py
+++ b/tests/integration/cmk/gui/test_login.py
@@ -9,6 +9,8 @@
from tests.testlib import CMKWebSession
from tests.testlib.site import Site
+from cmk.gui.type_defs import TwoFactorCredentials, WebAuthnCredential
+
def test_01_login_and_logout(site: Site) -> None:
web = CMKWebSession(site)
@@ -249,7 +251,11 @@ def test_failed_login_counter_human(site: Site) -> None:
# Login form
session.post(
"login.py",
- params={"_username": username, "_password": "wrong_password", "_login": "Login"},
+ params={
+ "_username": username,
+ "_password": "wrong_password",
+ "_login": "Login",
+ },
allow_redirect_to_login=True,
)
@@ -283,3 +289,49 @@ def test_failed_login_counter_automation(site: Site) -> None:
expected_code=401,
)
assert 0 == _get_failed_logins(site, username)
+
+
+@contextlib.contextmanager
+def enable_2fa(site: Site, username: str) -> Iterator[None]:
+ """This will mimic 2fa. it will not work in the UI and lead to crashes...
+ In master/2.3 we use totp which is easier to mimic...
+
+ Caution, this overrides any previous 2fa configs"""
+
+ site.write_text_file(
+ f"var/check_mk/web/{username}/two_factor_credentials.mk",
+ repr(
+ TwoFactorCredentials(
+ webauthn_credentials={
+ "foo": WebAuthnCredential(
+ credential_id="foo",
+ registered_at=0,
+ alias="foo",
+ credential_data=b"foo",
+ )
+ },
+ backup_codes=[],
+ )
+ ),
+ )
+ try:
+ yield
+ finally:
+ site.delete_file(f"var/check_mk/web/{username}/two_factor_credentials.mk")
+
+
+def test_rest_api_access_with_enabled_2fa(site: Site) -> None:
+ """you're not supposed to access the rest api if you have 2fa enabled (except for cookie auth)
+
+ See: CMK-18988"""
+ username = "cmkadmin"
+ password = site.admin_password
+ with enable_2fa(site, "cmkadmin"):
+ session = CMKWebSession(site)
+ response = session.get(
+ f"/{site.id}/check_mk/api/1.0/version",
+ auth=(username, password),
+ expected_code=401,
+ )
+ assert not "site" in response.json()
+ assert not session.is_logged_in()