diff --git a/frontend/src/hacking-instructor/helpers/helpers.ts b/frontend/src/hacking-instructor/helpers/helpers.ts index 9f208abc..c002ae1b 100644 --- a/frontend/src/hacking-instructor/helpers/helpers.ts +++ b/frontend/src/hacking-instructor/helpers/helpers.ts @@ -3,259 +3,298 @@ * SPDX-License-Identifier: MIT */ -import jwtDecode from 'jwt-decode' +import jwtDecode from "jwt-decode"; -let config +let config; const playbackDelays = { faster: 0.5, fast: 0.75, normal: 1.0, slow: 1.25, - slower: 1.5 -} + slower: 1.5, +}; -export async function sleep (timeInMs: number): Promise { +export async function sleep(timeInMs: number): Promise { await new Promise((resolve) => { - setTimeout(resolve, timeInMs) - }) + setTimeout(resolve, timeInMs); + }); } -export function waitForInputToHaveValue (inputSelector: string, value: string, options: any = { ignoreCase: true, replacement: [] }) { +export function waitForInputToHaveValue( + inputSelector: string, + value: string, + options: any = { ignoreCase: true, replacement: [] } +) { return async () => { - const inputElement: HTMLInputElement = document.querySelector( - inputSelector - ) + const inputElement: HTMLInputElement = + document.querySelector(inputSelector); if (options.replacement?.length === 2) { if (!config) { - const res = await fetch('/rest/admin/application-configuration') - const json = await res.json() - config = json.config + const res = await fetch("/rest/admin/application-configuration"); + const json = await res.json(); + // We must return only the allowed Fields and not the + // whole config object ! Find the allowed values? + if (json && json.config) { + config = { + allowedFields: json.config.allowedFields, + }; + } else { + throw new Error("Attack aborted"); + } } - const propertyChain = options.replacement[1].split('.') - let replacementValue = config + const propertyChain = options.replacement[1].split("."); + let replacementValue = config; for (const property of propertyChain) { - replacementValue = replacementValue[property] + if (replacementValue[property] === undefined) { + throw new Error("Attack aborted"); + } + replacementValue = replacementValue[property]; } - value = value.replace(options.replacement[0], replacementValue) + value = value.replace(options.replacement[0], replacementValue); } while (true) { - if (options.ignoreCase && inputElement.value.toLowerCase() === value.toLowerCase()) { - break + if ( + options.ignoreCase && + inputElement.value.toLowerCase() === value.toLowerCase() + ) { + break; } else if (!options.ignoreCase && inputElement.value === value) { - break + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForInputToNotHaveValue (inputSelector: string, value: string, options = { ignoreCase: true }) { +export function waitForInputToNotHaveValue( + inputSelector: string, + value: string, + options = { ignoreCase: true } +) { return async () => { - const inputElement: HTMLInputElement = document.querySelector( - inputSelector - ) + const inputElement: HTMLInputElement = + document.querySelector(inputSelector); while (true) { - if (options.ignoreCase && inputElement.value.toLowerCase() !== value.toLowerCase()) { - break + if ( + options.ignoreCase && + inputElement.value.toLowerCase() !== value.toLowerCase() + ) { + break; } else if (!options.ignoreCase && inputElement.value !== value) { - break + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForInputToNotHaveValueAndNotBeEmpty (inputSelector: string, value: string, options = { ignoreCase: true }) { +export function waitForInputToNotHaveValueAndNotBeEmpty( + inputSelector: string, + value: string, + options = { ignoreCase: true } +) { return async () => { - const inputElement: HTMLInputElement = document.querySelector( - inputSelector - ) + const inputElement: HTMLInputElement = + document.querySelector(inputSelector); while (true) { - if (inputElement.value !== '') { - if (options.ignoreCase && inputElement.value.toLowerCase() !== value.toLowerCase()) { - break + if (inputElement.value !== "") { + if ( + options.ignoreCase && + inputElement.value.toLowerCase() !== value.toLowerCase() + ) { + break; } else if (!options.ignoreCase && inputElement.value !== value) { - break + break; } } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForInputToNotBeEmpty (inputSelector: string) { +export function waitForInputToNotBeEmpty(inputSelector: string) { return async () => { - const inputElement: HTMLInputElement = document.querySelector( - inputSelector - ) + const inputElement: HTMLInputElement = + document.querySelector(inputSelector); while (true) { - if (inputElement.value && inputElement.value !== '') { - break + if (inputElement.value && inputElement.value !== "") { + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForElementToGetClicked (elementSelector: string) { +export function waitForElementToGetClicked(elementSelector: string) { return async () => { - const element = document.querySelector( - elementSelector - ) + const element = document.querySelector(elementSelector); if (!element) { - console.warn(`Could not find Element with selector "${elementSelector}"`) + console.warn(`Could not find Element with selector "${elementSelector}"`); } await new Promise((resolve) => { - element.addEventListener('click', () => { resolve() }) - }) - } + element.addEventListener("click", () => { + resolve(); + }); + }); + }; } -export function waitForElementsInnerHtmlToBe (elementSelector: string, value: string) { +export function waitForElementsInnerHtmlToBe( + elementSelector: string, + value: string +) { return async () => { while (true) { - const element = document.querySelector( - elementSelector - ) + const element = document.querySelector(elementSelector); if (element && element.innerHTML === value) { - break + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitInMs (timeInMs: number) { +export function waitInMs(timeInMs: number) { return async () => { if (!config) { - const res = await fetch('/rest/admin/application-configuration') - const json = await res.json() - config = json.config + const res = await fetch("/rest/admin/application-configuration"); + const json = await res.json(); + config = json.config; } - let delay = playbackDelays[config.hackingInstructor.hintPlaybackSpeed] - delay ??= 1.0 - await sleep(timeInMs * delay) - } + let delay = playbackDelays[config.hackingInstructor.hintPlaybackSpeed]; + delay ??= 1.0; + await sleep(timeInMs * delay); + }; } -export function waitForAngularRouteToBeVisited (route: string) { +export function waitForAngularRouteToBeVisited(route: string) { return async () => { while (true) { if (window.location.hash.startsWith(`#/${route}`)) { - break + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForLogIn () { +export function waitForLogIn() { return async () => { while (true) { - if (localStorage.getItem('token') !== null) { - break + if (localStorage.getItem("token") !== null) { + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForAdminLogIn () { +export function waitForAdminLogIn() { return async () => { while (true) { - let role: string = '' + let role: string = ""; try { - const token: string = localStorage.getItem('token') - const decodedToken = jwtDecode(token) - const payload = decodedToken as any - role = payload.data.role + const token: string = localStorage.getItem("token"); + const decodedToken = jwtDecode(token); + const payload = decodedToken as any; + role = payload.data.role; } catch { - console.log('Role from token could not be accessed.') + console.log("Role from token could not be accessed."); } - if (role === 'admin') { - break + if (role === "admin") { + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForLogOut () { +export function waitForLogOut() { return async () => { while (true) { - if (localStorage.getItem('token') === null) { - break + if (localStorage.getItem("token") === null) { + break; } - await sleep(100) + await sleep(100); } - } + }; } /** * see https://stackoverflow.com/questions/7798748/find-out-whether-chrome-console-is-open/48287643#48287643 * does detect when devtools are opened horizontally or vertically but not when undocked or open on page load */ -export function waitForDevTools () { - const initialInnerHeight = window.innerHeight - const initialInnerWidth = window.innerWidth +export function waitForDevTools() { + const initialInnerHeight = window.innerHeight; + const initialInnerWidth = window.innerWidth; return async () => { while (true) { - if (window.innerHeight !== initialInnerHeight || window.innerWidth !== initialInnerWidth) { - break + if ( + window.innerHeight !== initialInnerHeight || + window.innerWidth !== initialInnerWidth + ) { + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForSelectToHaveValue (selectSelector: string, value: string) { +export function waitForSelectToHaveValue( + selectSelector: string, + value: string +) { return async () => { - const selectElement: HTMLSelectElement = document.querySelector( - selectSelector - ) + const selectElement: HTMLSelectElement = + document.querySelector(selectSelector); while (true) { if (selectElement.options[selectElement.selectedIndex].value === value) { - break + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForSelectToNotHaveValue (selectSelector: string, value: string) { +export function waitForSelectToNotHaveValue( + selectSelector: string, + value: string +) { return async () => { - const selectElement: HTMLSelectElement = document.querySelector( - selectSelector - ) + const selectElement: HTMLSelectElement = + document.querySelector(selectSelector); while (true) { if (selectElement.options[selectElement.selectedIndex].value !== value) { - break + break; } - await sleep(100) + await sleep(100); } - } + }; } -export function waitForRightUriQueryParamPair (key: string, value: string) { +export function waitForRightUriQueryParamPair(key: string, value: string) { return async () => { while (true) { - const encodedValue: string = encodeURIComponent(value).replace(/%3A/g, ':') - const encodedKey: string = encodeURIComponent(key).replace(/%3A/g, ':') - const expectedHash: string = `#/track-result/new?${encodedKey}=${encodedValue}` + const encodedValue: string = encodeURIComponent(value).replace( + /%3A/g, + ":" + ); + const encodedKey: string = encodeURIComponent(key).replace(/%3A/g, ":"); + const expectedHash: string = `#/track-result/new?${encodedKey}=${encodedValue}`; if (window.location.hash === expectedHash) { - break + break; } - await sleep(100) + await sleep(100); } - } + }; } diff --git a/package-lock.json b/package-lock.json index 931c7f47..c3f6d88a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "juicy-chat-bot": "~0.8.0", "libxmljs": "^1.0.11", "marsdb": "^0.6.11", + "mathjs": "^14.0.1", "median": "^0.0.2", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", @@ -6403,6 +6404,19 @@ "dev": true, "license": "MIT" }, + "node_modules/complex.js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -7073,7 +7087,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true, "license": "MIT" }, "node_modules/decode-uri-component": { @@ -8226,6 +8239,12 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -10340,6 +10359,19 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz", + "integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -13469,6 +13501,12 @@ "node": "*" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, "node_modules/jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", @@ -16899,6 +16937,29 @@ "node": ">= 0.4" } }, + "node_modules/mathjs": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.0.1.tgz", + "integrity": "sha512-yyJgLwC6UXuve724np8tHRMYaTtb5UqiOGQkjwbSXgH8y1C/LcJ0pvdNDZLI2LT7r+iExh2Y5HwfAY+oZFtGIQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.25.7", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -21789,6 +21850,12 @@ "dev": true, "license": "MIT" }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, "node_modules/seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", @@ -23921,6 +23988,12 @@ "node": ">=0.10.0" } }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -24591,6 +24664,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/package.json b/package.json index c72537a6..cbd8e789 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "juicy-chat-bot": "~0.8.0", "libxmljs": "^1.0.11", "marsdb": "^0.6.11", + "mathjs": "^14.0.1", "median": "^0.0.2", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", diff --git a/routes/captcha.ts b/routes/captcha.ts index aa4a0286..84181d78 100644 --- a/routes/captcha.ts +++ b/routes/captcha.ts @@ -3,46 +3,58 @@ * SPDX-License-Identifier: MIT */ -import { type Request, type Response, type NextFunction } from 'express' -import { type Captcha } from '../data/types' -import { CaptchaModel } from '../models/captcha' +import { type Request, type Response, type NextFunction } from "express"; +import { type Captcha } from "../data/types"; +import { CaptchaModel } from "../models/captcha"; +import { evaluate } from "mathjs"; -function captchas () { +function captchas() { return async (req: Request, res: Response) => { - const captchaId = req.app.locals.captchaId++ - const operators = ['*', '+', '-'] + const captchaId = req.app.locals.captchaId++; + const operators = ["*", "+", "-"]; - const firstTerm = Math.floor((Math.random() * 10) + 1) - const secondTerm = Math.floor((Math.random() * 10) + 1) - const thirdTerm = Math.floor((Math.random() * 10) + 1) + const firstTerm = Math.floor(Math.random() * 10 + 1); + const secondTerm = Math.floor(Math.random() * 10 + 1); + const thirdTerm = Math.floor(Math.random() * 10 + 1); - const firstOperator = operators[Math.floor((Math.random() * 3))] - const secondOperator = operators[Math.floor((Math.random() * 3))] + const firstOperator = operators[Math.floor(Math.random() * 3)]; + const secondOperator = operators[Math.floor(Math.random() * 3)]; - const expression = firstTerm.toString() + firstOperator + secondTerm.toString() + secondOperator + thirdTerm.toString() - const answer = eval(expression).toString() // eslint-disable-line no-eval + const expression = + firstTerm.toString() + + firstOperator + + secondTerm.toString() + + secondOperator + + thirdTerm.toString(); + //never use eval function + const answer = evaluate(expression).toString(); // eslint-disable-line no-eval const captcha = { captchaId, captcha: expression, - answer - } - const captchaInstance = CaptchaModel.build(captcha) - await captchaInstance.save() - res.json(captcha) - } + answer, + }; + const captchaInstance = CaptchaModel.build(captcha); + await captchaInstance.save(); + res.json(captcha); + }; } -captchas.verifyCaptcha = () => (req: Request, res: Response, next: NextFunction) => { - CaptchaModel.findOne({ where: { captchaId: req.body.captchaId } }).then((captcha: Captcha | null) => { - if ((captcha != null) && req.body.captcha === captcha.answer) { - next() - } else { - res.status(401).send(res.__('Wrong answer to CAPTCHA. Please try again.')) - } - }).catch((error: Error) => { - next(error) - }) -} - -module.exports = captchas +captchas.verifyCaptcha = + () => (req: Request, res: Response, next: NextFunction) => { + CaptchaModel.findOne({ where: { captchaId: req.body.captchaId } }) + .then((captcha: Captcha | null) => { + if (captcha != null && req.body.captcha === captcha.answer) { + next(); + } else { + res + .status(401) + .send(res.__("Wrong answer to CAPTCHA. Please try again.")); + } + }) + .catch((error: Error) => { + next(error); + }); + }; + +module.exports = captchas; diff --git a/routes/login.ts b/routes/login.ts index f844def8..26359399 100644 --- a/routes/login.ts +++ b/routes/login.ts @@ -3,83 +3,157 @@ * SPDX-License-Identifier: MIT */ -import models = require('../models/index') -import { type Request, type Response, type NextFunction } from 'express' -import { type User } from '../data/types' -import { BasketModel } from '../models/basket' -import { UserModel } from '../models/user' -import challengeUtils = require('../lib/challengeUtils') -import config from 'config' -import { challenges } from '../data/datacache' +import models = require("../models/index"); +import { type Request, type Response, type NextFunction } from "express"; +import { type User } from "../data/types"; +import { BasketModel } from "../models/basket"; +import { UserModel } from "../models/user"; +import challengeUtils = require("../lib/challengeUtils"); +import config from "config"; +import { challenges } from "../data/datacache"; -import * as utils from '../lib/utils' -const security = require('../lib/insecurity') -const users = require('../data/datacache').users +import * as utils from "../lib/utils"; +const security = require("../lib/insecurity"); +const users = require("../data/datacache").users; // vuln-code-snippet start loginAdminChallenge loginBenderChallenge loginJimChallenge -module.exports = function login () { - function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) { - verifyPostLoginChallenges(user) // vuln-code-snippet hide-line +module.exports = function login() { + function afterLogin( + user: { data: User; bid: number }, + res: Response, + next: NextFunction + ) { + verifyPostLoginChallenges(user); // vuln-code-snippet hide-line BasketModel.findOrCreate({ where: { UserId: user.data.id } }) .then(([basket]: [BasketModel, boolean]) => { - const token = security.authorize(user) - user.bid = basket.id // keep track of original basket - security.authenticatedUsers.put(token, user) - res.json({ authentication: { token, bid: basket.id, umail: user.data.email } }) - }).catch((error: Error) => { - next(error) + const token = security.authorize(user); + user.bid = basket.id; // keep track of original basket + security.authenticatedUsers.put(token, user); + res.json({ + authentication: { token, bid: basket.id, umail: user.data.email }, + }); }) + .catch((error: Error) => { + next(error); + }); } return (req: Request, res: Response, next: NextFunction) => { - verifyPreLoginChallenges(req) // vuln-code-snippet hide-line - models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge - .then((authenticatedUser) => { // vuln-code-snippet neutral-line loginAdminChallenge loginBenderChallenge loginJimChallenge - const user = utils.queryResultToJson(authenticatedUser) - if (user.data?.id && user.data.totpSecret !== '') { + verifyPreLoginChallenges(req); // vuln-code-snippet hide-line + models.sequelize + .query( + `SELECT * FROM Users WHERE email = ? AND password = ? + AND deletedAt IS NULL`, + { + replacements: { + email: req.body.email || "", + password: security.hash(req.body.password || ""), + }, + model: UserModel, + plain: true, + } + ) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge + .then((authenticatedUser) => { + // vuln-code-snippet neutral-line loginAdminChallenge loginBenderChallenge loginJimChallenge + const user = utils.queryResultToJson(authenticatedUser); + if (user.data?.id && user.data.totpSecret !== "") { res.status(401).json({ - status: 'totp_token_required', + status: "totp_token_required", data: { tmpToken: security.authorize({ userId: user.data.id, - type: 'password_valid_needs_second_factor_token' - }) - } - }) + type: "password_valid_needs_second_factor_token", + }), + }, + }); } else if (user.data?.id) { // @ts-expect-error FIXME some properties missing in user - vuln-code-snippet hide-line - afterLogin(user, res, next) + afterLogin(user, res, next); } else { - res.status(401).send(res.__('Invalid email or password.')) + res.status(401).send(res.__("Invalid email or password.")); } - }).catch((error: Error) => { - next(error) }) - } + .catch((error: Error) => { + next(error); + }); + }; // vuln-code-snippet end loginAdminChallenge loginBenderChallenge loginJimChallenge - function verifyPreLoginChallenges (req: Request) { - challengeUtils.solveIf(challenges.weakPasswordChallenge, () => { return req.body.email === 'admin@' + config.get('application.domain') && req.body.password === 'admin123' }) - challengeUtils.solveIf(challenges.loginSupportChallenge, () => { return req.body.email === 'support@' + config.get('application.domain') && req.body.password === 'J6aVjTgOpRs@?5l!Zkq2AYnCE@RF$P' }) - challengeUtils.solveIf(challenges.loginRapperChallenge, () => { return req.body.email === 'mc.safesearch@' + config.get('application.domain') && req.body.password === 'Mr. N00dles' }) - challengeUtils.solveIf(challenges.loginAmyChallenge, () => { return req.body.email === 'amy@' + config.get('application.domain') && req.body.password === 'K1f.....................' }) - challengeUtils.solveIf(challenges.dlpPasswordSprayingChallenge, () => { return req.body.email === 'J12934@' + config.get('application.domain') && req.body.password === '0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB' }) - challengeUtils.solveIf(challenges.oauthUserPasswordChallenge, () => { return req.body.email === 'bjoern.kimminich@gmail.com' && req.body.password === 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' }) + function verifyPreLoginChallenges(req: Request) { + challengeUtils.solveIf(challenges.weakPasswordChallenge, () => { + return ( + req.body.email === + "admin@" + config.get("application.domain") && + req.body.password === "admin123" + ); + }); + challengeUtils.solveIf(challenges.loginSupportChallenge, () => { + return ( + req.body.email === + "support@" + config.get("application.domain") && + req.body.password === "J6aVjTgOpRs@?5l!Zkq2AYnCE@RF$P" + ); + }); + challengeUtils.solveIf(challenges.loginRapperChallenge, () => { + return ( + req.body.email === + "mc.safesearch@" + config.get("application.domain") && + req.body.password === "Mr. N00dles" + ); + }); + challengeUtils.solveIf(challenges.loginAmyChallenge, () => { + return ( + req.body.email === "amy@" + config.get("application.domain") && + req.body.password === "K1f....................." + ); + }); + challengeUtils.solveIf(challenges.dlpPasswordSprayingChallenge, () => { + return ( + req.body.email === + "J12934@" + config.get("application.domain") && + req.body.password === "0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB" + ); + }); + challengeUtils.solveIf(challenges.oauthUserPasswordChallenge, () => { + return ( + req.body.email === "bjoern.kimminich@gmail.com" && + req.body.password === "bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=" + ); + }); } - function verifyPostLoginChallenges (user: { data: User }) { - challengeUtils.solveIf(challenges.loginAdminChallenge, () => { return user.data.id === users.admin.id }) - challengeUtils.solveIf(challenges.loginJimChallenge, () => { return user.data.id === users.jim.id }) - challengeUtils.solveIf(challenges.loginBenderChallenge, () => { return user.data.id === users.bender.id }) - challengeUtils.solveIf(challenges.ghostLoginChallenge, () => { return user.data.id === users.chris.id }) - if (challengeUtils.notSolved(challenges.ephemeralAccountantChallenge) && user.data.email === 'acc0unt4nt@' + config.get('application.domain') && user.data.role === 'accounting') { - UserModel.count({ where: { email: 'acc0unt4nt@' + config.get('application.domain') } }).then((count: number) => { - if (count === 0) { - challengeUtils.solve(challenges.ephemeralAccountantChallenge) - } - }).catch(() => { - throw new Error('Unable to verify challenges! Try again') + function verifyPostLoginChallenges(user: { data: User }) { + challengeUtils.solveIf(challenges.loginAdminChallenge, () => { + return user.data.id === users.admin.id; + }); + challengeUtils.solveIf(challenges.loginJimChallenge, () => { + return user.data.id === users.jim.id; + }); + challengeUtils.solveIf(challenges.loginBenderChallenge, () => { + return user.data.id === users.bender.id; + }); + challengeUtils.solveIf(challenges.ghostLoginChallenge, () => { + return user.data.id === users.chris.id; + }); + if ( + challengeUtils.notSolved(challenges.ephemeralAccountantChallenge) && + user.data.email === + "acc0unt4nt@" + config.get("application.domain") && + user.data.role === "accounting" + ) { + UserModel.count({ + where: { + email: "acc0unt4nt@" + config.get("application.domain"), + }, }) + .then((count: number) => { + if (count === 0) { + challengeUtils.solve(challenges.ephemeralAccountantChallenge); + } + }) + .catch(() => { + throw new Error("Unable to verify challenges! Try again"); + }); } } -} +}; diff --git a/routes/search.ts b/routes/search.ts index 7a5d8fe8..f5b51920 100644 --- a/routes/search.ts +++ b/routes/search.ts @@ -3,72 +3,96 @@ * SPDX-License-Identifier: MIT */ -import * as models from '../models/index' -import { type Request, type Response, type NextFunction } from 'express' -import { UserModel } from '../models/user' -import { challenges } from '../data/datacache' +import * as models from "../models/index"; +import { type Request, type Response, type NextFunction } from "express"; +import { QueryTypes } from "sequelize"; +import { UserModel } from "../models/user"; +import { challenges } from "../data/datacache"; -import * as utils from '../lib/utils' -const challengeUtils = require('../lib/challengeUtils') +import * as utils from "../lib/utils"; +const challengeUtils = require("../lib/challengeUtils"); class ErrorWithParent extends Error { - parent: Error | undefined + parent: Error | undefined; } // vuln-code-snippet start unionSqlInjectionChallenge dbSchemaChallenge -module.exports = function searchProducts () { +module.exports = function searchProducts() { return (req: Request, res: Response, next: NextFunction) => { - let criteria: any = req.query.q === 'undefined' ? '' : req.query.q ?? '' - criteria = (criteria.length <= 200) ? criteria : criteria.substring(0, 200) - models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' OR description LIKE '%${criteria}%') AND deletedAt IS NULL) ORDER BY name`) // vuln-code-snippet vuln-line unionSqlInjectionChallenge dbSchemaChallenge + let criteria: any = req.query.q === "undefined" ? "" : req.query.q ?? ""; + criteria = criteria.length <= 200 ? criteria : criteria.substring(0, 200); + models.sequelize + .query( + `SELECT * FROM Products WHERE + ((name LIKE :criteria OR description LIKE :criteria) AND deletedAt IS NULL) + ORDER BY name`, + { + replacements: { criteria: `%${criteria}%` }, + type: QueryTypes.SELECT, + } + ) // vuln-code-snippet vuln-line unionSqlInjectionChallenge dbSchemaChallenge .then(([products]: any) => { - const dataString = JSON.stringify(products) - if (challengeUtils.notSolved(challenges.unionSqlInjectionChallenge)) { // vuln-code-snippet hide-start - let solved = true - UserModel.findAll().then(data => { - const users = utils.queryResultToJson(data) - if (users.data?.length) { - for (let i = 0; i < users.data.length; i++) { - solved = solved && utils.containsOrEscaped(dataString, users.data[i].email) && utils.contains(dataString, users.data[i].password) - if (!solved) { - break + const dataString = JSON.stringify(products); + if (challengeUtils.notSolved(challenges.unionSqlInjectionChallenge)) { + // vuln-code-snippet hide-start + let solved = true; + UserModel.findAll() + .then((data) => { + const users = utils.queryResultToJson(data); + if (users.data?.length) { + for (let i = 0; i < users.data.length; i++) { + solved = + solved && + utils.containsOrEscaped(dataString, users.data[i].email) && + utils.contains(dataString, users.data[i].password); + if (!solved) { + break; + } + } + if (solved) { + challengeUtils.solve(challenges.unionSqlInjectionChallenge); } } - if (solved) { - challengeUtils.solve(challenges.unionSqlInjectionChallenge) - } - } - }).catch((error: Error) => { - next(error) - }) + }) + .catch((error: Error) => { + next(error); + }); } if (challengeUtils.notSolved(challenges.dbSchemaChallenge)) { - let solved = true - void models.sequelize.query('SELECT sql FROM sqlite_master').then(([data]: any) => { - const tableDefinitions = utils.queryResultToJson(data) - if (tableDefinitions.data?.length) { - for (let i = 0; i < tableDefinitions.data.length; i++) { - if (tableDefinitions.data[i].sql) { - solved = solved && utils.containsOrEscaped(dataString, tableDefinitions.data[i].sql) - if (!solved) { - break + let solved = true; + void models.sequelize + .query("SELECT sql FROM sqlite_master") + .then(([data]: any) => { + const tableDefinitions = utils.queryResultToJson(data); + if (tableDefinitions.data?.length) { + for (let i = 0; i < tableDefinitions.data.length; i++) { + if (tableDefinitions.data[i].sql) { + solved = + solved && + utils.containsOrEscaped( + dataString, + tableDefinitions.data[i].sql + ); + if (!solved) { + break; + } } } + if (solved) { + challengeUtils.solve(challenges.dbSchemaChallenge); + } } - if (solved) { - challengeUtils.solve(challenges.dbSchemaChallenge) - } - } - }) + }); } // vuln-code-snippet hide-end for (let i = 0; i < products.length; i++) { - products[i].name = req.__(products[i].name) - products[i].description = req.__(products[i].description) + products[i].name = req.__(products[i].name); + products[i].description = req.__(products[i].description); } - res.json(utils.queryResultToJson(products)) - }).catch((error: ErrorWithParent) => { - next(error.parent) + res.json(utils.queryResultToJson(products)); }) - } -} + .catch((error: ErrorWithParent) => { + next(error.parent); + }); + }; +}; // vuln-code-snippet end unionSqlInjectionChallenge dbSchemaChallenge diff --git a/routes/userProfile.ts b/routes/userProfile.ts index a7b89b39..749a5c6a 100644 --- a/routes/userProfile.ts +++ b/routes/userProfile.ts @@ -3,76 +3,100 @@ * SPDX-License-Identifier: MIT */ -import fs = require('fs') -import { type Request, type Response, type NextFunction } from 'express' -import { challenges } from '../data/datacache' +import fs = require("fs"); +import { type Request, type Response, type NextFunction } from "express"; +import { challenges } from "../data/datacache"; -import { UserModel } from '../models/user' -import challengeUtils = require('../lib/challengeUtils') -import config from 'config' -import * as utils from '../lib/utils' -import { AllHtmlEntities as Entities } from 'html-entities' -const security = require('../lib/insecurity') -const pug = require('pug') -const themes = require('../views/themes/themes').themes -const entities = new Entities() +import { UserModel } from "../models/user"; +import challengeUtils = require("../lib/challengeUtils"); +import config from "config"; +import * as utils from "../lib/utils"; +import { AllHtmlEntities as Entities } from "html-entities"; +import { evaluate } from "mathjs"; +const security = require("../lib/insecurity"); +const pug = require("pug"); +const themes = require("../views/themes/themes").themes; +const entities = new Entities(); -module.exports = function getUserProfile () { +module.exports = function getUserProfile() { return (req: Request, res: Response, next: NextFunction) => { - fs.readFile('views/userProfile.pug', function (err, buf) { - if (err != null) throw err - const loggedInUser = security.authenticatedUsers.get(req.cookies.token) + fs.readFile("views/userProfile.pug", function (err, buf) { + if (err != null) throw err; + const loggedInUser = security.authenticatedUsers.get(req.cookies.token); if (loggedInUser) { - UserModel.findByPk(loggedInUser.data.id).then((user: UserModel | null) => { - let template = buf.toString() - let username = user?.username - if (username?.match(/#{(.*)}/) !== null && utils.isChallengeEnabled(challenges.usernameXssChallenge)) { - req.app.locals.abused_ssti_bug = true - const code = username?.substring(2, username.length - 1) - try { - if (!code) { - throw new Error('Username is null') + UserModel.findByPk(loggedInUser.data.id) + .then((user: UserModel | null) => { + let template = buf.toString(); + let username = user?.username; + if ( + username?.match(/#{(.*)}/) !== null && + utils.isChallengeEnabled(challenges.usernameXssChallenge) + ) { + req.app.locals.abused_ssti_bug = true; + const code = username?.substring(2, username.length - 1); + try { + if (!code) { + throw new Error("Username is null"); + } + username = evaluate(code); // eslint-disable-line no-eval + } catch (err) { + username = "\\" + username; } - username = eval(code) // eslint-disable-line no-eval - } catch (err) { - username = '\\' + username + } else { + username = "\\" + username; } - } else { - username = '\\' + username - } - const theme = themes[config.get('application.theme')] - if (username) { - template = template.replace(/_username_/g, username) - } - template = template.replace(/_emailHash_/g, security.hash(user?.email)) - template = template.replace(/_title_/g, entities.encode(config.get('application.name'))) - template = template.replace(/_favicon_/g, favicon()) - template = template.replace(/_bgColor_/g, theme.bgColor) - template = template.replace(/_textColor_/g, theme.textColor) - template = template.replace(/_navColor_/g, theme.navColor) - template = template.replace(/_primLight_/g, theme.primLight) - template = template.replace(/_primDark_/g, theme.primDark) - template = template.replace(/_logo_/g, utils.extractFilename(config.get('application.logo'))) - const fn = pug.compile(template) - const CSP = `img-src 'self' ${user?.profileImage}; script-src 'self' 'unsafe-eval' https://code.getmdl.io http://ajax.googleapis.com` - // @ts-expect-error FIXME type issue with string vs. undefined for username - challengeUtils.solveIf(challenges.usernameXssChallenge, () => { return user?.profileImage.match(/;[ ]*script-src(.)*'unsafe-inline'/g) !== null && utils.contains(username, '') }) + const theme = themes[config.get("application.theme")]; + if (username) { + template = template.replace(/_username_/g, username); + } + template = template.replace( + /_emailHash_/g, + security.hash(user?.email) + ); + template = template.replace( + /_title_/g, + entities.encode(config.get("application.name")) + ); + template = template.replace(/_favicon_/g, favicon()); + template = template.replace(/_bgColor_/g, theme.bgColor); + template = template.replace(/_textColor_/g, theme.textColor); + template = template.replace(/_navColor_/g, theme.navColor); + template = template.replace(/_primLight_/g, theme.primLight); + template = template.replace(/_primDark_/g, theme.primDark); + template = template.replace( + /_logo_/g, + utils.extractFilename(config.get("application.logo")) + ); + const fn = pug.compile(template); + const CSP = `img-src 'self' ${user?.profileImage}; script-src 'self' 'unsafe-eval' https://code.getmdl.io http://ajax.googleapis.com`; + // @ts-expect-error FIXME type issue with string vs. undefined for username + challengeUtils.solveIf(challenges.usernameXssChallenge, () => { + return ( + user?.profileImage.match( + /;[ ]*script-src(.)*'unsafe-inline'/g + ) !== null && + utils.contains(username, "") + ); + }); - res.set({ - 'Content-Security-Policy': CSP - }) + res.set({ + "Content-Security-Policy": CSP, + }); - res.send(fn(user)) - }).catch((error: Error) => { - next(error) - }) + res.send(fn(user)); + }) + .catch((error: Error) => { + next(error); + }); } else { - next(new Error('Blocked illegal activity by ' + req.socket.remoteAddress)) + next( + new Error("Blocked illegal activity by " + req.socket.remoteAddress) + ); } - }) - } + }); + }; - function favicon () { - return utils.extractFilename(config.get('application.favicon')) + function favicon() { + return utils.extractFilename(config.get("application.favicon")); } -} +};