Massively increase the aggression of the rate limiter

This commit is contained in:
zkldi
2021-11-22 05:24:42 +00:00
parent bcca7aff34
commit c758c5cfb4
4 changed files with 121 additions and 80 deletions
+19 -5
View File
@@ -1,8 +1,10 @@
import rateLimit from "express-rate-limit";
import { RedisClient } from "external/redis/redis";
import { ONE_MINUTE } from "lib/constants/time";
import CreateLogCtx from "lib/logger/logger";
import { Environment, ServerConfig, TachiConfig } from "lib/setup/config";
import RateLimitRedis from "rate-limit-redis";
import { integer } from "tachi-common";
const logger = CreateLogCtx(__filename);
@@ -13,12 +15,13 @@ const store =
export function ClearTestingRateLimitCache() {
// ???
RateLimitMiddleware.resetKey(`::ffff:127.0.0.1`);
NormalRateLimitMiddleware.resetKey(`::ffff:127.0.0.1`);
AggressiveRateLimitMiddleware.resetKey(`::ffff:127.0.0.1`);
HyperAggressiveRateLimitMiddleware.resetKey(`::ffff:127.0.0.1`);
}
// 100 requests / minute is the current cap
export const RateLimitMiddleware = rateLimit({
max: ServerConfig.RATE_LIMIT,
const CreateRateLimitOptions = (max: integer, windowMs?: number): rateLimit.Options => ({
max,
onLimitReached: (req) => {
logger.warn(`User ${req.ip} hit rate limit.`, {
url: req.url,
@@ -32,6 +35,17 @@ export const RateLimitMiddleware = rateLimit({
description: `You have exceeded ${ServerConfig.RATE_LIMIT} requests per minute. Please wait.`,
status: 429,
message: "You're being rate limited.",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
},
windowMs,
});
// 100 requests / minute is the current cap
export const NormalRateLimitMiddleware = rateLimit(CreateRateLimitOptions(ServerConfig.RATE_LIMIT));
// 15 requests every 10 minutes.
export const AggressiveRateLimitMiddleware = rateLimit(CreateRateLimitOptions(10, ONE_MINUTE * 10));
// 2 requests every 20 minutes.
export const HyperAggressiveRateLimitMiddleware = rateLimit(
CreateRateLimitOptions(2, ONE_MINUTE * 20)
);
+89 -69
View File
@@ -6,6 +6,10 @@ import CreateLogCtx from "lib/logger/logger";
import { Environment, ServerConfig } from "lib/setup/config";
import Prudence from "prudence";
import prValidate from "server/middleware/prudence-validate";
import {
AggressiveRateLimitMiddleware,
HyperAggressiveRateLimitMiddleware,
} from "server/middleware/rate-limiter";
import { integer } from "tachi-common";
import { DecrementCounterValue, GetNextCounterValue } from "utils/db";
import { Random20Hex } from "utils/misc";
@@ -40,6 +44,7 @@ const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u;
*/
router.post(
"/login",
AggressiveRateLimitMiddleware,
prValidate(
{
username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u),
@@ -159,6 +164,7 @@ router.post(
*/
router.post(
"/register",
AggressiveRateLimitMiddleware,
prValidate(
{
username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u),
@@ -324,6 +330,7 @@ router.post(
*/
router.post(
"/verify-email",
AggressiveRateLimitMiddleware,
prValidate({
code: "string",
}),
@@ -359,37 +366,42 @@ router.post(
*
* @name POST /api/v1/auth/resend-verify-email
*/
router.post("/resend-verify-email", prValidate({ email: "string" }), async (req, res) => {
// Immediately send a response so the existence of emails
// cannot be timing attacked out.
res.status(200).json({
success: true,
description: `Sent an email if the email address has not been verified.`,
body: {},
});
router.post(
"/resend-verify-email",
HyperAggressiveRateLimitMiddleware,
prValidate({ email: "string" }),
async (req, res) => {
// Immediately send a response so the existence of emails
// cannot be timing attacked out.
res.status(200).json({
success: true,
description: `Sent an email if the email address has not been verified.`,
body: {},
});
const verifyInfo = await db["verify-email-codes"].findOne({ email: req.body.email });
const verifyInfo = await db["verify-email-codes"].findOne({ email: req.body.email });
if (!verifyInfo) {
logger.warn(
`Attempted to send reset email to ${req.body.email}, but no verifyInfo was set for them.`
);
return;
if (!verifyInfo) {
logger.warn(
`Attempted to send reset email to ${req.body.email}, but no verifyInfo was set for them.`
);
return;
}
const user = await GetUserWithID(verifyInfo.userID);
if (!user) {
logger.severe(`Email verifyInfo belongs to user that no longer exists?`, verifyInfo);
return;
}
// Send the email again.
const { text, html } = EmailFormatVerifyEmail(user!.username, verifyInfo.code);
SendEmail(req.body.email, "Email Verification", html, text);
}
const user = await GetUserWithID(verifyInfo.userID);
if (!user) {
logger.severe(`Email verifyInfo belongs to user that no longer exists?`, verifyInfo);
return;
}
// Send the email again.
const { text, html } = EmailFormatVerifyEmail(user!.username, verifyInfo.code);
SendEmail(req.body.email, "Email Verification", html, text);
});
);
/**
* Logs out the requesting user.
@@ -420,53 +432,60 @@ router.post("/logout", (req, res) => {
*
* @name POST /api/v1/auth/forgot-password
*/
router.post("/forgot-password", prValidate({ email: "string" }), async (req, res) => {
if (!ServerConfig.EMAIL_CONFIG && Environment.nodeEnv !== "test") {
return res.status(501).json({
success: false,
description: `This server does not support password resets.`,
});
}
logger.debug(`Recieved password reset request for ${req.body.email}.`);
// For timing attack and infosec reasons, we can't do anything but **immediately** return here.
res.status(202).json({
success: true,
description: "A code has been sent to your email.",
body: {},
});
const userPrivateInfo = await db["user-private-information"].findOne({ email: req.body.email });
if (userPrivateInfo) {
const user = await db.users.findOne({ id: userPrivateInfo.userID });
if (!user) {
logger.severe(
`User ${userPrivateInfo.userID} has private information but no real account.`
);
return;
router.post(
"/forgot-password",
HyperAggressiveRateLimitMiddleware,
prValidate({ email: "string" }),
async (req, res) => {
if (!ServerConfig.EMAIL_CONFIG && Environment.nodeEnv !== "test") {
return res.status(501).json({
success: false,
description: `This server does not support password resets.`,
});
}
const code = `M${Random20Hex()}`;
logger.verbose(`Created password reset code for ${FormatUserDoc(user)}.`);
await db["password-reset-codes"].insert({
code,
userID: user.id,
createdOn: Date.now(),
logger.debug(`Recieved password reset request for ${req.body.email}.`);
// For timing attack and infosec reasons, we can't do anything but **immediately** return here.
res.status(202).json({
success: true,
description: "A code has been sent to your email.",
body: {},
});
const { html, text } = EmailFormatResetPassword(user.username, code, req.ip);
const userPrivateInfo = await db["user-private-information"].findOne({
email: req.body.email,
});
SendEmail(userPrivateInfo.email, "Reset Password", html, text);
} else {
logger.info(
`Silently rejected password reset request for ${req.body.email}, as no user has this email.`
);
if (userPrivateInfo) {
const user = await db.users.findOne({ id: userPrivateInfo.userID });
if (!user) {
logger.severe(
`User ${userPrivateInfo.userID} has private information but no real account.`
);
return;
}
const code = `M${Random20Hex()}`;
logger.verbose(`Created password reset code for ${FormatUserDoc(user)}.`);
await db["password-reset-codes"].insert({
code,
userID: user.id,
createdOn: Date.now(),
});
const { html, text } = EmailFormatResetPassword(user.username, code, req.ip);
SendEmail(userPrivateInfo.email, "Reset Password", html, text);
} else {
logger.info(
`Silently rejected password reset request for ${req.body.email}, as no user has this email.`
);
}
}
});
);
/**
* Takes a code generated from /forgot-password, a new password,
@@ -479,6 +498,7 @@ router.post("/forgot-password", prValidate({ email: "string" }), async (req, res
*/
router.post(
"/reset-password",
AggressiveRateLimitMiddleware,
prValidate({
code: "string",
password: ValidatePassword,
+11 -1
View File
@@ -1,4 +1,8 @@
import { Router } from "express";
import {
AggressiveRateLimitMiddleware,
NormalRateLimitMiddleware,
} from "server/middleware/rate-limiter";
import adminRouter from "./admin/router";
import authRouter from "./auth/router";
import clientsRouter from "./clients/router";
@@ -15,8 +19,14 @@ import usersRouter from "./users/router";
const router: Router = Router({ mergeParams: true });
router.use("/admin", adminRouter);
// Auth is up here so it can have special rate limiting rules,
// since it needs slightly harsher ones!
router.use("/auth", authRouter);
// Everything else can use the normal rate limiter!
router.use(NormalRateLimitMiddleware);
router.use("/admin", adminRouter);
router.use("/status", statusRouter);
router.use("/import", importRouter);
router.use("/imports", importsRouter);
+2 -5
View File
@@ -1,16 +1,13 @@
import { Router } from "express";
import { UpdateLastSeen } from "server/middleware/update-last-seen";
import { SetRequestPermissions } from "../middleware/auth";
import { RateLimitMiddleware } from "../middleware/rate-limiter";
import { NormalRateLimitMiddleware } from "../middleware/rate-limiter";
import apiRouterV1 from "./api/v1/router";
import irRouter from "./ir/router";
const router: Router = Router({ mergeParams: true });
// Add APIAuth and RateLimiting
router.use(RateLimitMiddleware);
router.use("/ir", irRouter);
router.use("/ir", NormalRateLimitMiddleware, irRouter);
// request perms only apply to the api, IR may reuse this
// but also may require custom authentication.