From 8b08dda9345483fa94352f7e27f633e05379d130 Mon Sep 17 00:00:00 2001
From: Julian Krauser <jkrauser209@gmail.com>
Date: Fri, 7 Feb 2025 17:27:45 +0100
Subject: [PATCH] change: Api Security and Rate Limiting

---
 package-lock.json                     | 122 ++++++++++++++++++++++++++
 package.json                          |   5 ++
 src/middleware/allowSetup.ts          |   4 +-
 src/middleware/authenticate.ts        |   4 +-
 src/middleware/authenticateAPI.ts     |   4 +-
 src/middleware/detectPWA.ts           |   4 +-
 src/middleware/errorHandler.ts        |   4 +-
 src/middleware/preventWebApiAccess.ts |   4 +-
 src/routes/index.ts                   |  41 +++++++--
 9 files changed, 173 insertions(+), 19 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index de7a150..c5037ad 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,11 +12,15 @@
         "cors": "^2.8.5",
         "dotenv": "^16.4.5",
         "express": "^5.0.0-beta.3",
+        "express-rate-limit": "^7.5.0",
+        "express-validator": "^7.2.1",
         "handlebars": "^4.7.8",
+        "helmet": "^8.0.0",
         "ics": "^3.8.1",
         "jsonwebtoken": "^9.0.2",
         "lodash.uniqby": "^4.7.0",
         "moment": "^2.30.1",
+        "morgan": "^1.10.0",
         "ms": "^2.1.3",
         "multer": "^1.4.5-lts.1",
         "mysql": "^2.18.1",
@@ -39,6 +43,7 @@
         "@types/express": "^4.17.17",
         "@types/jsonwebtoken": "^9.0.6",
         "@types/lodash.uniqby": "^4.7.9",
+        "@types/morgan": "^1.9.9",
         "@types/ms": "^0.7.34",
         "@types/multer": "^1.4.12",
         "@types/mysql": "^2.15.21",
@@ -563,6 +568,16 @@
       "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
       "dev": true
     },
+    "node_modules/@types/morgan": {
+      "version": "1.9.9",
+      "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz",
+      "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/ms": {
       "version": "0.7.34",
       "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
@@ -956,6 +971,24 @@
         "node": "^4.5.0 || >= 5.9"
       }
     },
+    "node_modules/basic-auth": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+      "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/basic-auth/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "license": "MIT"
+    },
     "node_modules/basic-ftp": {
       "version": "5.0.5",
       "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
@@ -2045,6 +2078,34 @@
         "node": ">= 4"
       }
     },
+    "node_modules/express-rate-limit": {
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+      "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": "^4.11 || 5 || ^5.0.0-beta.1"
+      }
+    },
+    "node_modules/express-validator": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
+      "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.17.21",
+        "validator": "~13.12.0"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      }
+    },
     "node_modules/express/node_modules/debug": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
@@ -2405,6 +2466,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/helmet": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz",
+      "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
     "node_modules/highlight.js": {
       "version": "10.7.3",
       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
@@ -2772,6 +2842,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
     "node_modules/lodash.includes": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -3248,6 +3324,34 @@
         "node": "*"
       }
     },
+    "node_modules/morgan": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+      "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "basic-auth": "~2.0.1",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-finished": "~2.3.0",
+        "on-headers": "~1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/morgan/node_modules/on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3531,6 +3635,15 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -5482,6 +5595,15 @@
       "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
       "devOptional": true
     },
