diff --git a/src/authdrift/rules/oauth-email-key-extended.yaml b/src/authdrift/rules/oauth-email-key-extended.yaml new file mode 100644 index 0000000..1931c5d --- /dev/null +++ b/src/authdrift/rules/oauth-email-key-extended.yaml @@ -0,0 +1,69 @@ +rules: + - id: authlib-google-oauth-email-as-primary-key + message: | + This authlib OAuth handler is using `userinfo['email']` as a user lookup key. + + When a user renames their Gmail address (Google rolled out Gmail rename in + March 2026), the email returned by Google changes and your application will + silently create a duplicate user record. The same drift occurs in ~0.04% of + normal Sign-in-with-Google logins (Truffle Security, January 2025). + + Fix: use `userinfo['sub']` (the OIDC subject claim) as the immutable primary + key. Treat `userinfo['email']` as a mutable contact attribute, not an + identifier. + + See: https://github.com/Neelagiri65/authdrift#why-this-matters + severity: WARNING + languages: + - python + patterns: + - pattern-either: + - pattern: $MODEL.objects.get(email=$UI['email']) + - pattern: $MODEL.objects.filter(email=$UI['email']) + - pattern: $MODEL.objects.get(email=$UI.get('email')) + - pattern: $MODEL.query.filter_by(email=$UI['email']) + - pattern: db.session.query($MODEL).filter_by(email=$UI['email']) + + - id: firebase-auth-getUserByEmail + message: | + This code uses `getUserByEmail()` to resolve a Firebase Auth user. + + When a user renames their Gmail address (Google rolled out Gmail rename in + March 2026), the email returned by the identity provider changes and your + application will create a duplicate user record on next sign-in. + + Fix: use `getUserByUid()` or key on `user.uid` (the immutable Firebase + subject ID). Treat email as a mutable contact attribute. + + See: https://github.com/Neelagiri65/authdrift#why-this-matters + severity: WARNING + languages: + - javascript + - typescript + patterns: + - pattern-either: + - pattern: admin.auth().getUserByEmail($EMAIL) + - pattern: getAuth().getUserByEmail($EMAIL) + - pattern: $AUTH.getUserByEmail($EMAIL) + + - id: lucia-auth-email-as-primary-key + message: | + This Lucia auth flow is resolving users by email instead of the provider's + immutable subject ID. + + When a user renames their Gmail address (Google rolled out Gmail rename in + March 2026), the email returned by the provider changes and a duplicate user + record will be created on next sign-in. + + Fix: key the lookup on the provider's `providerUserId` (OIDC `sub` claim) + and treat email as a mutable contact attribute. + + See: https://github.com/Neelagiri65/authdrift#why-this-matters + severity: WARNING + languages: + - javascript + - typescript + patterns: + - pattern-either: + - pattern: db.table("user").where("email", "=", $EMAIL) + - pattern: db.getUserByEmail($EMAIL) diff --git a/tests/fixtures/authlib-safe.py b/tests/fixtures/authlib-safe.py new file mode 100644 index 0000000..83e13bf --- /dev/null +++ b/tests/fixtures/authlib-safe.py @@ -0,0 +1,28 @@ +# Should NOT trigger any rules — using sub as primary key +from authlib.integrations.flask_client import OAuth +from flask import Flask +from myapp.models import User + +app = Flask(__name__) +oauth = OAuth(app) + +google = oauth.register( + name="google", + client_id="...", + client_secret="...", + server_metadata_url="https://accounts.google.com/.well-known/openid-configuration", + client_kwargs={"scope": "openid email profile"}, +) + + +@app.route("/auth/callback") +def auth_callback(): + token = google.authorize_access_token() + userinfo = token.get("userinfo") + + # SAFE: using sub (OIDC subject ID) as immutable primary key + user = User.objects.get(provider="google", provider_uid=userinfo["sub"]) + # email is stored as mutable contact attribute only + user.email = userinfo.get("email", "") + user.save() + return user diff --git a/tests/fixtures/authlib-vulnerable.py b/tests/fixtures/authlib-vulnerable.py new file mode 100644 index 0000000..555b8d1 --- /dev/null +++ b/tests/fixtures/authlib-vulnerable.py @@ -0,0 +1,25 @@ +# Should trigger authlib-google-oauth-email-as-primary-key +from authlib.integrations.flask_client import OAuth +from flask import Flask +from myapp.models import User + +app = Flask(__name__) +oauth = OAuth(app) + +google = oauth.register( + name="google", + client_id="...", + client_secret="...", + server_metadata_url="https://accounts.google.com/.well-known/openid-configuration", + client_kwargs={"scope": "openid email profile"}, +) + + +@app.route("/auth/callback") +def auth_callback(): + token = google.authorize_access_token() + userinfo = token.get("userinfo") + + # VULNERABLE: using email as primary lookup key + user = User.objects.get(email=userinfo["email"]) + return user diff --git a/tests/fixtures/firebase-vulnerable.js b/tests/fixtures/firebase-vulnerable.js new file mode 100644 index 0000000..64bc6a3 --- /dev/null +++ b/tests/fixtures/firebase-vulnerable.js @@ -0,0 +1,8 @@ +// Should trigger firebase-auth-getUserByEmail +const admin = require("firebase-admin"); + +async function handleSignIn(email) { + // VULNERABLE: resolving user by email instead of UID + const user = await admin.auth().getUserByEmail(email); + return user; +}