-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New Auditor: Flag Identity Providers with disabled signature verifica…
…tion (#37) Add auditor for OIDC and SAML IDPs without signature verification
- Loading branch information
Showing
3 changed files
with
128 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
kcwarden/auditors/idp/identity_provider_with_signature_verification_disabled.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from kcwarden.api import Auditor | ||
from kcwarden.custom_types.result import Severity | ||
|
||
|
||
class IdentityProviderWithSignatureVerificationDisabled(Auditor): | ||
DEFAULT_SEVERITY = Severity.Critical | ||
SHORT_DESCRIPTION = "Identity Provider does not verify upstream IDPs signatures" | ||
LONG_DESCRIPTION = "Keycloak allows you to configure external identity providers. When using OpenID Connect, it is important to verify the cryptographic signature that secures the identity and access tokens generated by the identity provider. This IDP configuration disables this signature check, which makes it vulnerable to accepting forged access tokens, leading to account takeover and other security issues." | ||
REFERENCE = "" | ||
|
||
def should_consider_idp(self, idp) -> bool: | ||
return self.is_not_ignored(idp) and idp.get_provider_id() in ["oidc", "keycloak-oidc", "saml"] | ||
|
||
def idp_does_not_verify_signatures(self, config): | ||
return config.get("validateSignature") == "false" | ||
|
||
def audit(self): | ||
for idp in self._DB.get_all_identity_providers(): | ||
# Skip IDPs that were explicitly ignored, or that aren't OIDC IDPs | ||
if not self.should_consider_idp(idp): | ||
continue | ||
# Check if signature verification is disabled | ||
if self.idp_does_not_verify_signatures(idp.get_config()): | ||
yield self.generate_finding(idp) |
94 changes: 94 additions & 0 deletions
94
tests/auditors/idp/test_oidc_identity_provider_with_signature_verification_disabled.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import pytest | ||
from unittest.mock import Mock | ||
|
||
from kcwarden.auditors.idp.identity_provider_with_signature_verification_disabled import ( | ||
IdentityProviderWithSignatureVerificationDisabled, | ||
) | ||
from kcwarden.custom_types import config_keys | ||
|
||
|
||
class TestOIDCIdentityProviderWithSignatureVerificationDisabled: | ||
@pytest.fixture | ||
def auditor(self, database, default_config): | ||
auditor_instance = IdentityProviderWithSignatureVerificationDisabled(database, default_config) | ||
auditor_instance._DB = Mock() | ||
return auditor_instance | ||
|
||
@pytest.mark.parametrize( | ||
"provider_id, expected", | ||
[ | ||
("oidc", True), # OIDC provider should be considered | ||
("keycloak-oidc", True), # Keycloak OIDC provider should be considered | ||
("saml", True), # SAML provider should also be considered | ||
("github", False), # SAML provider should also be considered | ||
], | ||
) | ||
def test_should_consider_idp(self, auditor, provider_id, expected): | ||
mock_idp = Mock() | ||
mock_idp.get_provider_id.return_value = provider_id | ||
assert auditor.should_consider_idp(mock_idp) == expected | ||
|
||
@pytest.mark.parametrize( | ||
"config, expected", | ||
[ | ||
({"validateSignature": "true"}, False), # Signature verification enabled | ||
({"validateSignature": "false"}, True), # Signature verification disabled | ||
], | ||
) | ||
def test_idp_does_not_enforce_pkce(self, auditor, config, expected): | ||
assert auditor.idp_does_not_verify_signatures(config) == expected | ||
|
||
def test_audit_function_no_findings(self, auditor, mock_idp): | ||
# Setup IDP with correct PKCE configuration | ||
mock_idp.get_provider_id.return_value = "oidc" | ||
mock_idp.get_config.return_value = {"validateSignature": "true"} | ||
auditor._DB.get_all_identity_providers.return_value = [mock_idp] | ||
|
||
results = list(auditor.audit()) | ||
assert len(results) == 0 | ||
|
||
def test_audit_function_with_findings(self, auditor, mock_idp): | ||
# Setup IDP without correct PKCE configuration | ||
mock_idp.get_provider_id.return_value = "oidc" | ||
mock_idp.get_config.return_value = {"validateSignature": "false"} | ||
auditor._DB.get_all_identity_providers.return_value = [mock_idp] | ||
|
||
results = list(auditor.audit()) | ||
assert len(results) == 1 | ||
|
||
def test_audit_function_multiple_idps(self, auditor): | ||
# Create separate mock IDPs with distinct settings | ||
idp1 = Mock() | ||
idp1.get_provider_id.return_value = "oidc" | ||
idp1.get_config.return_value = {"validateSignature": "false"} | ||
|
||
idp2 = Mock() | ||
idp2.get_provider_id.return_value = "oidc" | ||
idp2.get_config.return_value = {"validateSignature": "true"} | ||
|
||
idp3 = Mock() | ||
idp3.get_provider_id.return_value = "keycloak-oidc" | ||
idp3.get_config.return_value = {"validateSignature": "false"} | ||
|
||
idp4 = Mock() | ||
idp4.get_provider_id.return_value = "saml" | ||
idp4.get_config.return_value = {"validateSignature": "false"} | ||
|
||
auditor._DB.get_all_identity_providers.return_value = [idp1, idp2, idp3, idp4] | ||
results = list(auditor.audit()) | ||
assert len(results) == 3 # Expect findings from idp1, idp3 and idp4 | ||
|
||
def test_ignore_list_functionality(self, auditor, mock_idp): | ||
# Setup IDP without force sync mode and with mappers | ||
# Setup IDP without correct PKCE configuration | ||
mock_idp.get_provider_id.return_value = "oidc" | ||
mock_idp.get_config.return_value = {"validateSignature": "false"} | ||
mock_idp.get_alias.return_value = "ignored_idp" | ||
mock_idp.get_name.return_value = mock_idp.get_alias.return_value | ||
auditor._DB.get_all_identity_providers.return_value = [mock_idp] | ||
|
||
# Add the IDP to the ignore list | ||
auditor._CONFIG = {config_keys.AUDITOR_CONFIG: {auditor.get_classname(): ["ignored_idp"]}} | ||
|
||
results = list(auditor.audit()) | ||
assert len(results) == 0 # No findings due to ignore list |