+    "node_modules/validator": {
+      "version": "13.12.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+      "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
diff --git a/package.json b/package.json
index 996162f..f9316bb 100644
--- a/package.json
+++ b/package.json
@@ -27,11 +27,15 @@
     "cors": "^2.8.5",
     "dotenv": "^16.4.5",
     "express": "^5.0.0-beta.3",
+    "express-rate-limit": "^7.5.0",
+    "express-validator": "^7.2.1",
     "handlebars": "^4.7.8",
+    "helmet": "^8.0.0",
     "ics": "^3.8.1",
     "jsonwebtoken": "^9.0.2",
     "lodash.uniqby": "^4.7.0",
     "moment": "^2.30.1",
+    "morgan": "^1.10.0",
     "ms": "^2.1.3",
     "multer": "^1.4.5-lts.1",
     "mysql": "^2.18.1",
@@ -54,6 +58,7 @@
     "@types/express": "^4.17.17",
     "@types/jsonwebtoken": "^9.0.6",
     "@types/lodash.uniqby": "^4.7.9",
+    "@types/morgan": "^1.9.9",
     "@types/ms": "^0.7.34",
     "@types/multer": "^1.4.12",
     "@types/mysql": "^2.15.21",
diff --git a/src/middleware/allowSetup.ts b/src/middleware/allowSetup.ts
index 18844bd..981d955 100644
--- a/src/middleware/allowSetup.ts
+++ b/src/middleware/allowSetup.ts
@@ -1,8 +1,8 @@
-import { Request, Response } from "express";
+import { NextFunction, Request, Response } from "express";
 import UserService from "../service/user/userService";
 import CustomRequestException from "../exceptions/customRequestException";
 
-export default async function allowSetup(req: Request, res: Response, next: Function) {
+export default async function allowSetup(req: Request, res: Response, next: NextFunction) {
   let count = await UserService.count();
   if (count != 0) {
     throw new CustomRequestException(405, "service is already set up");
diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts
index 9e62bb4..abeb832 100644
--- a/src/middleware/authenticate.ts
+++ b/src/middleware/authenticate.ts
@@ -1,11 +1,11 @@
-import { Request, Response } from "express";
+import { NextFunction, Request, Response } from "express";
 import jwt from "jsonwebtoken";
 import BadRequestException from "../exceptions/badRequestException";
 import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
 import InternalException from "../exceptions/internalException";
 import { JWTHelper } from "../helpers/jwtHelper";
 
-export default async function authenticate(req: Request, res: Response, next: Function) {
+export default async function authenticate(req: Request, res: Response, next: NextFunction) {
   const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined;
 
   if (!bearer) {
diff --git a/src/middleware/authenticateAPI.ts b/src/middleware/authenticateAPI.ts
index b05060e..7b87474 100644
--- a/src/middleware/authenticateAPI.ts
+++ b/src/middleware/authenticateAPI.ts
@@ -1,11 +1,11 @@
-import { Request, Response } from "express";
+import { NextFunction, Request, Response } from "express";
 import jwt from "jsonwebtoken";
 import BadRequestException from "../exceptions/badRequestException";
 import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
 import InternalException from "../exceptions/internalException";
 import { JWTHelper } from "../helpers/jwtHelper";
 
-export default async function authenticateAPI(req: Request, res: Response, next: Function) {
+export default async function authenticateAPI(req: Request, res: Response, next: NextFunction) {
   const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined;
 
   if (!bearer) {
diff --git a/src/middleware/detectPWA.ts b/src/middleware/detectPWA.ts
index d30df95..0aa3b0f 100644
--- a/src/middleware/detectPWA.ts
+++ b/src/middleware/detectPWA.ts
@@ -1,6 +1,6 @@
-import { Request, Response } from "express";
+import { NextFunction, Request, Response } from "express";
 
-export default async function detectPWA(req: Request, res: Response, next: Function) {
+export default async function detectPWA(req: Request, res: Response, next: NextFunction) {
   const userAgent = req.headers["user-agent"] || "";
   if ((userAgent.includes("Mobile") && userAgent.includes("Standalone")) || req.headers["x-pwa-client"] === "true") {
     req.isPWA = true;
diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts
index 92e45ad..0ffe741 100644
--- a/src/middleware/errorHandler.ts
+++ b/src/middleware/errorHandler.ts
@@ -1,9 +1,9 @@
-import { Request, Response } from "express";
+import { NextFunction, Request, Response } from "express";
 import { ExceptionBase } from "../exceptions/exceptionsBaseType";
 import CustomRequestException from "../exceptions/customRequestException";
 import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
 
-export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: Function) {
+export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: NextFunction) {
   let status = 500;
   let msg = "Internal Server Error";
 
diff --git a/src/middleware/preventWebApiAccess.ts b/src/middleware/preventWebApiAccess.ts
index 3c3b7c6..534c2fd 100644
--- a/src/middleware/preventWebApiAccess.ts
+++ b/src/middleware/preventWebApiAccess.ts
@@ -1,7 +1,7 @@
-import { Request, Response } from "express";
+import { NextFunction, Request, Response } from "express";
 import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
 
-export default async function preventWebapiAccess(req: Request, res: Response, next: Function) {
+export default async function preventWebapiAccess(req: Request, res: Response, next: NextFunction) {
   if (req.isWebApiRequest) {
     throw new ForbiddenRequestException("This route cannot be accessed via webapi");
   } else {
diff --git a/src/routes/index.ts b/src/routes/index.ts
index eca2062..75b25d7 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -1,6 +1,9 @@
 import express from "express";
-import type { Express } from "express";
+import type { Express, NextFunction, Request, RequestHandler, Response } from "express";
 import cors from "cors";
+import helmet from "helmet";
+import morgan from "morgan";
+import rateLimit from "express-rate-limit";
 
 import allowSetup from "../middleware/allowSetup";
 import authenticate from "../middleware/authenticate";
@@ -20,25 +23,49 @@ import server from "./server";
 import PermissionHelper from "../helpers/permissionHelper";
 import preventWebapiAccess from "../middleware/preventWebApiAccess";
 
+const strictLimiter = rateLimit({
+  windowMs: 15 * 60 * 1000,
+  max: 10,
+  message: "Zu viele Anmeldeversuche innerhalb von 15 Minuten. Bitte warten.",
+});
+
+const generalLimiter = rateLimit({
+  windowMs: 60 * 1000,
+  max: 500,
+  message: "Zu viele Anfragen innerhalb von 1 Minute. Bitte warten.",
+});
+
+function excludePaths(middleware: RequestHandler, excludedPaths: Array<string>) {
+  return (req: Request, res: Response, next: NextFunction) => {
+    if (excludedPaths.includes(req.path)) {
+      return next();
+    }
+    return middleware(req, res, next);
+  };
+}
+
 export default (app: Express) => {
   app.set("query parser", "extended");
+  app.use(cors());
+  app.options("*", cors());
+  app.use(helmet());
+  app.use(morgan("short"));
   app.use(express.json());
   app.use(
     express.urlencoded({
       extended: true,
     })
   );
-  app.use(cors());
-  app.options("*", cors());
 
   app.use(detectPWA);
   app.use("/api/public", publicAvailable);
-  app.use("/api/setup", preventWebapiAccess, allowSetup, setup);
-  app.use("/api/reset", preventWebapiAccess, reset);
-  app.use("/api/invite", preventWebapiAccess, invite);
-  app.use("/api/auth", preventWebapiAccess, auth);
+  app.use("/api/setup", strictLimiter, preventWebapiAccess, allowSetup, setup);
+  app.use("/api/reset", strictLimiter, preventWebapiAccess, reset);
+  app.use("/api/invite", strictLimiter, preventWebapiAccess, invite);
+  app.use("/api/auth", strictLimiter, preventWebapiAccess, auth);
   app.use("/api/webapi", authenticateAPI, webapi);
   app.use(authenticate);
+  app.use(excludePaths(generalLimiter, ["/synchronize"]));
   app.use("/api/admin", admin);
   app.use("/api/user", preventWebapiAccess, user);
   app.use("/api/server", preventWebapiAccess, PermissionHelper.isAdminMiddleware(), server);