chore: Rate limiter audit (#3965)

* chore: Rate limiter audit api/users

* Make requests required

* api/collections

* Remove checkRateLimit on FileOperation (now done at route level through rate limiter)

* auth rate limit

* Add metric logging when rate limit exceeded

* Refactor to shared configs

* test
This commit is contained in:
Tom Moor
2022-08-14 16:04:04 +01:00
committed by GitHub
parent 9338328a82
commit a326e0ee88
14 changed files with 367 additions and 282 deletions

View File

@@ -2,6 +2,7 @@ import crypto from "crypto";
import Router from "koa-router";
import { Op, WhereOptions } from "sequelize";
import { UserValidation } from "@shared/validations";
import { RateLimiterStrategy } from "@server/RateLimiter";
import userDemoter from "@server/commands/userDemoter";
import userDestroyer from "@server/commands/userDestroyer";
import userInviter from "@server/commands/userInviter";
@@ -13,6 +14,7 @@ import env from "@server/env";
import { ValidationError } from "@server/errors";
import logger from "@server/logging/Logger";
import auth from "@server/middlewares/authentication";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import { Event, User, Team } from "@server/models";
import { UserFlag, UserRole } from "@server/models/User";
import { can, authorize } from "@server/policies";
@@ -308,26 +310,31 @@ router.post("users.activate", auth(), async (ctx) => {
};
});
router.post("users.invite", auth(), async (ctx) => {
const { invites } = ctx.body;
assertArray(invites, "invites must be an array");
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
authorize(user, "inviteUser", team);
router.post(
"users.invite",
auth(),
rateLimiter(RateLimiterStrategy.TenPerHour),
async (ctx) => {
const { invites } = ctx.body;
assertArray(invites, "invites must be an array");
const { user } = ctx.state;
const team = await Team.findByPk(user.teamId);
authorize(user, "inviteUser", team);
const response = await userInviter({
user,
invites: invites.slice(0, UserValidation.maxInvitesPerRequest),
ip: ctx.request.ip,
});
const response = await userInviter({
user,
invites: invites.slice(0, UserValidation.maxInvitesPerRequest),
ip: ctx.request.ip,
});
ctx.body = {
data: {
sent: response.sent,
users: response.users.map((user) => presentUser(user)),
},
};
});
ctx.body = {
data: {
sent: response.sent,
users: response.users.map((user) => presentUser(user)),
},
};
}
);
router.post("users.resendInvite", auth(), async (ctx) => {
const { id } = ctx.body;
@@ -371,49 +378,59 @@ router.post("users.resendInvite", auth(), async (ctx) => {
};
});
router.post("users.requestDelete", auth(), async (ctx) => {
const { user } = ctx.state;
authorize(user, "delete", user);
router.post(
"users.requestDelete",
auth(),
rateLimiter(RateLimiterStrategy.FivePerHour),
async (ctx) => {
const { user } = ctx.state;
authorize(user, "delete", user);
if (emailEnabled) {
await ConfirmUserDeleteEmail.schedule({
to: user.email,
deleteConfirmationCode: user.deleteConfirmationCode,
if (emailEnabled) {
await ConfirmUserDeleteEmail.schedule({
to: user.email,
deleteConfirmationCode: user.deleteConfirmationCode,
});
}
ctx.body = {
success: true,
};
}
);
router.post(
"users.delete",
auth(),
rateLimiter(RateLimiterStrategy.FivePerHour),
async (ctx) => {
const { code = "" } = ctx.body;
const { user } = ctx.state;
authorize(user, "delete", user);
const deleteConfirmationCode = user.deleteConfirmationCode;
if (
emailEnabled &&
(code.length !== deleteConfirmationCode.length ||
!crypto.timingSafeEqual(
Buffer.from(code),
Buffer.from(deleteConfirmationCode)
))
) {
throw ValidationError("The confirmation code was incorrect");
}
await userDestroyer({
user,
actor: user,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
}
ctx.body = {
success: true,
};
});
router.post("users.delete", auth(), async (ctx) => {
const { code = "" } = ctx.body;
const { user } = ctx.state;
authorize(user, "delete", user);
const deleteConfirmationCode = user.deleteConfirmationCode;
if (
emailEnabled &&
(code.length !== deleteConfirmationCode.length ||
!crypto.timingSafeEqual(
Buffer.from(code),
Buffer.from(deleteConfirmationCode)
))
) {
throw ValidationError("The confirmation code was incorrect");
}
await userDestroyer({
user,
actor: user,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
});
);
export default router;