From 5504091e4ddbeee5c9fe017215994837abaf0e33 Mon Sep 17 00:00:00 2001 From: Aidan Honor Date: Tue, 18 Mar 2025 10:00:00 -0400 Subject: [PATCH] feat: implement banned prefix list, security fixes, validation fixes for some endpoints --- data/bannedprefix.txt | 60 ++++++++++++++++++++++++++ src/actions/accounts/addAccount.ts | 15 +++++++ src/actions/accounts/getUserAccount.ts | 9 +++- src/server.ts | 4 +- src/utils/rateLimit.ts | 6 ++- src/utils/validators.ts | 19 ++++++++ 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 data/bannedprefix.txt diff --git a/data/bannedprefix.txt b/data/bannedprefix.txt new file mode 100644 index 0000000..3faeaa8 --- /dev/null +++ b/data/bannedprefix.txt @@ -0,0 +1,60 @@ +postmaster +webmaster +admin +administrator +hostmaster +info +support +contact +abuse +security +root +billing +sales +service +helpdesk +management +moderator +owner +verify +verification +access +login +register +registration +subscribe +subscription +unsubscribe +unsubscription +password +reset +help +data +manager +recovery +restore +sysadmin +audit +domainadmin +dns +auth +2fa +mfa +otp +it +finance +legal +compliance +test +example +sandbox +alert +alerts +notice +vaultwarden +pass +gitea +notification +feedback +no-reply +noreply \ No newline at end of file diff --git a/src/actions/accounts/addAccount.ts b/src/actions/accounts/addAccount.ts index 9551f0a..32c34ed 100644 --- a/src/actions/accounts/addAccount.ts +++ b/src/actions/accounts/addAccount.ts @@ -6,18 +6,31 @@ import { eq } from "drizzle-orm"; import fs from "fs/promises"; import { drizzle } from "drizzle-orm/bun-sqlite"; import { updateAccountsCache } from "../../utils/updateAccountsCache"; +import { isBannedPrefix } from "../../utils/validators"; const db = drizzle(process.env.DB_FILE_NAME!); export const addAccount = async (req: Request, res: Response): Promise => { const { email, password, migrate } = req.body; + if (!email || !password) { + console.log("[!] Error\nTASK| addAccount\nERR | Missing email or password"); + res.status(400).json({ error: "Missing email or password" }); + return; + } + if (!validateEmail(email)) { console.log("[!] Error\nTASK| addAccount\nERR | Invalid email format"); res.status(400).json({ error: "Invalid email format" }); return; } + if (await isBannedPrefix(email)) { + console.log("[!] Error\nTASK| addAccount\nERR | Banned email prefix\nACC |", email); + res.status(400).json({ error: "Banned email prefix" }); + return; + } + let finalPassword = password; if (migrate) { @@ -27,6 +40,8 @@ export const addAccount = async (req: Request, res: Response): Promise => const line = data.split("\n").find(l => l.trim() === email); if (!line) { console.log("[!] Error\nTASK| addAccount (subtask: migrate)\nERR | Account not found in migrate.txt\nACC |", email); + + // A backend error is returned so users do not attempt to abuse the migration form res.status(500).json({ error: "Backend error" }); return; } else { diff --git a/src/actions/accounts/getUserAccount.ts b/src/actions/accounts/getUserAccount.ts index 3271801..5e42aff 100644 --- a/src/actions/accounts/getUserAccount.ts +++ b/src/actions/accounts/getUserAccount.ts @@ -8,7 +8,12 @@ const db = drizzle(process.env.DB_FILE_NAME!); export const getUserAccount = async (req: Request, res: Response): Promise => { const { email } = req.body; - console.log("[*] Task started\nTASK| getUserAccount\nACC |", email); + + if (!email) { + console.log("[!] Error\nTASK| getUserAccount\nERR | Missing email\nACC |", email); + res.status(400).json({error: "Missing email"}); + return + } if (!validateEmail(email)) { console.log("[!] Error\nTASK| getUserAccount\nERR | Invalid email format\nACC |", email); @@ -16,6 +21,8 @@ export const getUserAccount = async (req: Request, res: Response): Promise return; } + console.log("[*] Task started\nTASK| getUserAccount\nACC |", email); + try { const account = await db .select() diff --git a/src/server.ts b/src/server.ts index fd3c340..d476614 100644 --- a/src/server.ts +++ b/src/server.ts @@ -33,12 +33,12 @@ app.listen(PORT, () => { figlet('mail-connect', (err, data) => { if (err) { console.log('mail-connect'); - console.log('Version: 0.1.0'); + console.log('Version: 0.1.1'); console.log(`API listening on port ${PORT}\n`); console.dir("[!] " + err); } else { console.log(data); - console.log('Version: 0.1.0'); + console.log('Version: 0.1.1'); console.log(`API listening on port ${PORT}\n`); } }); diff --git a/src/utils/rateLimit.ts b/src/utils/rateLimit.ts index 63c45c9..fbf7367 100644 --- a/src/utils/rateLimit.ts +++ b/src/utils/rateLimit.ts @@ -1,5 +1,6 @@ import rateLimit from "express-rate-limit"; import fs from "fs/promises"; +import path from "path"; interface RateLimitOptions { windowMs: number; @@ -13,8 +14,9 @@ interface RateLimitConfig { export const loadRateLimitConfig = async (): Promise => { try { - await fs.access(`${process.env.MAILCONNECT_ROOT_DIR}/ratelimit.json`); - const data = await fs.readFile(`${process.env.MAILCONNECT_ROOT_DIR}/ratelimit.json`, "utf8"); + const rateLimitPath = path.join(process.cwd(), "ratelimit.json"); + await fs.access(rateLimitPath); + const data = await fs.readFile(rateLimitPath, "utf8"); return JSON.parse(data); } catch (err) { console.error("[!] Error loading ratelimit config:\n", err); diff --git a/src/utils/validators.ts b/src/utils/validators.ts index 04ed3a2..46f7854 100644 --- a/src/utils/validators.ts +++ b/src/utils/validators.ts @@ -1,8 +1,27 @@ import validator from "validator"; import PasswordValidator from "password-validator"; +import fs from "fs/promises"; +import path from "path"; + +let bannedPrefixesCache: string[] | null = null; export const validateEmail = (email: string): boolean => validator.isEmail(email); +export const isBannedPrefix = async (email: string): Promise => { + try { + if (!bannedPrefixesCache) { + const filePath = path.join(process.cwd(), "data", "bannedprefix.txt"); + const data = await fs.readFile(filePath, "utf8"); + bannedPrefixesCache = data.split("\n").filter(prefix => prefix.trim()); + } + const prefix = email.split("@")[0]; + return bannedPrefixesCache.includes(prefix); + } catch (error) { + console.error("[!] Error checking for banned prefix:", error); + return false; + } +} + export const passwordSchema = new PasswordValidator(); passwordSchema.is().min(8).is().max(64).has().letters().has().digits().has().not().spaces();