From 86be43f1c0143495abe003654a4e415a154b11d4 Mon Sep 17 00:00:00 2001 From: gumi Date: Fri, 6 Mar 2020 15:36:44 -0500 Subject: prevent uuid bruteforcing --- src/routers/vault/middlewares/account.js | 19 +++++++++++- src/routers/vault/middlewares/evol/account.js | 33 ++++++++++++++++++++ src/routers/vault/middlewares/identity.js | 15 +++++++-- src/routers/vault/middlewares/legacy/account.js | 33 ++++++++++++++++++++ src/routers/vault/middlewares/session.js | 41 ++++++++++++++++++++++--- src/routers/vault/models/vault/login.js | 5 +++ src/routers/vault/types/Session.js | 2 ++ 7 files changed, 140 insertions(+), 8 deletions(-) diff --git a/src/routers/vault/middlewares/account.js b/src/routers/vault/middlewares/account.js index 9360728..42a63a4 100644 --- a/src/routers/vault/middlewares/account.js +++ b/src/routers/vault/middlewares/account.js @@ -44,6 +44,7 @@ const get_data = async (req, res, next) => { // TODO: make this a method of Session primaryIdentity: session.primaryIdentity, allowNonPrimary: session.allowNonPrimary, + strictIPCheck: session.strictIPCheck, vaultId: session.vault, }, }); @@ -64,7 +65,7 @@ const update_account = async (req, res, next) => { } if (!req.body || !Reflect.has(req.body, "primary") || !Reflect.has(req.body, "allow") || - !Number.isInteger(req.body.primary)) { + !Reflect.has(req.body, "strict") || !Number.isInteger(req.body.primary)) { res.status(400).json({ status: "error", error: "invalid format", @@ -94,6 +95,17 @@ const update_account = async (req, res, next) => { return; } + if (session.strictIPCheck && session.ip !== req.ip) { + // the ip is not the same + res.status(401).json({ + status: "error", + error: "ip address mismatch", + }); + req.app.locals.logger.warn(`Vault.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + const update_fields = {}; if (session.primaryIdentity !== req.body.primary) { @@ -122,6 +134,10 @@ const update_account = async (req, res, next) => { // update allow non-primary update_fields.allowNonPrimary = !!req.body.allow; } + if (session.strictIPCheck !== !!req.body.strict) { + // update allow non-primary + update_fields.strictIPCheck = !!req.body.strict; + } // update SQL if (Object.keys(update_fields).length) { @@ -132,6 +148,7 @@ const update_account = async (req, res, next) => { // now update our cache session.allowNonPrimary = !!req.body.allow; + session.strictIPCheck = !!req.body.strict; session.primaryIdentity = +req.body.primary; for (const ident of session.identities) { diff --git a/src/routers/vault/middlewares/evol/account.js b/src/routers/vault/middlewares/evol/account.js index 50248b2..3a22158 100644 --- a/src/routers/vault/middlewares/evol/account.js +++ b/src/routers/vault/middlewares/evol/account.js @@ -42,6 +42,17 @@ const get_accounts = async (req, res, next) => { return; } + if (session.strictIPCheck && session.ip !== req.ip) { + // the ip is not the same + res.status(403).json({ + status: "error", + error: "ip address mismatch", + }); + req.app.locals.logger.warn(`Vault.evol.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + res.status(200).json({ status: "success", accounts: session.gameAccounts, @@ -96,6 +107,17 @@ const new_account = async (req, res, next) => { return; } + if (session.strictIPCheck && session.ip !== req.ip) { + // the ip is not the same + res.status(403).json({ + status: "error", + error: "ip address mismatch", + }); + req.app.locals.logger.warn(`Vault.evol.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + // this check is necessary because login.userid has no UNIQUE constraint const existing = await req.app.locals.evol.login.findOne({ where: {userid: req.body.username} @@ -190,6 +212,17 @@ const update_account = async (req, res, next) => { return; } + if (session.strictIPCheck && session.ip !== req.ip) { + // the ip is not the same + res.status(403).json({ + status: "error", + error: "ip address mismatch", + }); + req.app.locals.logger.warn(`Vault.evol.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + let account = null; for (const acc of session.gameAccounts) { if (acc.accountId === req.body.accountId) { diff --git a/src/routers/vault/middlewares/identity.js b/src/routers/vault/middlewares/identity.js index 8ee5b6b..158e5df 100644 --- a/src/routers/vault/middlewares/identity.js +++ b/src/routers/vault/middlewares/identity.js @@ -88,7 +88,14 @@ const add_identity = async (req, res, next) => { status: "error", error: "token has expired", }); - req.app.locals.cooldown(req, 15e3); + + // max 3 attempts per 15 minutes + if (req.app.locals.brute.consume(req, 3, 9e5)) { + req.app.locals.cooldown(req, 15e3); + } else { + req.app.locals.logger.warn(`Vault.identity: validation request flood [${req.ip}]`); + req.app.locals.cooldown(req, 3.6e6); + } return; } @@ -217,7 +224,11 @@ const add_identity = async (req, res, next) => { return; } - const uuid = uuidv4(); + let uuid; + do { // avoid collisions + uuid = uuidv4(); + } while (req.app.locals.session.get(uuid)); + req.app.locals.identity_pending.set(uuid, { ip: req.ip, vault: session.vault, diff --git a/src/routers/vault/middlewares/legacy/account.js b/src/routers/vault/middlewares/legacy/account.js index fb507de..29da5a6 100644 --- a/src/routers/vault/middlewares/legacy/account.js +++ b/src/routers/vault/middlewares/legacy/account.js @@ -48,6 +48,17 @@ const get_accounts = async (req, res, next) => { return; } + if (session.strictIPCheck && session.ip !== req.ip) { + // the ip is not the same + res.status(403).json({ + status: "error", + error: "ip address mismatch", + }); + req.app.locals.logger.warn(`Vault.legacy.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + res.status(200).json({ status: "success", accounts: session.legacyAccounts, @@ -101,6 +112,17 @@ const claim_by_password = async (req, res, next) => { return; } + if (session.strictIPCheck && session.ip !== req.ip) { + // the ip is not the same + res.status(403).json({ + status: "error", + error: "ip address mismatch", + }); + req.app.locals.logger.warn(`Vault.legacy.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + const legacy = await req.app.locals.legacy.login.findOne({ where: {userid: req.body.username} }); @@ -255,6 +277,17 @@ const migrate = async (req, res, next) => { return; } + if (session.strictIPCheck && session.ip !== req.ip) { + // the ip is not the same + res.status(403).json({ + status: "error", + error: "ip address mismatch", + }); + req.app.locals.logger.warn(`Vault.legacy.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + let legacy = null; // check if we own it diff --git a/src/routers/vault/middlewares/session.js b/src/routers/vault/middlewares/session.js index be5787a..4451080 100644 --- a/src/routers/vault/middlewares/session.js +++ b/src/routers/vault/middlewares/session.js @@ -74,8 +74,29 @@ const auth_session = async (req, res, next) => { identity: null, } }); - // don't log: this can get spammy - req.app.locals.cooldown(req, 1e3); + + // max 3 attempts per 15 minutes + if (req.app.locals.brute.consume(req, 3, 9e5)) { + req.app.locals.cooldown(req, 1e3); + } else { + req.app.locals.logger.warn(`Vault.session: authentication request flood [${req.ip}]`); + req.app.locals.cooldown(req, 3.6e6); + } + return; + } + + if (session.strictIPCheck && session.ip !== req.ip) { + // not the same ip + res.status(403).json({ + status: "error", + error: "ip address mismatch", + session: { + expires: 0, + identity: null, + } + }); + + req.app.locals.cooldown(req, 5e3); return; } @@ -122,6 +143,7 @@ const auth_session = async (req, res, next) => { session.identity = ident.id; session.primaryIdentity = ident.id; session.allowNonPrimary = user.allowNonPrimary; + session.strictIPCheck = user.strictIPCheck; session.identities = [{ // TODO: make this a class! email: ident.email, @@ -215,7 +237,11 @@ const new_session = async (req, res, next) => { if (Reflect.has(req.body, "confirm") && req.body.confirm === true) { // account creation request - const uuid = uuidv4(); + let uuid; + do { // avoid collisions + uuid = uuidv4(); + } while (req.app.locals.session.get(uuid)); + const session = new Session(req.ip, req.body.email); req.app.locals.session.set(uuid, session); @@ -276,7 +302,7 @@ const new_session = async (req, res, next) => { // auth flow if (account.primaryIdentity === null || account.primaryIdentity === undefined) { // the vault account has no primary identity (bug): let's fix this - console.warn(`Vault.session: fixing account with no primary identity <${session.vault}@vault> [${req.ip}]`); + console.warn(`Vault.session: fixing account with no primary identity <${account.id}@vault> [${req.ip}]`); account.primaryIdentity = identity.id; await account.save(); } else if (identity.id !== account.primaryIdentity && !account.allowNonPrimary) { @@ -290,11 +316,16 @@ const new_session = async (req, res, next) => { // TODO: if account has WebAuthn do WebAuthn authentication flow - const uuid = uuidv4(); + let uuid; + do { // avoid collisions + uuid = uuidv4(); + } while (req.app.locals.session.get(uuid)); + const session = new Session(req.ip, req.body.email); session.vault = account.id; session.primaryIdentity = account.primaryIdentity; session.allowNonPrimary = account.allowNonPrimary; + session.strictIPCheck = account.strictIPCheck; session.identity = identity.id; req.app.locals.session.set(uuid, session); diff --git a/src/routers/vault/models/vault/login.js b/src/routers/vault/models/vault/login.js index 1c9c51e..262ed65 100644 --- a/src/routers/vault/models/vault/login.js +++ b/src/routers/vault/models/vault/login.js @@ -17,6 +17,11 @@ module.exports = { defaultValue: true, allowNull: false, }, + strictIPCheck: { + type: Sequelize.BOOLEAN, + defaultValue: true, + allowNull: false, + }, creationDate: { type: Sequelize.DATE, allowNull: false, diff --git a/src/routers/vault/types/Session.js b/src/routers/vault/types/Session.js index 1809cac..ff7e20d 100644 --- a/src/routers/vault/types/Session.js +++ b/src/routers/vault/types/Session.js @@ -24,6 +24,8 @@ module.exports = class Session { gameAccounts = []; /** ip that was used to init the session */ ip; + /** refuse to authenticate a session with a different IP */ + strictIPCheck = true; constructor (ip, email) { this.ip = ip; -- cgit v1.2.3-60-g2f50