feat: implement banned prefix list, security fixes, validation fixes for some endpoints

This commit is contained in:
Aidan 2025-03-18 10:00:00 -04:00
parent b2906afe5d
commit 5504091e4d
6 changed files with 108 additions and 5 deletions

60
data/bannedprefix.txt Normal file
View File

@ -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

View File

@ -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<void> => {
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<void> =>
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 {

View File

@ -8,7 +8,12 @@ const db = drizzle(process.env.DB_FILE_NAME!);
export const getUserAccount = async (req: Request, res: Response): Promise<void> => {
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<void>
return;
}
console.log("[*] Task started\nTASK| getUserAccount\nACC |", email);
try {
const account = await db
.select()

View File

@ -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`);
}
});

View File

@ -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<RateLimitConfig> => {
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);

View File

@ -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<boolean> => {
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();