Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/auditors/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,37 @@ It is highly recommended to explicitly set the redirect URIs for this client.
The auditor triggers if a client sets an override for the access token lifespan
and if the value is too long.
See the realm auditor [AccessTokenLifespanTooLong](./realm.md#AccessTokenLifespanTooLong) for details.

## SamlClientAssertionSignatureCheck

This auditor identifies SAML clients configured to issue tokens without signing the Assertion block. By default, Keycloak might not enforce signing of the specific assertion element within the SAML response.

This configuration allows for Token Forgery or Signature Exclusion attacks. If the Service Provider (SP) accepts unsigned assertions, an attacker could capture the XML, modify the NameID (username) or Roles, Base64 encode it, and potentially impersonate any user or escalate privileges. To prevent this, the saml.assertion.signature attribute should be set to true, ensuring Keycloak cryptographically signs the assertion block.

## SamlClientEncryptCheck

This auditor checks if the SAML client is configured to encrypt the SAML Assertion. When disabled, the SAML Assertion is transmitted as a standard Base64 encoded XML string, effectively in cleartext.

The absence of encryption leads to Information Disclosure, as intermediaries can easily read Personally Identifiable Information (PII) contained in the assertion. Critically, sending assertions in cleartext significantly facilitates XML Signature Wrapping (XSW) attacks. Without encryption, an attacker can manipulate the XML structure without needing the Service Provider's private key to decrypt and re-encrypt the payload. It is highly recommended to enable saml.encrypt to ensure confidentiality and integrity.

## SamlClientOneTimeUseCheck

This auditor detects if the <OneTimeUse> condition is omitted from SAML Assertions issued by Keycloak. While the Service Provider (SP) is technically responsible for tracking used Assertion IDs (jti) to prevent replay attacks, relying solely on the SP is risky if the SP is stateless or has a flushed cache.

Keycloak should be configured to explicitly add the OneTimeUse condition to the assertion. This acts as a critical defense-in-depth measure against SAML Token Replay attacks. If an attacker steals a token, this condition signals to the SP that the token must not be accepted more than once, enforcing stricter validation rules.

## SamlClientSignatureCheck

This auditor verifies if Keycloak is configured to validate the digital signature of the AuthnRequest sent by the Service Provider. If this check (saml.client.signature) is disabled, Keycloak will process login requests from the SP without verifying their authenticity.

This misconfiguration exposes the system to AuthnRequest Spoofing and Login CSRF. An attacker could generate a fake login request, potentially forcing a victim to log into an attacker-controlled account. Furthermore, if the AssertionConsumerServiceURL is not strictly validated elsewhere, an attacker could alter the request to redirect the token to a malicious location. Keycloak should always be configured to require and verify client signatures for SAML requests.

## SamlClientWeakAlgorithmCheck

This auditor scans SAML clients for the use of weak signature algorithms, specifically RSA_SHA1 or DSA_SHA1. Although these algorithms were standards in the past, they are now considered cryptographically weak and are deprecated in modern security standards.

Using these algorithms leaves the signing process vulnerable to collision attacks, potentially allowing attackers to forge signatures. It is strongly recommended to update the saml.signature.algorithm to a stronger standard, such as RSA_SHA256 or higher, to ensure the cryptographic integrity of the SAML exchange.

## SamlClientWildcardRedirectUriCheck

This auditor identifies SAML clients that utilize wildcard characters /* at the end of their configured Redirect URIs (Assertion Consumer Service URLs). For example, a configuration like http://localhost:9009/* is considered dangerous. Wildcards in this context facilitate Open Redirect vulnerabilities and Token Theft. It allows Keycloak to redirect the user (and the SAML artifact) to any subdirectory or path under the specified domain. If the application running on that domain has an open redirect vulnerability or allows user-generated content, an attacker could manipulate the URL to steal the authorization code or SAML artifact. Redirect URIs should be explicit and specific to prevent unauthorized redirection.
48 changes: 48 additions & 0 deletions docs/auditors/idp.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,51 @@ However, if dynamic synchronization of user attributes and roles with the upstre
This setting can be applied globally to the IDP, affecting all user data, including name and email, or specifically to relevant mappers, allowing for selective updates based on upstream changes.

This finding carries a higher severity compared to the general recommendation for enabling `Force` sync mode due to the explicit use of Identity Provider Mappers, indicating a reliance on upstream IDP data for crucial access control decisions.

## SamlIdpPostBindingResponseCheck

This auditor warns about SAML Identity Providers configured to use the **HTTP-Redirect (GET)** binding instead of the **HTTP-POST** binding for responses.
This occurs when the `Post Binding Response` setting is disabled.

When using HTTP-Redirect, the entire SAML XML payload is encoded into the URL query parameters.
This places sensitive token data into the URL, which is frequently recorded in browser history, proxy logs, and firewall logs, leading to potential data leakage.
Additionally, this configuration risks Denial of Service (DoS) issues, as the large XML payload can easily exceed browser or server URL length limits, causing login failures.

We recommend enabling `Post Binding Response` to ensure the SAML payload is sent within the HTTP body rather than the URL.

## SamlIdpValidateSignatureCheck

This auditor warns about SAML Identity Providers configured with `Validate Signature` set to `false`.
When disabled, Keycloak accepts SAML responses without verifying the digital signature of the upstream Identity Provider.

This is a critical security risk.
Without signature verification, an attacker can forge a completely fabricated SAML response or inject a malicious assertion into a valid response (known as XML Signature Wrapping or XSW).
This effectively allows an attacker to log in as any user, including administrators, without a valid password.
We strongly recommend ensuring that `Validate Signature` is enabled for all SAML providers.

## SamlIdpWantAssertionsEncryptedCheck

This auditor identifies SAML Identity Providers that do not require assertions to be encrypted (`Want Assertions Encrypted` is disabled).
When assertions are unencrypted, they are transported as Base64 strings that can be easily decoded.

Because the assertion passes through the user's browser (User Agent), any Sensitive Personally Identifiable Information (PII) contained within—such as emails, phone numbers, or group memberships—becomes visible in plain text.
This data can be exposed in browser network tabs, browser extensions, and intermediate proxy logs.
To prevent confidentiality breaches and PII leakage, we recommend enabling encryption for assertions.

## SamlIdpWantAssertionsSignedCheck

This auditor warns about SAML Identity Providers where `Want Assertions Signed` is disabled.
While the outer SAML Response envelope might be validly signed (if `Validate Signature` is on), the specific Assertion element containing the user identity is not required to be signed in this configuration.

This allows for **Assertion Substitution** attacks.
An attacker could take a valid, signed response envelope and replace the internal assertion with a forged one, bypassing authentication integrity.
For robust security, both the outer envelope and the inner assertions should be signed to prevent identity spoofing.

## SamlIdpWantAuthnRequestsSignedCheck

This auditor flags SAML Identity Providers where `Want AuthnRequests Signed` is disabled.
In this state, Keycloak sends authentication requests to the Identity Provider without a signature, causing the IdP to treat them as anonymous requests.

This configuration increases the risk of **IdP Confusion** and **Login CSRF** attacks.
It allows an attacker to craft malicious login links that force a user to authenticate against an attacker-controlled IdP or manipulate the login context, potentially leading to session hijacking.
We recommend enabling signed authentication requests to ensure the IdP can verify the origin of the login attempt.
23 changes: 23 additions & 0 deletions kcwarden/auditors/client/saml_client_assertion_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from kcwarden.api import Auditor
from kcwarden.custom_types.result import Severity

class SamlClientAssertionSignatureCheck(Auditor):
DEFAULT_SEVERITY = Severity.High
SHORT_DESCRIPTION = "SAML Assertion block is not signed"
LONG_DESCRIPTION = "Keycloak issues tokens without signing the Assertion block. This allows attackers to modify the NameID (username) or Roles in the XML to commit Token Forgery."
REFERENCE = ""

def should_consider_client(self, client) -> bool:
return self.is_not_ignored(client) and client.get_protocol() == "saml"

@staticmethod
def is_vulnerable(client) -> bool:
attributes = client.get_attributes()
val = attributes.get("saml.assertion.signature", "false")
return val != "true"

def audit(self):
for client in self._DB.get_all_clients():
if self.should_consider_client(client):
if self.is_vulnerable(client):
yield self.generate_finding(client)
23 changes: 23 additions & 0 deletions kcwarden/auditors/client/saml_client_encrypt_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from kcwarden.api import Auditor
from kcwarden.custom_types.result import Severity

class SamlClientEncryptCheck(Auditor):
DEFAULT_SEVERITY = Severity.High
SHORT_DESCRIPTION = "SAML Assertion encryption is disabled"
LONG_DESCRIPTION = "The SAML Assertion is sent in cleartext (Base64 encoded only). This allows intermediaries to read PII and facilitates XML Signature Wrapping (XSW) attacks."
REFERENCE = ""

def should_consider_client(self, client) -> bool:
return self.is_not_ignored(client) and client.get_protocol() == "saml"

@staticmethod
def is_vulnerable(client) -> bool:
attributes = client.get_attributes()
val = attributes.get("saml.encrypt", "false")
return val != "true"

def audit(self):
for client in self._DB.get_all_clients():
if self.should_consider_client(client):
if self.is_vulnerable(client):
yield self.generate_finding(client)
23 changes: 23 additions & 0 deletions kcwarden/auditors/client/saml_client_onetimeuse_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from kcwarden.api import Auditor
from kcwarden.custom_types.result import Severity

class SamlClientOneTimeUseCheck(Auditor):
DEFAULT_SEVERITY = Severity.Medium
SHORT_DESCRIPTION = "SAML OneTimeUse condition not enabled"
LONG_DESCRIPTION = "Keycloak is not configured to add the <OneTimeUse> condition to SAML Assertions. This increases the risk of Replay Attacks if the Service Provider does not strictly track Assertion IDs."
REFERENCE = ""

def should_consider_client(self, client) -> bool:
return self.is_not_ignored(client) and client.get_protocol() == "saml"

@staticmethod
def is_vulnerable(client) -> bool:
attributes = client.get_attributes()
val = attributes.get("saml.onetimeuse.condition", "false")
return val != "true"

def audit(self):
for client in self._DB.get_all_clients():
if self.should_consider_client(client):
if self.is_vulnerable(client):
yield self.generate_finding(client)
23 changes: 23 additions & 0 deletions kcwarden/auditors/client/saml_client_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from kcwarden.api import Auditor
from kcwarden.custom_types.result import Severity

class SamlClientSignatureCheck(Auditor):
DEFAULT_SEVERITY = Severity.High
SHORT_DESCRIPTION = "SAML Client AuthnRequest signature not required"
LONG_DESCRIPTION = "Keycloak is configured not to verify the digital signature of the AuthnRequest sent by the Service Provider. This risks AuthnRequest Spoofing and Login CSRF."
REFERENCE = ""

def should_consider_client(self, client) -> bool:
return self.is_not_ignored(client) and client.get_protocol() == "saml"

@staticmethod
def is_vulnerable(client) -> bool:
attributes = client.get_attributes()
val = attributes.get("saml.client.signature", "false")
return val != "true"

def audit(self):
for client in self._DB.get_all_clients():
if self.should_consider_client(client):
if self.is_vulnerable(client):
yield self.generate_finding(client)
28 changes: 28 additions & 0 deletions kcwarden/auditors/client/saml_client_weak_algorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from kcwarden.api import Auditor
from kcwarden.custom_types.result import Severity

class SamlClientWeakAlgorithmCheck(Auditor):
DEFAULT_SEVERITY = Severity.Medium
SHORT_DESCRIPTION = "Weak SAML Signature Algorithm detected"
LONG_DESCRIPTION = "The client is configured to use RSA_SHA1 or DSA_SHA1. These algorithms are considered weak and vulnerable to collision attacks."
REFERENCE = ""

def should_consider_client(self, client) -> bool:
return self.is_not_ignored(client) and client.get_protocol() == "saml"

@staticmethod
def is_vulnerable(client) -> bool:
attributes = client.get_attributes()
algo = attributes.get("saml.signature.algorithm", "")
weak_algos = ["RSA_SHA1", "DSA_SHA1"]

return algo in weak_algos

def audit(self):
for client in self._DB.get_all_clients():
if self.should_consider_client(client):
if self.is_vulnerable(client):
attributes = getattr(client, "attributes", client.get("attributes", {}))
algo = attributes.get("saml.signature.algorithm", "Unknown")

yield self.generate_finding(client, additional_details={"detected_algorithm": algo})
33 changes: 33 additions & 0 deletions kcwarden/auditors/client/saml_client_wildcard_redirect_uris.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from kcwarden.api import Auditor
from kcwarden.custom_types.result import Severity

class SamlClientWildcardRedirectUriCheck(Auditor):
DEFAULT_SEVERITY = Severity.Medium
SHORT_DESCRIPTION = "Client allows wildcard redirect URIs"
LONG_DESCRIPTION = "The client configuration contains a wildcard (*) at the end of a Redirect URI. This allows open redirects to subdirectories, potentially leading to token theft."
REFERENCE = ""

def should_consider_client(self, client) -> bool:
return self.is_not_ignored(client) and client.get_protocol() == "saml"

def is_vulnerable(self, client) -> bool:
uris = client.get_redirect_uris()

if not uris:
return False

for uri in uris:
# Check for trailing wildcard
if uri and uri.strip().endswith("*"):
return True
return False

def audit(self):
for client in self._DB.get_all_clients():
if self.should_consider_client(client):
if self.is_vulnerable(client):
# Re-fetch URIs for the report detail
uris = client.get_redirect_uris()
bad_uris = [u for u in uris if u.endswith("*")]

yield self.generate_finding(client, additional_details={"vulnerable_uris": bad_uris})
23 changes: 23 additions & 0 deletions kcwarden/auditors/idp/saml_idp_post_binding_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from kcwarden.api import Auditor
from kcwarden.custom_types.result import Severity

class SamlIdpPostBindingResponseCheck(Auditor):
DEFAULT_SEVERITY = Severity.Low
SHORT_DESCRIPTION = "SAML IdP uses HTTP-Redirect (GET) binding"
LONG_DESCRIPTION = "The 'Post Binding Response' setting is disabled, forcing the use of HTTP-Redirect (GET). This places the entire SAML XML payload into URL query parameters, leading to potential data leakage in logs and Denial of Service due to URL length limits."
REFERENCE = ""

def should_consider_idp(self, idp) -> bool:
return self.is_not_ignored(idp) and idp.get_provider_id() == "saml"

def is_vulnerable(self, idp) -> bool:
config = idp.get_config()
return config.get("postBindingResponse", "false") != "true"

def audit(self):
for idp in self._DB.get_all_identity_providers():
if not self.should_consider_idp(idp):
continue
if not self.is_vulnerable(idp):
continue
yield self.generate_finding(idp)
24 changes: 24 additions & 0 deletions kcwarden/auditors/idp/saml_idp_validate_signature.py
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 SamlIdpValidateSignatureCheck(Auditor):
DEFAULT_SEVERITY = Severity.High
SHORT_DESCRIPTION = "SAML IdP 'Validate Signature' is disabled"
LONG_DESCRIPTION = "The Identity Provider is configured with 'validateSignature' set to false. Keycloak will not verify the digital signature of incoming SAML documents, allowing for token forgery."
REFERENCE = ""

def should_consider_idp(self, idp) -> bool:
return self.is_not_ignored(idp) and idp.get_provider_id() == "saml"

def is_vulnerable(self, idp) -> bool:
config = idp.get_config()
val = config.get("validateSignature", "false")
return val != "true"

def audit(self):
for idp in self._DB.get_all_identity_providers():
if not self.should_consider_idp(idp):
continue
if not self.is_vulnerable(idp):
continue
yield self.generate_finding(idp)
24 changes: 24 additions & 0 deletions kcwarden/auditors/idp/saml_idp_want_assertions_encrypted.py
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 SamlIdpWantAssertionsEncryptedCheck(Auditor):
DEFAULT_SEVERITY = Severity.Medium
SHORT_DESCRIPTION = "SAML IdP 'Want Assertions Encrypted' is disabled"
LONG_DESCRIPTION = "The Identity Provider accepts unencrypted assertions. This exposes PII to intermediaries and makes the system more susceptible to XML Signature Wrapping (XSW) attacks."
REFERENCE = ""

def should_consider_idp(self, idp) -> bool:
return self.is_not_ignored(idp) and idp.get_provider_id() == "saml"

def is_vulnerable(self, idp) -> bool:
config = idp.get_config()
val = config.get("wantAssertionsEncrypted", "false")
return val != "true"

def audit(self):
for idp in self._DB.get_all_identity_providers():
if not self.should_consider_idp(idp):
continue
if not self.is_vulnerable(idp):
continue
yield self.generate_finding(idp)
24 changes: 24 additions & 0 deletions kcwarden/auditors/idp/saml_idp_want_assertions_signed.py
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 SamlIdpWantAssertionsSignedCheck(Auditor):
DEFAULT_SEVERITY = Severity.High
SHORT_DESCRIPTION = "SAML IdP 'Want Assertions Signed' is disabled"
LONG_DESCRIPTION = "The Identity Provider does not require SAML Assertions to be signed. This may allow attackers to modify the assertion content (e.g., username/roles) even if the envelope signature is valid, or if used in conjunction with other flaws."
REFERENCE = ""

def should_consider_idp(self, idp) -> bool:
return self.is_not_ignored(idp) and idp.get_provider_id() == "saml"

def is_vulnerable(self, idp) -> bool:
config = idp.get_config()
val = config.get("wantAssertionsSigned", "false")
return val != "true"

def audit(self):
for idp in self._DB.get_all_identity_providers():
if not self.should_consider_idp(idp):
continue
if not self.is_vulnerable(idp):
continue
yield self.generate_finding(idp)
Loading