From 73a6aa44fd4e379ec87e2adfcbb7c33c8fc595af Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 25 Mar 2024 16:25:44 +0100 Subject: [PATCH 1/6] [Task] #50, create CSRF Validation for login form --- src/controller/login.ts | 18 ++++++++++------ src/middleware/limit.ts | 37 ++++++++++++++++---------------- src/scripts/crypt.ts | 2 -- src/scripts/token.ts | 47 +++++++++++++++++++++++++++++++++++++---- types.d.ts | 5 +++++ views/login-form.ejs | 6 ++---- 6 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/controller/login.ts b/src/controller/login.ts index 7c71236..2a553e9 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -1,29 +1,33 @@ import express, { Request, Response, NextFunction } from 'express'; import { create as createError } from '@src/middleware/error'; -import logger from '@src/scripts/logger'; import { crypt, compare } from '@src/scripts/crypt'; import { loginSlowDown, loginLimiter, baseSlowDown, baseRateLimiter } from '@src/middleware/limit'; -import { createToken } from '@src/scripts/token'; +import { createJWT, createCSRF, validateCSRF } from '@src/scripts/token'; + const router = express.Router(); -router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response) { - res.locals.text = "start"; - loginLimiter(req, res, () => { +router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response, next: NextFunction) { + loginLimiter(req, res, () => { + const csrfToken = createCSRF(res, next); + res.locals = {...res.locals, text: 'start', csrfToken: csrfToken}; res.render("login-form"); }); }); router.post("/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { - logger.log(req.body); loginLimiter(req, res, async () => { let validLogin = false; + const validCSRF = validateCSRF(req.body.csrfToken); const user = req.body.user; const password = req.body.password; let userFound = false; if (!user || !password) { return createError(res, 422, "Body does not contain all expected information", next); } + if (!validCSRF) { + return createError(res, 403, "Invalid CSRF Token", next); + } // Loop through all environment variables for (const key in process.env) { @@ -43,7 +47,7 @@ router.post("/", loginSlowDown, async function postLogin(req: Request, res: Resp } if (validLogin) { - const token = createToken(req, res); + const token = createJWT(req, res); res.json({ "token": token }); } else { if (!userFound) { diff --git a/src/middleware/limit.ts b/src/middleware/limit.ts index eb9aea5..1301071 100644 --- a/src/middleware/limit.ts +++ b/src/middleware/limit.ts @@ -3,6 +3,8 @@ import { rateLimit, Options as rateLimiterOptions } from 'express-rate-limit'; import { slowDown, Options as slowDownOptions } from 'express-slow-down'; import logger from '@src/scripts/logger'; +const ipsThatReachedLimit: RateLimit.obj = {}; // prevent logs from flooding + /* ** configurations */ @@ -33,30 +35,17 @@ const baseRateLimitOptions: Partial = { } -/* -** cleanup -*/ -const ipsThatReachedLimit: RateLimit.obj = {}; // prevent logs from flooding -setInterval(() => { - const oneHourAgo = Date.now() - 60 * 60 * 1000; - for (const ip in ipsThatReachedLimit) { - if (ipsThatReachedLimit[ip].time < oneHourAgo) { - delete ipsThatReachedLimit[ip]; - } - } -}, 60 * 60 * 1000); - /* ** exported section */ export const baseSlowDown = slowDown(baseSlowDownOptions); -export const loginSlowDown = slowDown({ - ...baseSlowDownOptions, - delayAfter: 1, // no delay for amount of attempts - delayMs: (used: number) => (used - 1) * 250, // Add delay after delayAfter is reached - }); +export const loginSlowDown = slowDown({ + ...baseSlowDownOptions, + delayAfter: 1, // no delay for amount of attempts + delayMs: (used: number) => (used - 1) * 250, // Add delay after delayAfter is reached +}); export const baseRateLimiter = rateLimit(baseRateLimitOptions); @@ -69,4 +58,14 @@ export const loginLimiter = rateLimit({ ...baseRateLimitOptions, limit: 3, message: 'Too many attempts without valid login', -}); \ No newline at end of file +}); + + +export function cleanup() { + const oneHourAgo = Date.now() - 60 * 60 * 1000; + for (const ip in ipsThatReachedLimit) { + if (ipsThatReachedLimit[ip].time < oneHourAgo) { + delete ipsThatReachedLimit[ip]; + } + } +} \ No newline at end of file diff --git a/src/scripts/crypt.ts b/src/scripts/crypt.ts index 1928c52..6bef0df 100644 --- a/src/scripts/crypt.ts +++ b/src/scripts/crypt.ts @@ -16,5 +16,3 @@ function pepper(password: string) { if (!key) { throw new Error('KEYA is not defined in the environment variables'); } return password + crypto.createHmac('sha256', key).digest("base64"); } - - diff --git a/src/scripts/token.ts b/src/scripts/token.ts index f26a718..7939668 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -1,9 +1,49 @@ import jwt from 'jsonwebtoken'; import logger from '@src/scripts/logger'; -import {Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; +import crypto from 'crypto'; +import { create as createError } from '@src/middleware/error'; -export function validateToken(req: Request) { +const csrfTokens: Set = new Set(); + +export function createCSRF(res: Response, next: NextFunction): string { + if (csrfTokens.size > 100) { // Max Number of Tokens in memory + res.set('Retry-After', '300'); // 5 minutes + createError(res, 503, "Too many tokens", next); + } + + const token = crypto.randomBytes(32).toString('hex'); + const expiry = Date.now() + (5 * 60 * 1000); // Token expires in 5 minutes + const csrfToken: CSRFToken = { token, expiry }; + csrfTokens.add(csrfToken); + + return token; +} + +export function validateCSRF(token: string): boolean { + const currentTime = Date.now(); + let valid: boolean = false; + for (const entry of csrfTokens) { + if (entry.token === token) { + valid = entry.expiry > currentTime; + csrfTokens.delete(entry); + } + } + + return valid; +} + +export function cleanupCSRF() { + const currentTime = Date.now(); + for (const entry of csrfTokens) { + if (entry.expiry < currentTime) { + csrfTokens.delete(entry); + } + } +} + +export function validateJWT(req: Request) { const key = process.env.KEYA; const header = req.header('Authorization'); const [type, token] = header ? header.split(' ') : ""; @@ -33,7 +73,7 @@ export function validateToken(req: Request) { return { success: true }; } -export function createToken(req: Request, res: Response) { +export function createJWT(req: Request, res: Response) { const key = process.env.KEYA; if (!key) { throw new Error('Configuration is wrong'); } const today = new Date(); @@ -44,6 +84,5 @@ export function createToken(req: Request, res: Response) { }; const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); res.locals.token = token; - logger.log(JSON.stringify(payload), true); return token; } diff --git a/types.d.ts b/types.d.ts index 6bb5b4c..3cde1ce 100644 --- a/types.d.ts +++ b/types.d.ts @@ -118,6 +118,11 @@ namespace Models { } } +interface CSRFToken { + token: string; + expiry: number; +} + interface HttpError extends Error { status?: number; statusCode?: number; diff --git a/views/login-form.ejs b/views/login-form.ejs index 802a7c9..34bf551 100644 --- a/views/login-form.ejs +++ b/views/login-form.ejs @@ -10,7 +10,7 @@ - From 1c1def93fa4150bca4e8734c162de45a8e895c03 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 25 Mar 2024 16:26:20 +0100 Subject: [PATCH 2/6] [Task] #43, added icon to repository for later use --- httpdocs/icon.png | Bin 0 -> 50481 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 httpdocs/icon.png diff --git a/httpdocs/icon.png b/httpdocs/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..827ec59bfce7546e32bc5aaa563e86ee6a924cc9 GIT binary patch literal 50481 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 VlY&WEpLqlPWF!?nSAH@K`aiw_K*0b2 literal 0 HcmV?d00001 From 7ee7d6bdb08bd1926bd48784fa2cdbd03ca930c4 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Mon, 25 Mar 2024 16:26:46 +0100 Subject: [PATCH 3/6] [Task] #50, cleanup cetntralized; rename token functions --- src/app.ts | 9 ++++++++- src/middleware/logged-in.ts | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app.ts b/src/app.ts index d7e1084..ff7a87e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,7 +12,8 @@ import readRouter from '@src/controller/read'; import loginRouter from '@src/controller/login'; import path from 'path'; import logger from '@src/scripts/logger'; -import { baseRateLimiter } from './middleware/limit'; +import { baseRateLimiter, cleanup as cleanupRateLimitedIps } from './middleware/limit'; +import { cleanupCSRF } from "@src/scripts/token"; // configurations config(); // dotenv @@ -75,6 +76,12 @@ const server = app.listen(80, () => { logger.log(`Server running //localhost:80, ENV: ${process.env.NODE_ENV}`, true); }); +// scheduled cleanup +setInterval(() => { + cleanupCSRF(); + cleanupRateLimitedIps(); +}, 1000 * 60 * 5); + // catching shutdowns ['SIGINT', 'SIGTERM', 'exit'].forEach((signal) => { process.on(signal, () => { diff --git a/src/middleware/logged-in.ts b/src/middleware/logged-in.ts index eddca04..f6546ba 100644 --- a/src/middleware/logged-in.ts +++ b/src/middleware/logged-in.ts @@ -1,10 +1,10 @@ import { Request, Response, NextFunction } from 'express'; -import { validateToken } from '@src/scripts/token'; +import { validateJWT } from '@src/scripts/token'; import { create as createError } from '@src/middleware/error'; export function isLoggedIn(req: Request, res: Response, next: NextFunction) { - const result = validateToken(req); + const result = validateJWT(req); if (!result.success) { createError(res, result.status, result.message || "", next) } else { From 356fe44ded2ad2f4dbd3a500ca546bcb8426772b Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 26 Mar 2024 00:58:27 +0100 Subject: [PATCH 4/6] [Task] #50, reduced token length and improved error handling --- src/controller/login.ts | 14 +++++--------- src/scripts/token.ts | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/controller/login.ts b/src/controller/login.ts index 2a553e9..6a47f4c 100644 --- a/src/controller/login.ts +++ b/src/controller/login.ts @@ -8,7 +8,7 @@ import { createJWT, createCSRF, validateCSRF } from '@src/scripts/token'; const router = express.Router(); router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response, next: NextFunction) { - loginLimiter(req, res, () => { + loginLimiter(req, res, () => { const csrfToken = createCSRF(res, next); res.locals = {...res.locals, text: 'start', csrfToken: csrfToken}; res.render("login-form"); @@ -18,16 +18,12 @@ router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request router.post("/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { loginLimiter(req, res, async () => { let validLogin = false; - const validCSRF = validateCSRF(req.body.csrfToken); + const token = req.body.csrfToken; const user = req.body.user; const password = req.body.password; let userFound = false; - if (!user || !password) { - return createError(res, 422, "Body does not contain all expected information", next); - } - if (!validCSRF) { - return createError(res, 403, "Invalid CSRF Token", next); - } + if (!user || !password) { return createError(res, 422, "Body does not contain all expected information", next); } + if (!token || !validateCSRF(req.body.csrfToken)) { return createError(res, 403, "Invalid CSRF Token", next); } // Loop through all environment variables for (const key in process.env) { @@ -53,7 +49,7 @@ router.post("/", loginSlowDown, async function postLogin(req: Request, res: Resp if (!userFound) { await crypt(password); // If no matching user is found, perform a dummy password comparison to prevent timing attacks } - return createError(res, 403, `invalid login credentials`, next); + return createError(res, 403, `Invalid credentials`, next); } }); }); diff --git a/src/scripts/token.ts b/src/scripts/token.ts index 7939668..ccb0fe1 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -1,5 +1,4 @@ import jwt from 'jsonwebtoken'; -import logger from '@src/scripts/logger'; import { NextFunction, Request, Response } from 'express'; import crypto from 'crypto'; import { create as createError } from '@src/middleware/error'; @@ -13,7 +12,7 @@ export function createCSRF(res: Response, next: NextFunction): string { createError(res, 503, "Too many tokens", next); } - const token = crypto.randomBytes(32).toString('hex'); + const token = crypto.randomBytes(16).toString('hex'); const expiry = Date.now() + (5 * 60 * 1000); // Token expires in 5 minutes const csrfToken: CSRFToken = { token, expiry }; csrfTokens.add(csrfToken); @@ -22,6 +21,7 @@ export function createCSRF(res: Response, next: NextFunction): string { } export function validateCSRF(token: string): boolean { + console.log(csrfTokens, token); const currentTime = Date.now(); let valid: boolean = false; for (const entry of csrfTokens) { From 283e8e54d3873b9862b06e4b263d0a7b4e94418f Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 26 Mar 2024 00:58:54 +0100 Subject: [PATCH 5/6] [Task] #50 csrf tests added to login --- src/app.ts | 4 ++-- src/tests/login.test.ts | 44 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index ff7a87e..b11cfdb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,8 +44,8 @@ app.use(compression()) app.use(hpp()); app.use(baseRateLimiter); app.use((req, res, next) => { // limit body for specific http methods - if(['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { - return express.urlencoded({ limit: '0.5kb', extended: true })(req, res, next); + if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { + return express.urlencoded({ limit: '0.5kb', extended: true })(req, res, next); } next(); }); diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index 6fbc340..957a73f 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -6,14 +6,21 @@ const userDataLarge = qs.stringify({ password: "pass", kilobyte: 'BPSwVu5vcvhWB17HcfIdyQK83mHJZKChv7zDihBJoifWK9EJFzK7VYf3kUgIqkc0io8DnSdewzc9U0GpzodQUFz0KLMaogsJruEbNSKvxnzUxS5UqSR64lLOmGumoPcn2InC0Ebpqfdiw90HFVZVlE3AY6Lhgbx8ILHi55RvpuGefDjBsePgow8Jh9sc8uVMCDglLmHQ0zk3PumMj0KlOszbMmX9fG0pPUsvLLc40biPBv9t97K3BFjYd3fGriRAQ3bFhGHBz2wzGbNQfHjKFDHuSvXOw8KReM7Wwd4Cl02QQ3RnDJVwH6cayh4BqFRXlP3i6uXw0l9qxdTv0q1CtV9rJho6zwo04gkGLvsS3AoYJQtHnOtUDdHPExu7l3nMKnPoRUwl7K2ePfHRuppFGqa43Q49bI04VjEhrB9k5S2uZJoxZdm63rIUrydmkZWdvBLVVZUIXwwIRnwLmoa26htKOz9FPKwWIPOM0NZj4jAoPhKqLDJwziNZn5UupzxBXoUM3BIyEk3K8GXs7eBduH9GCK2z2HPF0fJNtGiHASe7jCOC2mhSC5zGf9k0Yu1Ey63oQQZUtT7L57lp7UzPE2p6wzKDlbJZOn0Ho5OUfq3hE2C8fQRO1M6jDvRTiUIKhhxSHYd75Pvh4SG9lD8w5OHASusLDxmzKBUuG4GrGrQYpd0awJkqnKp5lk7psLD22YTtjTuDgI500tQLXSslxI1kIuB8RnN1LsxHyRQMVtXmNFOKKZV2U2frWpImIz2wSHCYrwRGygwDtiFfwtVwTapjhQqUMyb1vrWWi3EL1Y50fDCjDDHlvLI4N2tr2DULFf3a9m2SYWSoE6CYP4og5YyqjhqFQFm9urREInyZi9L0iQoMYxEqxTjGiVJfKmaSChSd0kQz6z2OdsxFbkMWJ2CAHOL1XNK8iFFSp93fIspaNMIonRVDCj4ZIP1LaPHDmIYcYTNU4k3Uz6VBHSIc1VjiG3sc2MZpKw9An0tJVlWbtVSk2RGYWIANAYyr5pQS' }); -const userData = qs.stringify({ +const userDataWithoutToken = qs.stringify({ user: "user", password: "pass" }); +let csrfToken = "-"; +const userDataWithToken = { + user: "user", + password: "pass", + csrfToken: "" +}; + describe('Login', () => { it('form available', async () => { - let serverStatus = {}; + let serverStatus; let response = { data: "", status: "" }; try { response = await axios.get('http://localhost:80/login'); @@ -24,6 +31,10 @@ describe('Login', () => { expect(serverStatus).toBe(200); expect(response.data).toContain(' { @@ -39,13 +50,38 @@ describe('Login', () => { } }) - it('invalid login verification test', async () => { + it('invalid csrf shows correct error', async () => { + try { + await axios.post('http://localhost:80/login', userDataWithoutToken); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + expect(axiosError.response.status).toBe(403); + if (axiosError.response.data) { + expect(JSON.stringify(axiosError.response.data)).toContain('Invalid CSRF'); + } else { + throw Error("fail"); + } + } else { + console.error(axiosError); + } + } + }) + + + it('test invalid credentials to return error', async () => { try { - await axios.post('http://localhost:80/login', userData); + userDataWithToken.csrfToken = csrfToken; + await axios.post('http://localhost:80/login', qs.stringify(userDataWithToken)); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { expect(axiosError.response.status).toBe(403); + if (axiosError.response.data) { + expect(JSON.stringify(axiosError.response.data)).toContain('Invalid credentials'); + } else { + throw Error("fail"); + } } else { console.error(axiosError); } From b5cebab7b4ed22c02bfa35c47d5f202bb89411f2 Mon Sep 17 00:00:00 2001 From: Type-Style Date: Tue, 26 Mar 2024 12:28:00 +0100 Subject: [PATCH 6/6] [Task] #50, added test case for csrf, repaired integration --- src/scripts/token.ts | 1 - src/tests/integration.test.ts | 21 ++++++++++++++++++--- src/tests/login.test.ts | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/scripts/token.ts b/src/scripts/token.ts index ccb0fe1..f9f4d7a 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -21,7 +21,6 @@ export function createCSRF(res: Response, next: NextFunction): string { } export function validateCSRF(token: string): boolean { - console.log(csrfTokens, token); const currentTime = Date.now(); let valid: boolean = false; for (const entry of csrfTokens) { diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 4fd5f4e..067d3ea 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -231,10 +231,25 @@ describe('API calls', () => { describe('read and login', () => { let token = ""; - const testData = qs.stringify({ + const testData = { user: "TEST", password: "test", - }); + csrfToken: "" + } + + it('form available / get Token', async () => { + let response = {data:""}; + try { + response = await axios.get('http://localhost:80/login'); + } catch (error) { + console.error(error); + } + + const regex = /name="csrfToken" value="([^"]*)"/; + const match = response.data.match(regex); + testData.csrfToken = match ? match[1] : '-'; + }) + test(`redirect without logged in`, async () => { try { await axios.get("http://localhost:80/read/"); @@ -249,7 +264,7 @@ describe('read and login', () => { }); it('test user can login', async () => { - const response = await axios.post('http://localhost:80/login', testData); + const response = await axios.post('http://localhost:80/login', qs.stringify(testData)); expect(response.status).toBe(200); expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json')); diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index 957a73f..e177826 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -20,7 +20,7 @@ const userDataWithToken = { describe('Login', () => { it('form available', async () => { - let serverStatus; + let serverStatus = {}; let response = { data: "", status: "" }; try { response = await axios.get('http://localhost:80/login'); @@ -71,7 +71,7 @@ describe('Login', () => { it('test invalid credentials to return error', async () => { try { - userDataWithToken.csrfToken = csrfToken; + userDataWithToken.csrfToken = csrfToken await axios.post('http://localhost:80/login', qs.stringify(userDataWithToken)); } catch (error) { const axiosError = error as AxiosError;