summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgumi <git@gumi.ca>2020-03-03 23:02:36 -0500
committergumi <git@gumi.ca>2020-03-03 23:02:36 -0500
commit349053954d45e4625ab35e6b2383608e5132eba3 (patch)
tree1939eb58d8296bd43ce21e80708381c56e0aa120
parent2df2f8a3f9eafdf1a28ce458a874135d666d0cf9 (diff)
downloadapi-349053954d45e4625ab35e6b2383608e5132eba3.tar.gz
api-349053954d45e4625ab35e6b2383608e5132eba3.tar.bz2
api-349053954d45e4625ab35e6b2383608e5132eba3.tar.xz
api-349053954d45e4625ab35e6b2383608e5132eba3.zip
add rudimentary anti-bruteforcing
-rw-r--r--src/api.js2
-rw-r--r--src/brute.js18
-rw-r--r--src/routers/vault/middlewares/evol/account.js12
-rw-r--r--src/routers/vault/middlewares/identity.js1
-rw-r--r--src/routers/vault/middlewares/legacy/account.js39
-rw-r--r--src/routers/vault/middlewares/session.js18
6 files changed, 79 insertions, 11 deletions
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<route, Map<ip, counter>>
+
+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 {