From 349053954d45e4625ab35e6b2383608e5132eba3 Mon Sep 17 00:00:00 2001 From: gumi Date: Tue, 3 Mar 2020 23:02:36 -0500 Subject: add rudimentary anti-bruteforcing --- src/api.js | 2 ++ src/brute.js | 18 ++++++++++++ src/routers/vault/middlewares/evol/account.js | 12 ++++++-- src/routers/vault/middlewares/identity.js | 1 + src/routers/vault/middlewares/legacy/account.js | 39 ++++++++++++++++++++----- src/routers/vault/middlewares/session.js | 18 ++++++++++-- 6 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 src/brute.js diff --git a/src/api.js b/src/api.js index 8347e1c..5a8188f 100644 --- a/src/api.js +++ b/src/api.js @@ -2,6 +2,7 @@ const express = require("express"); // from npm registry const https = require("https"); // built-in const Limiter = require("./limiter.js"); const Logger = require("./logger.js"); +const Brute = require("./brute.js"); const api = express(); if (!process.env.NODE_ENV) { @@ -28,6 +29,7 @@ api.locals = Object.assign({ from: process.env.MAILER__FROM, }, logger: Logger, + brute: Brute, }, api.locals); diff --git a/src/brute.js b/src/brute.js new file mode 100644 index 0000000..2ea767e --- /dev/null +++ b/src/brute.js @@ -0,0 +1,18 @@ +const limiters = new Map(); // Map> + +const consume = (req, max = 5, expire = 3.6e6) => { + const route = req.method + req.baseUrl + req.path; + const route_map = limiters.get(route) || limiters.set(route, new Map()).get(route); + const attempts = route_map.get(req.ip) || route_map.set(req.ip, []).get(req.ip); + + if (attempts.length >= max) { + return 0; + } else { + attempts.push(setTimeout(() => attempts.pop(), expire)); + return max - attempts.length; + } +}; + +module.exports = { + consume, +}; diff --git a/src/routers/vault/middlewares/evol/account.js b/src/routers/vault/middlewares/evol/account.js index 5067334..80f741d 100644 --- a/src/routers/vault/middlewares/evol/account.js +++ b/src/routers/vault/middlewares/evol/account.js @@ -13,8 +13,16 @@ const get_account_list = async (req, vault_id) => { where: {vaultId: vault_id}, }); - for (let acc of claimed) { - acc = await req.app.locals.evol.login.findByPk(acc.accountId); + for (const acc_ of claimed) { + const acc = await req.app.locals.evol.login.findByPk(acc_.accountId); + + if (acc === null || acc === undefined) { + // unexpected: account was deleted + console.info(`Vault.evol.account: unlinking deleted account ${acc_.accountId} {${vault_id}} [${req.ip}]`); + await acc_.destroy(); // un-claim the account + continue; + } + const chars = []; const chars_ = await req.app.locals.evol.char.findAll({ where: {accountId: acc.accountId}, diff --git a/src/routers/vault/middlewares/identity.js b/src/routers/vault/middlewares/identity.js index 51a986f..638d5bc 100644 --- a/src/routers/vault/middlewares/identity.js +++ b/src/routers/vault/middlewares/identity.js @@ -229,6 +229,7 @@ const add_identity = async (req, res, next) => { if (process.env.NODE_ENV === "development") { console.log(`uuid: ${uuid}`); } else { + // TODO: limit total number of emails that can be dispatched by a single ip in an hour transporter.sendMail({ from: process.env.VAULT__MAILER__FROM, to: req.body.email, diff --git a/src/routers/vault/middlewares/legacy/account.js b/src/routers/vault/middlewares/legacy/account.js index dddabdb..fa42ca2 100644 --- a/src/routers/vault/middlewares/legacy/account.js +++ b/src/routers/vault/middlewares/legacy/account.js @@ -1,5 +1,4 @@ "use strict"; -const uuidv4 = require("uuid/v4"); const md5saltcrypt = require("../../utils/md5saltcrypt.js"); const flatfile = require("../../utils/flatfile.js"); @@ -17,8 +16,16 @@ const get_account_list = async (req, vault_id) => { where: {vaultId: vault_id}, }); - for (let acc of claimed) { - acc = await req.app.locals.legacy.login.findByPk(acc.accountId); + for (const acc_ of claimed) { + const acc = await req.app.locals.legacy.login.findByPk(acc_.accountId); + + if (acc === null || acc === undefined) { + // unexpected: account was deleted + console.info(`Vault.legacy.account: unlinking deleted account ${acc_.accountId} {${vault_id}} [${req.ip}]`); + await acc_.destroy(); // un-claim the account + continue; + } + const chars = []; const chars_ = await req.app.locals.legacy.char.findAll({ where: {accountId: acc.accountId}, @@ -150,7 +157,17 @@ const claim_by_password = async (req, res, next) => { status: "error", error: "not found", }); - req.app.locals.cooldown(req, 1e3); + + // max 5 attempts per 15 minutes + if (req.app.locals.brute.consume(req, 5, 9e5)) { + // some attempts left + console.warn(`Vault.legacy.account: failed to log in to Legacy account {${session.vault}} [${req.ip}]`); + req.app.locals.cooldown(req, 3e3); + } else { + // no attempts left: big cooldown + req.app.locals.logger.warn(`Vault.legacy.account: login request flood {${session.vault}} [${req.ip}]`); + req.app.locals.cooldown(req, 3.6e6); + } return; } @@ -169,9 +186,17 @@ const claim_by_password = async (req, res, next) => { status: "error", error: "not found", }); - console.warn(`Vault.legacy.account: failed to log in to Legacy account {${session.vault}} [${req.ip}]`); - req.app.locals.cooldown(req, 3e5); - // TODO: huge cooldown after 8 attempts + + // max 5 attempts per 15 minutes + if (req.app.locals.brute.consume(req, 5, 9e5)) { + // some attempts left + console.warn(`Vault.legacy.account: failed to log in to Legacy account {${session.vault}} [${req.ip}]`); + req.app.locals.cooldown(req, 3e3); + } else { + // no attempts left: big cooldown + req.app.locals.logger.warn(`Vault.legacy.account: login request flood {${session.vault}} [${req.ip}]`); + req.app.locals.cooldown(req, 3.6e6); + } return; } } diff --git a/src/routers/vault/middlewares/session.js b/src/routers/vault/middlewares/session.js index b12a535..0073e90 100644 --- a/src/routers/vault/middlewares/session.js +++ b/src/routers/vault/middlewares/session.js @@ -229,13 +229,27 @@ const new_session = async (req, res, next) => { res.status(200).json({ status: "success" }); - req.app.locals.cooldown(req, 6e4); + + // max 5 attempts per 15 minutes + if (req.app.locals.brute.consume(req, 5, 9e5)) { + req.app.locals.cooldown(req, 6e4); + } else { + req.app.locals.logger.warn(`Vault.session: account creation request flood [${req.ip}]`); + req.app.locals.cooldown(req, 3.6e6); + } return; } else { res.status(202).json({ status: "pending", }); - req.app.locals.cooldown(req, 1e3); + + // max 5 attempts per 15 minutes + if (req.app.locals.brute.consume(req, 5, 9e5)) { + req.app.locals.cooldown(req, 1e3); + } else { + req.app.locals.logger.warn(`Vault.session: email check flood [${req.ip}]`); + req.app.locals.cooldown(req, 3.6e6); + } return; } } else { -- cgit v1.2.3-60-g2f50