🔒️ Use email normalization to prevent ban evasion (#1615)

This commit is contained in:
Luke Vella 2025-03-06 10:29:38 +00:00 committed by GitHub
parent f3ffe71df3
commit 3d8604a379
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 10 deletions

View file

@ -1,4 +1,95 @@
export const isEmailBlocked = (email: string) => {
import { prisma } from "@rallly/database";
/**
* Normalizes an email address by removing aliases and standardizing format
* based on the email provider's alias conventions.
*
* Handles:
* - Gmail: Removes dots and plus aliases (user.name+alias@gmail.com username@gmail.com)
* - Yahoo: Removes hyphen aliases (user-alias@yahoo.com user@yahoo.com)
* - Outlook/Hotmail/Live: Removes plus aliases
* - iCloud: Removes plus aliases
* - ProtonMail: Removes plus aliases
* - FastMail: Removes plus aliases
* - Hey: Removes plus aliases
*
* @param email The email address to normalize
* @returns The normalized email address
*/
function normalizeEmail(email: string): string {
if (!email || !email.includes("@")) {
return email;
}
const parts = email.toLowerCase().split("@");
let localPart = parts[0];
const domain = parts[1];
// Handle Gmail's dot-ignoring and plus aliases
if (domain === "gmail.com" || domain === "googlemail.com") {
// Remove all dots from the local part
localPart = localPart.replace(/\./g, "");
// Remove everything after the first plus
localPart = localPart.split("+")[0];
}
// Handle Yahoo's hyphen aliases
else if (
domain === "yahoo.com" ||
domain === "ymail.com" ||
domain === "rocketmail.com"
) {
// Remove everything after the first hyphen
localPart = localPart.split("-")[0];
}
// Handle plus aliases for other common providers
else if (
[
"outlook.com",
"hotmail.com",
"live.com",
"msn.com",
"icloud.com",
"me.com",
"mac.com",
"protonmail.com",
"pm.me",
"proton.me",
"fastmail.com",
"fastmail.fm",
"hey.com",
].includes(domain)
) {
// Remove everything after the first plus
localPart = localPart.split("+")[0];
}
return `${localPart}@${domain}`;
}
/**
* Checks if a user is banned by their email address,
* taking into account email aliases that could be used to bypass bans.
*
* @param email The email address to check
* @returns Whether the user with this email is banned
*/
export async function isEmailBanned(email: string) {
const normalizedEmail = normalizeEmail(email);
// Check both the original and normalized emails
const bannedUser = await prisma.user.count({
where: {
OR: [
{ email, banned: true },
{ email: normalizedEmail, banned: true },
],
},
});
return bannedUser > 0;
}
export function isEmailBlocked(email: string) {
if (process.env.ALLOWED_EMAILS) {
const allowedEmails = process.env.ALLOWED_EMAILS.split(",");
// Check whether the email matches enough of the patterns specified in ALLOWED_EMAILS
@ -15,5 +106,6 @@ export const isEmailBlocked = (email: string) => {
return true;
}
}
return false;
};
}

View file

@ -6,7 +6,7 @@ import type { Provider } from "next-auth/providers";
import z from "zod";
import { CustomPrismaAdapter } from "./auth/adapters/prisma";
import { isEmailBlocked } from "./auth/helpers/is-email-blocked";
import { isEmailBanned, isEmailBlocked } from "./auth/helpers/is-email-blocked";
import { mergeGuestsIntoUser } from "./auth/helpers/merge-user";
import { getLegacySession } from "./auth/legacy/next-auth-cookie-migration";
import { EmailProvider } from "./auth/providers/email";
@ -104,15 +104,14 @@ const {
});
return false;
}
if (user.banned) {
return false;
}
// Make sure email is allowed
if (user.email) {
const isBlocked = isEmailBlocked(user.email);
if (isBlocked) {
return false;
}
// Check if user is banned
if (user.banned) {
if (isEmailBlocked(user.email) || (await isEmailBanned(user.email))) {
return false;
}
}