juice-shop
/
server.ts
733 строки · 34.5 Кб
1/*
2* Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
3* SPDX-License-Identifier: MIT
4*/
5import dataErasure from './routes/dataErasure'6import fs = require('fs')7import { type Request, type Response, type NextFunction } from 'express'8import { sequelize } from './models'9import { UserModel } from './models/user'10import { QuantityModel } from './models/quantity'11import { CardModel } from './models/card'12import { PrivacyRequestModel } from './models/privacyRequests'13import { AddressModel } from './models/address'14import { SecurityAnswerModel } from './models/securityAnswer'15import { SecurityQuestionModel } from './models/securityQuestion'16import { RecycleModel } from './models/recycle'17import { ComplaintModel } from './models/complaint'18import { ChallengeModel } from './models/challenge'19import { BasketItemModel } from './models/basketitem'20import { FeedbackModel } from './models/feedback'21import { ProductModel } from './models/product'22import { WalletModel } from './models/wallet'23import logger from './lib/logger'24import config from 'config'25import path from 'path'26import morgan from 'morgan'27import colors from 'colors/safe'28import * as utils from './lib/utils'29import * as Prometheus from 'prom-client'30import datacreator from './data/datacreator'31
32import validatePreconditions from './lib/startup/validatePreconditions'33import cleanupFtpFolder from './lib/startup/cleanupFtpFolder'34import validateConfig from './lib/startup/validateConfig'35import restoreOverwrittenFilesWithOriginals from './lib/startup/restoreOverwrittenFilesWithOriginals'36import registerWebsocketEvents from './lib/startup/registerWebsocketEvents'37import customizeApplication from './lib/startup/customizeApplication'38import customizeEasterEgg from './lib/startup/customizeEasterEgg' // vuln-code-snippet hide-line39
40import authenticatedUsers from './routes/authenticatedUsers'41
42const startTime = Date.now()43const finale = require('finale-rest')44const express = require('express')45const compression = require('compression')46const helmet = require('helmet')47const featurePolicy = require('feature-policy')48const errorhandler = require('errorhandler')49const cookieParser = require('cookie-parser')50const serveIndex = require('serve-index')51const bodyParser = require('body-parser')52const cors = require('cors')53const securityTxt = require('express-security.txt')54const robots = require('express-robots-txt')55const yaml = require('js-yaml')56const swaggerUi = require('swagger-ui-express')57const RateLimit = require('express-rate-limit')58const ipfilter = require('express-ipfilter').IpFilter59const swaggerDocument = yaml.load(fs.readFileSync('./swagger.yml', 'utf8'))60const {61ensureFileIsPassed,62handleZipFileUpload,63checkUploadSize,64checkFileType,65handleXmlUpload
66} = require('./routes/fileUpload')67const profileImageFileUpload = require('./routes/profileImageFileUpload')68const profileImageUrlUpload = require('./routes/profileImageUrlUpload')69const redirect = require('./routes/redirect')70const vulnCodeSnippet = require('./routes/vulnCodeSnippet')71const vulnCodeFixes = require('./routes/vulnCodeFixes')72const angular = require('./routes/angular')73const easterEgg = require('./routes/easterEgg')74const premiumReward = require('./routes/premiumReward')75const privacyPolicyProof = require('./routes/privacyPolicyProof')76const appVersion = require('./routes/appVersion')77const repeatNotification = require('./routes/repeatNotification')78const continueCode = require('./routes/continueCode')79const restoreProgress = require('./routes/restoreProgress')80const fileServer = require('./routes/fileServer')81const quarantineServer = require('./routes/quarantineServer')82const keyServer = require('./routes/keyServer')83const logFileServer = require('./routes/logfileServer')84const metrics = require('./routes/metrics')85const currentUser = require('./routes/currentUser')86const login = require('./routes/login')87const changePassword = require('./routes/changePassword')88const resetPassword = require('./routes/resetPassword')89const securityQuestion = require('./routes/securityQuestion')90const search = require('./routes/search')91const coupon = require('./routes/coupon')92const basket = require('./routes/basket')93const order = require('./routes/order')94const verify = require('./routes/verify')95const recycles = require('./routes/recycles')96const b2bOrder = require('./routes/b2bOrder')97const showProductReviews = require('./routes/showProductReviews')98const createProductReviews = require('./routes/createProductReviews')99const checkKeys = require('./routes/checkKeys')100const nftMint = require('./routes/nftMint')101const web3Wallet = require('./routes/web3Wallet')102const updateProductReviews = require('./routes/updateProductReviews')103const likeProductReviews = require('./routes/likeProductReviews')104const security = require('./lib/insecurity')105const app = express()106const server = require('http').Server(app)107const appConfiguration = require('./routes/appConfiguration')108const captcha = require('./routes/captcha')109const trackOrder = require('./routes/trackOrder')110const countryMapping = require('./routes/countryMapping')111const basketItems = require('./routes/basketItems')112const saveLoginIp = require('./routes/saveLoginIp')113const userProfile = require('./routes/userProfile')114const updateUserProfile = require('./routes/updateUserProfile')115const videoHandler = require('./routes/videoHandler')116const twoFactorAuth = require('./routes/2fa')117const languageList = require('./routes/languages')118const imageCaptcha = require('./routes/imageCaptcha')119const dataExport = require('./routes/dataExport')120const address = require('./routes/address')121const payment = require('./routes/payment')122const wallet = require('./routes/wallet')123const orderHistory = require('./routes/orderHistory')124const delivery = require('./routes/delivery')125const deluxe = require('./routes/deluxe')126const memory = require('./routes/memory')127const chatbot = require('./routes/chatbot')128const locales = require('./data/static/locales.json')129const i18n = require('i18n')130const antiCheat = require('./lib/antiCheat')131
132const appName = config.get<string>('application.customMetricsPrefix')133const startupGauge = new Prometheus.Gauge({134name: `${appName}_startup_duration_seconds`,135help: `Duration ${appName} required to perform a certain task during startup`,136labelNames: ['task']137})138
139// Wraps the function and measures its (async) execution time
140const collectDurationPromise = (name: string, func: (...args: any) => Promise<any>) => {141return async (...args: any) => {142const end = startupGauge.startTimer({ task: name })143try {144const res = await func(...args)145end()146return res147} catch (err) {148console.error('Error in timed startup function: ' + name, err)149throw err150}151}152}
153
154/* Sets view engine to hbs */
155app.set('view engine', 'hbs')156
157void collectDurationPromise('validatePreconditions', validatePreconditions)()158void collectDurationPromise('cleanupFtpFolder', cleanupFtpFolder)()159void collectDurationPromise('validateConfig', validateConfig)({})160
161// Function called first to ensure that all the i18n files are reloaded successfully before other linked operations.
162restoreOverwrittenFilesWithOriginals().then(() => {163/* Locals */164app.locals.captchaId = 0165app.locals.captchaReqId = 1166app.locals.captchaBypassReqTimes = []167app.locals.abused_ssti_bug = false168app.locals.abused_ssrf_bug = false169
170/* Compression for all requests */171app.use(compression())172
173/* Bludgeon solution for possible CORS problems: Allow everything! */174app.options('*', cors())175app.use(cors())176
177/* Security middleware */178app.use(helmet.noSniff())179app.use(helmet.frameguard())180// app.use(helmet.xssFilter()); // = no protection from persisted XSS via RESTful API181app.disable('x-powered-by')182app.use(featurePolicy({183features: {184payment: ["'self'"]185}186}))187
188/* Hiring header */189app.use((req: Request, res: Response, next: NextFunction) => {190res.append('X-Recruiting', config.get('application.securityTxt.hiring'))191next()192})193
194/* Remove duplicate slashes from URL which allowed bypassing subsequent filters */195app.use((req: Request, res: Response, next: NextFunction) => {196req.url = req.url.replace(/[/]+/g, '/')197next()198})199
200/* Increase request counter metric for every request */201app.use(metrics.observeRequestMetricsMiddleware())202
203/* Security Policy */204const securityTxtExpiration = new Date()205securityTxtExpiration.setFullYear(securityTxtExpiration.getFullYear() + 1)206app.get(['/.well-known/security.txt', '/security.txt'], verify.accessControlChallenges())207app.use(['/.well-known/security.txt', '/security.txt'], securityTxt({208contact: config.get('application.securityTxt.contact'),209encryption: config.get('application.securityTxt.encryption'),210acknowledgements: config.get('application.securityTxt.acknowledgements'),211'Preferred-Languages': [...new Set(locales.map((locale: { key: string }) => locale.key.substr(0, 2)))].join(', '),212hiring: config.get('application.securityTxt.hiring'),213csaf: config.get<string>('server.baseUrl') + config.get<string>('application.securityTxt.csaf'),214expires: securityTxtExpiration.toUTCString()215}))216
217/* robots.txt */218app.use(robots({ UserAgent: '*', Disallow: '/ftp' }))219
220/* Check for any URLs having been called that would be expected for challenge solving without cheating */221app.use(antiCheat.checkForPreSolveInteractions())222
223/* Checks for challenges solved by retrieving a file implicitly or explicitly */224app.use('/assets/public/images/padding', verify.accessControlChallenges())225app.use('/assets/public/images/products', verify.accessControlChallenges())226app.use('/assets/public/images/uploads', verify.accessControlChallenges())227app.use('/assets/i18n', verify.accessControlChallenges())228
229/* Checks for challenges solved by abusing SSTi and SSRF bugs */230app.use('/solve/challenges/server-side', verify.serverSideChallenges())231
232/* Create middleware to change paths from the serve-index plugin from absolute to relative */233const serveIndexMiddleware = (req: Request, res: Response, next: NextFunction) => {234const origEnd = res.end235// @ts-expect-error FIXME assignment broken due to seemingly void return value236res.end = function () {237if (arguments.length) {238const reqPath = req.originalUrl.replace(/\?.*$/, '')239const currentFolder = reqPath.split('/').pop() as string240arguments[0] = arguments[0].replace(/a href="([^"]+?)"/gi, function (matchString: string, matchedUrl: string) {241let relativePath = path.relative(reqPath, matchedUrl)242if (relativePath === '') {243relativePath = currentFolder244} else if (!relativePath.startsWith('.') && currentFolder !== '') {245relativePath = currentFolder + '/' + relativePath246} else {247relativePath = relativePath.replace('..', '.')248}249return 'a href="' + relativePath + '"'250})251}252// @ts-expect-error FIXME passed argument has wrong type253origEnd.apply(this, arguments)254}255next()256}257
258// vuln-code-snippet start directoryListingChallenge accessLogDisclosureChallenge259/* /ftp directory browsing and file download */ // vuln-code-snippet neutral-line directoryListingChallenge260app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true })) // vuln-code-snippet vuln-line directoryListingChallenge261app.use('/ftp(?!/quarantine)/:file', fileServer()) // vuln-code-snippet vuln-line directoryListingChallenge262app.use('/ftp/quarantine/:file', quarantineServer()) // vuln-code-snippet neutral-line directoryListingChallenge263
264app.use('/.well-known', serveIndexMiddleware, serveIndex('.well-known', { icons: true, view: 'details' }))265app.use('/.well-known', express.static('.well-known'))266
267/* /encryptionkeys directory browsing */268app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))269app.use('/encryptionkeys/:file', keyServer())270
271/* /logs directory browsing */ // vuln-code-snippet neutral-line accessLogDisclosureChallenge272app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' })) // vuln-code-snippet vuln-line accessLogDisclosureChallenge273app.use('/support/logs', verify.accessControlChallenges()) // vuln-code-snippet hide-line274app.use('/support/logs/:file', logFileServer()) // vuln-code-snippet vuln-line accessLogDisclosureChallenge275
276/* Swagger documentation for B2B v2 endpoints */277app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))278
279app.use(express.static(path.resolve('frontend/dist/frontend')))280app.use(cookieParser('kekse'))281// vuln-code-snippet end directoryListingChallenge accessLogDisclosureChallenge282
283/* Configure and enable backend-side i18n */284i18n.configure({285locales: locales.map((locale: { key: string }) => locale.key),286directory: path.resolve('i18n'),287cookie: 'language',288defaultLocale: 'en',289autoReload: true290})291app.use(i18n.init)292
293app.use(bodyParser.urlencoded({ extended: true }))294/* File Upload */295app.post('/file-upload', uploadToMemory.single('file'), ensureFileIsPassed, metrics.observeFileUploadMetricsMiddleware(), handleZipFileUpload, checkUploadSize, checkFileType, handleXmlUpload)296app.post('/profile/image/file', uploadToMemory.single('file'), ensureFileIsPassed, metrics.observeFileUploadMetricsMiddleware(), profileImageFileUpload())297app.post('/profile/image/url', uploadToMemory.single('file'), profileImageUrlUpload())298app.post('/rest/memories', uploadToDisk.single('image'), ensureFileIsPassed, security.appendUserId(), metrics.observeFileUploadMetricsMiddleware(), memory.addMemory())299
300app.use(bodyParser.text({ type: '*/*' }))301app.use(function jsonParser (req: Request, res: Response, next: NextFunction) {302// @ts-expect-error FIXME intentionally saving original request in this property303req.rawBody = req.body304if (req.headers['content-type']?.includes('application/json')) {305if (!req.body) {306req.body = {}307}308if (req.body !== Object(req.body)) { // Expensive workaround for 500 errors during Frisby test run (see #640)309req.body = JSON.parse(req.body)310}311}312next()313})314
315/* HTTP request logging */316const accessLogStream = require('file-stream-rotator').getStream({317filename: path.resolve('logs/access.log'),318frequency: 'daily',319verbose: false,320max_logs: '2d'321})322app.use(morgan('combined', { stream: accessLogStream }))323
324// vuln-code-snippet start resetPasswordMortyChallenge325/* Rate limiting */326app.enable('trust proxy')327app.use('/rest/user/reset-password', new RateLimit({328windowMs: 5 * 60 * 1000,329max: 100,330keyGenerator ({ headers, ip }: { headers: any, ip: any }) { return headers['X-Forwarded-For'] ?? ip } // vuln-code-snippet vuln-line resetPasswordMortyChallenge331}))332// vuln-code-snippet end resetPasswordMortyChallenge333
334// vuln-code-snippet start changeProductChallenge335/** Authorization **/336/* Checks on JWT in Authorization header */ // vuln-code-snippet hide-line337app.use(verify.jwtChallenges()) // vuln-code-snippet hide-line338/* Baskets: Unauthorized users are not allowed to access baskets */339app.use('/rest/basket', security.isAuthorized(), security.appendUserId())340/* BasketItems: API only accessible for authenticated users */341app.use('/api/BasketItems', security.isAuthorized())342app.use('/api/BasketItems/:id', security.isAuthorized())343/* Feedbacks: GET allowed for feedback carousel, POST allowed in order to provide feedback without being logged in */344app.use('/api/Feedbacks/:id', security.isAuthorized())345/* Users: Only POST is allowed in order to register a new user */346app.get('/api/Users', security.isAuthorized())347app.route('/api/Users/:id')348.get(security.isAuthorized())349.put(security.denyAll())350.delete(security.denyAll())351/* Products: Only GET is allowed in order to view products */ // vuln-code-snippet neutral-line changeProductChallenge352app.post('/api/Products', security.isAuthorized()) // vuln-code-snippet neutral-line changeProductChallenge353// app.put('/api/Products/:id', security.isAuthorized()) // vuln-code-snippet vuln-line changeProductChallenge354app.delete('/api/Products/:id', security.denyAll())355/* Challenges: GET list of challenges allowed. Everything else forbidden entirely */356app.post('/api/Challenges', security.denyAll())357app.use('/api/Challenges/:id', security.denyAll())358/* Complaints: POST and GET allowed when logged in only */359app.get('/api/Complaints', security.isAuthorized())360app.post('/api/Complaints', security.isAuthorized())361app.use('/api/Complaints/:id', security.denyAll())362/* Recycles: POST and GET allowed when logged in only */363app.get('/api/Recycles', recycles.blockRecycleItems())364app.post('/api/Recycles', security.isAuthorized())365/* Challenge evaluation before finale takes over */366app.get('/api/Recycles/:id', recycles.getRecycleItem())367app.put('/api/Recycles/:id', security.denyAll())368app.delete('/api/Recycles/:id', security.denyAll())369/* SecurityQuestions: Only GET list of questions allowed. */370app.post('/api/SecurityQuestions', security.denyAll())371app.use('/api/SecurityQuestions/:id', security.denyAll())372/* SecurityAnswers: Only POST of answer allowed. */373app.get('/api/SecurityAnswers', security.denyAll())374app.use('/api/SecurityAnswers/:id', security.denyAll())375/* REST API */376app.use('/rest/user/authentication-details', security.isAuthorized())377app.use('/rest/basket/:id', security.isAuthorized())378app.use('/rest/basket/:id/order', security.isAuthorized())379/* Challenge evaluation before finale takes over */ // vuln-code-snippet hide-start380app.post('/api/Feedbacks', verify.forgedFeedbackChallenge())381/* Captcha verification before finale takes over */382app.post('/api/Feedbacks', captcha.verifyCaptcha())383/* Captcha Bypass challenge verification */384app.post('/api/Feedbacks', verify.captchaBypassChallenge())385/* User registration challenge verifications before finale takes over */386app.post('/api/Users', (req: Request, res: Response, next: NextFunction) => {387if (req.body.email !== undefined && req.body.password !== undefined && req.body.passwordRepeat !== undefined) {388if (req.body.email.length !== 0 && req.body.password.length !== 0) {389req.body.email = req.body.email.trim()390req.body.password = req.body.password.trim()391req.body.passwordRepeat = req.body.passwordRepeat.trim()392} else {393res.status(400).send(res.__('Invalid email/password cannot be empty'))394}395}396next()397})398app.post('/api/Users', verify.registerAdminChallenge())399app.post('/api/Users', verify.passwordRepeatChallenge()) // vuln-code-snippet hide-end400app.post('/api/Users', verify.emptyUserRegistration())401/* Unauthorized users are not allowed to access B2B API */402app.use('/b2b/v2', security.isAuthorized())403/* Check if the quantity is available in stock and limit per user not exceeded, then add item to basket */404app.put('/api/BasketItems/:id', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemUpdate())405app.post('/api/BasketItems', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemAddition(), basketItems.addBasketItem())406/* Accounting users are allowed to check and update quantities */407app.delete('/api/Quantitys/:id', security.denyAll())408app.post('/api/Quantitys', security.denyAll())409app.use('/api/Quantitys/:id', security.isAccounting(), ipfilter(['123.456.789'], { mode: 'allow' }))410/* Feedbacks: Do not allow changes of existing feedback */411app.put('/api/Feedbacks/:id', security.denyAll())412/* PrivacyRequests: Only allowed for authenticated users */413app.use('/api/PrivacyRequests', security.isAuthorized())414app.use('/api/PrivacyRequests/:id', security.isAuthorized())415/* PaymentMethodRequests: Only allowed for authenticated users */416app.post('/api/Cards', security.appendUserId())417app.get('/api/Cards', security.appendUserId(), payment.getPaymentMethods())418app.put('/api/Cards/:id', security.denyAll())419app.delete('/api/Cards/:id', security.appendUserId(), payment.delPaymentMethodById())420app.get('/api/Cards/:id', security.appendUserId(), payment.getPaymentMethodById())421/* PrivacyRequests: Only POST allowed for authenticated users */422app.post('/api/PrivacyRequests', security.isAuthorized())423app.get('/api/PrivacyRequests', security.denyAll())424app.use('/api/PrivacyRequests/:id', security.denyAll())425
426app.post('/api/Addresss', security.appendUserId())427app.get('/api/Addresss', security.appendUserId(), address.getAddress())428app.put('/api/Addresss/:id', security.appendUserId())429app.delete('/api/Addresss/:id', security.appendUserId(), address.delAddressById())430app.get('/api/Addresss/:id', security.appendUserId(), address.getAddressById())431app.get('/api/Deliverys', delivery.getDeliveryMethods())432app.get('/api/Deliverys/:id', delivery.getDeliveryMethod())433// vuln-code-snippet end changeProductChallenge434
435/* Verify the 2FA Token */436app.post('/rest/2fa/verify',437new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }),438twoFactorAuth.verify()439)440/* Check 2FA Status for the current User */441app.get('/rest/2fa/status', security.isAuthorized(), twoFactorAuth.status())442/* Enable 2FA for the current User */443app.post('/rest/2fa/setup',444new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }),445security.isAuthorized(),446twoFactorAuth.setup()447)448/* Disable 2FA Status for the current User */449app.post('/rest/2fa/disable',450new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }),451security.isAuthorized(),452twoFactorAuth.disable()453)454/* Verifying DB related challenges can be postponed until the next request for challenges is coming via finale */455app.use(verify.databaseRelatedChallenges())456
457// vuln-code-snippet start registerAdminChallenge458/* Generated API endpoints */459finale.initialize({ app, sequelize })460
461const autoModels = [462{ name: 'User', exclude: ['password', 'totpSecret'], model: UserModel },463{ name: 'Product', exclude: [], model: ProductModel },464{ name: 'Feedback', exclude: [], model: FeedbackModel },465{ name: 'BasketItem', exclude: [], model: BasketItemModel },466{ name: 'Challenge', exclude: [], model: ChallengeModel },467{ name: 'Complaint', exclude: [], model: ComplaintModel },468{ name: 'Recycle', exclude: [], model: RecycleModel },469{ name: 'SecurityQuestion', exclude: [], model: SecurityQuestionModel },470{ name: 'SecurityAnswer', exclude: [], model: SecurityAnswerModel },471{ name: 'Address', exclude: [], model: AddressModel },472{ name: 'PrivacyRequest', exclude: [], model: PrivacyRequestModel },473{ name: 'Card', exclude: [], model: CardModel },474{ name: 'Quantity', exclude: [], model: QuantityModel }475]476
477for (const { name, exclude, model } of autoModels) {478const resource = finale.resource({479model,480endpoints: [`/api/${name}s`, `/api/${name}s/:id`],481excludeAttributes: exclude,482pagination: false483})484
485// create a wallet when a new user is registered using API486if (name === 'User') { // vuln-code-snippet neutral-line registerAdminChallenge487resource.create.send.before((req: Request, res: Response, context: { instance: { id: any }, continue: any }) => { // vuln-code-snippet vuln-line registerAdminChallenge488WalletModel.create({ UserId: context.instance.id }).catch((err: unknown) => {489console.log(err)490})491return context.continue // vuln-code-snippet neutral-line registerAdminChallenge492}) // vuln-code-snippet neutral-line registerAdminChallenge493} // vuln-code-snippet neutral-line registerAdminChallenge494// vuln-code-snippet end registerAdminChallenge495
496// translate challenge descriptions and hints on-the-fly497if (name === 'Challenge') {498resource.list.fetch.after((req: Request, res: Response, context: { instance: string | any[], continue: any }) => {499for (let i = 0; i < context.instance.length; i++) {500let description = context.instance[i].description501if (utils.contains(description, '<em>(This challenge is <strong>')) {502const warning = description.substring(description.indexOf(' <em>(This challenge is <strong>'))503description = description.substring(0, description.indexOf(' <em>(This challenge is <strong>'))504context.instance[i].description = req.__(description) + req.__(warning)505} else {506context.instance[i].description = req.__(description)507}508if (context.instance[i].hint) {509context.instance[i].hint = req.__(context.instance[i].hint)510}511}512return context.continue513})514resource.read.send.before((req: Request, res: Response, context: { instance: { description: string, hint: string }, continue: any }) => {515context.instance.description = req.__(context.instance.description)516if (context.instance.hint) {517context.instance.hint = req.__(context.instance.hint)518}519return context.continue520})521}522
523// translate security questions on-the-fly524if (name === 'SecurityQuestion') {525resource.list.fetch.after((req: Request, res: Response, context: { instance: string | any[], continue: any }) => {526for (let i = 0; i < context.instance.length; i++) {527context.instance[i].question = req.__(context.instance[i].question)528}529return context.continue530})531resource.read.send.before((req: Request, res: Response, context: { instance: { question: string }, continue: any }) => {532context.instance.question = req.__(context.instance.question)533return context.continue534})535}536
537// translate product names and descriptions on-the-fly538if (name === 'Product') {539resource.list.fetch.after((req: Request, res: Response, context: { instance: any[], continue: any }) => {540for (let i = 0; i < context.instance.length; i++) {541context.instance[i].name = req.__(context.instance[i].name)542context.instance[i].description = req.__(context.instance[i].description)543}544return context.continue545})546resource.read.send.before((req: Request, res: Response, context: { instance: { name: string, description: string }, continue: any }) => {547context.instance.name = req.__(context.instance.name)548context.instance.description = req.__(context.instance.description)549return context.continue550})551}552
553// fix the api difference between finale (fka epilogue) and previously used sequlize-restful554resource.all.send.before((req: Request, res: Response, context: { instance: { status: string, data: any }, continue: any }) => {555context.instance = {556status: 'success',557data: context.instance558}559return context.continue560})561}562
563/* Custom Restful API */564app.post('/rest/user/login', login())565app.get('/rest/user/change-password', changePassword())566app.post('/rest/user/reset-password', resetPassword())567app.get('/rest/user/security-question', securityQuestion())568app.get('/rest/user/whoami', security.updateAuthenticatedUsers(), currentUser())569app.get('/rest/user/authentication-details', authenticatedUsers())570app.get('/rest/products/search', search())571app.get('/rest/basket/:id', basket())572app.post('/rest/basket/:id/checkout', order())573app.put('/rest/basket/:id/coupon/:coupon', coupon())574app.get('/rest/admin/application-version', appVersion())575app.get('/rest/admin/application-configuration', appConfiguration())576app.get('/rest/repeat-notification', repeatNotification())577app.get('/rest/continue-code', continueCode.continueCode())578app.get('/rest/continue-code-findIt', continueCode.continueCodeFindIt())579app.get('/rest/continue-code-fixIt', continueCode.continueCodeFixIt())580app.put('/rest/continue-code-findIt/apply/:continueCode', restoreProgress.restoreProgressFindIt())581app.put('/rest/continue-code-fixIt/apply/:continueCode', restoreProgress.restoreProgressFixIt())582app.put('/rest/continue-code/apply/:continueCode', restoreProgress.restoreProgress())583app.get('/rest/admin/application-version', appVersion())584app.get('/rest/captcha', captcha())585app.get('/rest/image-captcha', imageCaptcha())586app.get('/rest/track-order/:id', trackOrder())587app.get('/rest/country-mapping', countryMapping())588app.get('/rest/saveLoginIp', saveLoginIp())589app.post('/rest/user/data-export', security.appendUserId(), imageCaptcha.verifyCaptcha())590app.post('/rest/user/data-export', security.appendUserId(), dataExport())591app.get('/rest/languages', languageList())592app.get('/rest/order-history', orderHistory.orderHistory())593app.get('/rest/order-history/orders', security.isAccounting(), orderHistory.allOrders())594app.put('/rest/order-history/:id/delivery-status', security.isAccounting(), orderHistory.toggleDeliveryStatus())595app.get('/rest/wallet/balance', security.appendUserId(), wallet.getWalletBalance())596app.put('/rest/wallet/balance', security.appendUserId(), wallet.addWalletBalance())597app.get('/rest/deluxe-membership', deluxe.deluxeMembershipStatus())598app.post('/rest/deluxe-membership', security.appendUserId(), deluxe.upgradeToDeluxe())599app.get('/rest/memories', memory.getMemories())600app.get('/rest/chatbot/status', chatbot.status())601app.post('/rest/chatbot/respond', chatbot.process())602/* NoSQL API endpoints */603app.get('/rest/products/:id/reviews', showProductReviews())604app.put('/rest/products/:id/reviews', createProductReviews())605app.patch('/rest/products/reviews', security.isAuthorized(), updateProductReviews())606app.post('/rest/products/reviews', security.isAuthorized(), likeProductReviews())607
608/* Web3 API endpoints */609app.post('/rest/web3/submitKey', checkKeys.checkKeys())610app.get('/rest/web3/nftUnlocked', checkKeys.nftUnlocked())611app.get('/rest/web3/nftMintListen', nftMint.nftMintListener())612app.post('/rest/web3/walletNFTVerify', nftMint.walletNFTVerify())613app.post('/rest/web3/walletExploitAddress', web3Wallet.contractExploitListener())614
615/* B2B Order API */616app.post('/b2b/v2/orders', b2bOrder())617
618/* File Serving */619app.get('/the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg', easterEgg())620app.get('/this/page/is/hidden/behind/an/incredibly/high/paywall/that/could/only/be/unlocked/by/sending/1btc/to/us', premiumReward())621app.get('/we/may/also/instruct/you/to/refuse/all/reasonably/necessary/responsibility', privacyPolicyProof())622
623/* Route for dataerasure page */624app.use('/dataerasure', dataErasure)625
626/* Route for redirects */627app.get('/redirect', redirect())628
629/* Routes for promotion video page */630app.get('/promotion', videoHandler.promotionVideo())631app.get('/video', videoHandler.getVideo())632
633/* Routes for profile page */634app.get('/profile', security.updateAuthenticatedUsers(), userProfile())635app.post('/profile', updateUserProfile())636
637/* Route for vulnerable code snippets */638app.get('/snippets', vulnCodeSnippet.serveChallengesWithCodeSnippet())639app.get('/snippets/:challenge', vulnCodeSnippet.serveCodeSnippet())640app.post('/snippets/verdict', vulnCodeSnippet.checkVulnLines())641app.get('/snippets/fixes/:key', vulnCodeFixes.serveCodeFixes())642app.post('/snippets/fixes', vulnCodeFixes.checkCorrectFix())643
644app.use(angular())645
646/* Error Handling */647app.use(verify.errorHandlingChallenge())648app.use(errorhandler())649}).catch((err) => {650console.error(err)651})652
653const multer = require('multer')654const uploadToMemory = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } })655const mimeTypeMap: any = {656'image/png': 'png',657'image/jpeg': 'jpg',658'image/jpg': 'jpg'659}
660const uploadToDisk = multer({661storage: multer.diskStorage({662destination: (req: Request, file: any, cb: any) => {663const isValid = mimeTypeMap[file.mimetype]664let error: Error | null = new Error('Invalid mime type')665if (isValid) {666error = null667}668cb(error, path.resolve('frontend/dist/frontend/assets/public/images/uploads/'))669},670filename: (req: Request, file: any, cb: any) => {671const name = security.sanitizeFilename(file.originalname)672.toLowerCase()673.split(' ')674.join('-')675const ext = mimeTypeMap[file.mimetype]676cb(null, name + '-' + Date.now() + '.' + ext)677}678})679})680
681const expectedModels = ['Address', 'Basket', 'BasketItem', 'Captcha', 'Card', 'Challenge', 'Complaint', 'Delivery', 'Feedback', 'ImageCaptcha', 'Memory', 'PrivacyRequestModel', 'Product', 'Quantity', 'Recycle', 'SecurityAnswer', 'SecurityQuestion', 'User', 'Wallet']682while (!expectedModels.every(model => Object.keys(sequelize.models).includes(model))) {683logger.info(`Entity models ${colors.bold(Object.keys(sequelize.models).length.toString())} of ${colors.bold(expectedModels.length.toString())} are initialized (${colors.yellow('WAITING')})`)684}
685logger.info(`Entity models ${colors.bold(Object.keys(sequelize.models).length.toString())} of ${colors.bold(expectedModels.length.toString())} are initialized (${colors.green('OK')})`)686
687// vuln-code-snippet start exposedMetricsChallenge
688/* Serve metrics */
689let metricsUpdateLoop: any690const Metrics = metrics.observeMetrics() // vuln-code-snippet neutral-line exposedMetricsChallenge691app.get('/metrics', metrics.serveMetrics()) // vuln-code-snippet vuln-line exposedMetricsChallenge692errorhandler.title = `${config.get<string>('application.name')} (Express ${utils.version('express')})`693
694export async function start (readyCallback?: () => void) {695const datacreatorEnd = startupGauge.startTimer({ task: 'datacreator' })696await sequelize.sync({ force: true })697await datacreator()698datacreatorEnd()699const port = process.env.PORT ?? config.get('server.port')700process.env.BASE_PATH = process.env.BASE_PATH ?? config.get('server.basePath')701
702metricsUpdateLoop = Metrics.updateLoop() // vuln-code-snippet neutral-line exposedMetricsChallenge703
704server.listen(port, () => {705logger.info(colors.cyan(`Server listening on port ${colors.bold(`${port}`)}`))706startupGauge.set({ task: 'ready' }, (Date.now() - startTime) / 1000)707if (process.env.BASE_PATH !== '') {708logger.info(colors.cyan(`Server using proxy base path ${colors.bold(`${process.env.BASE_PATH}`)} for redirects`))709}710registerWebsocketEvents(server)711if (readyCallback) {712readyCallback()713}714})715
716void collectDurationPromise('customizeApplication', customizeApplication)() // vuln-code-snippet hide-line717void collectDurationPromise('customizeEasterEgg', customizeEasterEgg)() // vuln-code-snippet hide-line718}
719
720export function close (exitCode: number | undefined) {721if (server) {722clearInterval(metricsUpdateLoop)723server.close()724}725if (exitCode !== undefined) {726process.exit(exitCode)727}728}
729// vuln-code-snippet end exposedMetricsChallenge
730
731// stop server on sigint or sigterm signals
732process.on('SIGINT', () => { close(0) })733process.on('SIGTERM', () => { close(0) })734