juice-shop

Форк
0
/
server.ts 
733 строки · 34.5 Кб
1
/*
2
 * Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
3
 * SPDX-License-Identifier: MIT
4
 */
5
import dataErasure from './routes/dataErasure'
6
import fs = require('fs')
7
import { type Request, type Response, type NextFunction } from 'express'
8
import { sequelize } from './models'
9
import { UserModel } from './models/user'
10
import { QuantityModel } from './models/quantity'
11
import { CardModel } from './models/card'
12
import { PrivacyRequestModel } from './models/privacyRequests'
13
import { AddressModel } from './models/address'
14
import { SecurityAnswerModel } from './models/securityAnswer'
15
import { SecurityQuestionModel } from './models/securityQuestion'
16
import { RecycleModel } from './models/recycle'
17
import { ComplaintModel } from './models/complaint'
18
import { ChallengeModel } from './models/challenge'
19
import { BasketItemModel } from './models/basketitem'
20
import { FeedbackModel } from './models/feedback'
21
import { ProductModel } from './models/product'
22
import { WalletModel } from './models/wallet'
23
import logger from './lib/logger'
24
import config from 'config'
25
import path from 'path'
26
import morgan from 'morgan'
27
import colors from 'colors/safe'
28
import * as utils from './lib/utils'
29
import * as Prometheus from 'prom-client'
30
import datacreator from './data/datacreator'
31

32
import validatePreconditions from './lib/startup/validatePreconditions'
33
import cleanupFtpFolder from './lib/startup/cleanupFtpFolder'
34
import validateConfig from './lib/startup/validateConfig'
35
import restoreOverwrittenFilesWithOriginals from './lib/startup/restoreOverwrittenFilesWithOriginals'
36
import registerWebsocketEvents from './lib/startup/registerWebsocketEvents'
37
import customizeApplication from './lib/startup/customizeApplication'
38
import customizeEasterEgg from './lib/startup/customizeEasterEgg' // vuln-code-snippet hide-line
39

40
import authenticatedUsers from './routes/authenticatedUsers'
41

42
const startTime = Date.now()
43
const finale = require('finale-rest')
44
const express = require('express')
45
const compression = require('compression')
46
const helmet = require('helmet')
47
const featurePolicy = require('feature-policy')
48
const errorhandler = require('errorhandler')
49
const cookieParser = require('cookie-parser')
50
const serveIndex = require('serve-index')
51
const bodyParser = require('body-parser')
52
const cors = require('cors')
53
const securityTxt = require('express-security.txt')
54
const robots = require('express-robots-txt')
55
const yaml = require('js-yaml')
56
const swaggerUi = require('swagger-ui-express')
57
const RateLimit = require('express-rate-limit')
58
const ipfilter = require('express-ipfilter').IpFilter
59
const swaggerDocument = yaml.load(fs.readFileSync('./swagger.yml', 'utf8'))
60
const {
61
  ensureFileIsPassed,
62
  handleZipFileUpload,
63
  checkUploadSize,
64
  checkFileType,
65
  handleXmlUpload
66
} = require('./routes/fileUpload')
67
const profileImageFileUpload = require('./routes/profileImageFileUpload')
68
const profileImageUrlUpload = require('./routes/profileImageUrlUpload')
69
const redirect = require('./routes/redirect')
70
const vulnCodeSnippet = require('./routes/vulnCodeSnippet')
71
const vulnCodeFixes = require('./routes/vulnCodeFixes')
72
const angular = require('./routes/angular')
73
const easterEgg = require('./routes/easterEgg')
74
const premiumReward = require('./routes/premiumReward')
75
const privacyPolicyProof = require('./routes/privacyPolicyProof')
76
const appVersion = require('./routes/appVersion')
77
const repeatNotification = require('./routes/repeatNotification')
78
const continueCode = require('./routes/continueCode')
79
const restoreProgress = require('./routes/restoreProgress')
80
const fileServer = require('./routes/fileServer')
81
const quarantineServer = require('./routes/quarantineServer')
82
const keyServer = require('./routes/keyServer')
83
const logFileServer = require('./routes/logfileServer')
84
const metrics = require('./routes/metrics')
85
const currentUser = require('./routes/currentUser')
86
const login = require('./routes/login')
87
const changePassword = require('./routes/changePassword')
88
const resetPassword = require('./routes/resetPassword')
89
const securityQuestion = require('./routes/securityQuestion')
90
const search = require('./routes/search')
91
const coupon = require('./routes/coupon')
92
const basket = require('./routes/basket')
93
const order = require('./routes/order')
94
const verify = require('./routes/verify')
95
const recycles = require('./routes/recycles')
96
const b2bOrder = require('./routes/b2bOrder')
97
const showProductReviews = require('./routes/showProductReviews')
98
const createProductReviews = require('./routes/createProductReviews')
99
const checkKeys = require('./routes/checkKeys')
100
const nftMint = require('./routes/nftMint')
101
const web3Wallet = require('./routes/web3Wallet')
102
const updateProductReviews = require('./routes/updateProductReviews')
103
const likeProductReviews = require('./routes/likeProductReviews')
104
const security = require('./lib/insecurity')
105
const app = express()
106
const server = require('http').Server(app)
107
const appConfiguration = require('./routes/appConfiguration')
108
const captcha = require('./routes/captcha')
109
const trackOrder = require('./routes/trackOrder')
110
const countryMapping = require('./routes/countryMapping')
111
const basketItems = require('./routes/basketItems')
112
const saveLoginIp = require('./routes/saveLoginIp')
113
const userProfile = require('./routes/userProfile')
114
const updateUserProfile = require('./routes/updateUserProfile')
115
const videoHandler = require('./routes/videoHandler')
116
const twoFactorAuth = require('./routes/2fa')
117
const languageList = require('./routes/languages')
118
const imageCaptcha = require('./routes/imageCaptcha')
119
const dataExport = require('./routes/dataExport')
120
const address = require('./routes/address')
121
const payment = require('./routes/payment')
122
const wallet = require('./routes/wallet')
123
const orderHistory = require('./routes/orderHistory')
124
const delivery = require('./routes/delivery')
125
const deluxe = require('./routes/deluxe')
126
const memory = require('./routes/memory')
127
const chatbot = require('./routes/chatbot')
128
const locales = require('./data/static/locales.json')
129
const i18n = require('i18n')
130
const antiCheat = require('./lib/antiCheat')
131

132
const appName = config.get<string>('application.customMetricsPrefix')
133
const startupGauge = new Prometheus.Gauge({
134
  name: `${appName}_startup_duration_seconds`,
135
  help: `Duration ${appName} required to perform a certain task during startup`,
136
  labelNames: ['task']
137
})
138

139
// Wraps the function and measures its (async) execution time
140
const collectDurationPromise = (name: string, func: (...args: any) => Promise<any>) => {
141
  return async (...args: any) => {
142
    const end = startupGauge.startTimer({ task: name })
143
    try {
144
      const res = await func(...args)
145
      end()
146
      return res
147
    } catch (err) {
148
      console.error('Error in timed startup function: ' + name, err)
149
      throw err
150
    }
151
  }
152
}
153

154
/* Sets view engine to hbs */
155
app.set('view engine', 'hbs')
156

157
void collectDurationPromise('validatePreconditions', validatePreconditions)()
158
void collectDurationPromise('cleanupFtpFolder', cleanupFtpFolder)()
159
void collectDurationPromise('validateConfig', validateConfig)({})
160

161
// Function called first to ensure that all the i18n files are reloaded successfully before other linked operations.
162
restoreOverwrittenFilesWithOriginals().then(() => {
163
  /* Locals */
164
  app.locals.captchaId = 0
165
  app.locals.captchaReqId = 1
166
  app.locals.captchaBypassReqTimes = []
167
  app.locals.abused_ssti_bug = false
168
  app.locals.abused_ssrf_bug = false
169

170
  /* Compression for all requests */
171
  app.use(compression())
172

173
  /* Bludgeon solution for possible CORS problems: Allow everything! */
174
  app.options('*', cors())
175
  app.use(cors())
176

177
  /* Security middleware */
178
  app.use(helmet.noSniff())
179
  app.use(helmet.frameguard())
180
  // app.use(helmet.xssFilter()); // = no protection from persisted XSS via RESTful API
181
  app.disable('x-powered-by')
182
  app.use(featurePolicy({
183
    features: {
184
      payment: ["'self'"]
185
    }
186
  }))
187

188
  /* Hiring header */
189
  app.use((req: Request, res: Response, next: NextFunction) => {
190
    res.append('X-Recruiting', config.get('application.securityTxt.hiring'))
191
    next()
192
  })
193

194
  /* Remove duplicate slashes from URL which allowed bypassing subsequent filters */
195
  app.use((req: Request, res: Response, next: NextFunction) => {
196
    req.url = req.url.replace(/[/]+/g, '/')
197
    next()
198
  })
199

200
  /* Increase request counter metric for every request */
201
  app.use(metrics.observeRequestMetricsMiddleware())
202

203
  /* Security Policy */
204
  const securityTxtExpiration = new Date()
205
  securityTxtExpiration.setFullYear(securityTxtExpiration.getFullYear() + 1)
206
  app.get(['/.well-known/security.txt', '/security.txt'], verify.accessControlChallenges())
207
  app.use(['/.well-known/security.txt', '/security.txt'], securityTxt({
208
    contact: config.get('application.securityTxt.contact'),
209
    encryption: config.get('application.securityTxt.encryption'),
210
    acknowledgements: config.get('application.securityTxt.acknowledgements'),
211
    'Preferred-Languages': [...new Set(locales.map((locale: { key: string }) => locale.key.substr(0, 2)))].join(', '),
212
    hiring: config.get('application.securityTxt.hiring'),
213
    csaf: config.get<string>('server.baseUrl') + config.get<string>('application.securityTxt.csaf'),
214
    expires: securityTxtExpiration.toUTCString()
215
  }))
216

217
  /* robots.txt */
218
  app.use(robots({ UserAgent: '*', Disallow: '/ftp' }))
219

220
  /* Check for any URLs having been called that would be expected for challenge solving without cheating */
221
  app.use(antiCheat.checkForPreSolveInteractions())
222

223
  /* Checks for challenges solved by retrieving a file implicitly or explicitly */
224
  app.use('/assets/public/images/padding', verify.accessControlChallenges())
225
  app.use('/assets/public/images/products', verify.accessControlChallenges())
226
  app.use('/assets/public/images/uploads', verify.accessControlChallenges())
227
  app.use('/assets/i18n', verify.accessControlChallenges())
228

229
  /* Checks for challenges solved by abusing SSTi and SSRF bugs */
230
  app.use('/solve/challenges/server-side', verify.serverSideChallenges())
231

232
  /* Create middleware to change paths from the serve-index plugin from absolute to relative */
233
  const serveIndexMiddleware = (req: Request, res: Response, next: NextFunction) => {
234
    const origEnd = res.end
235
    // @ts-expect-error FIXME assignment broken due to seemingly void return value
236
    res.end = function () {
237
      if (arguments.length) {
238
        const reqPath = req.originalUrl.replace(/\?.*$/, '')
239
        const currentFolder = reqPath.split('/').pop() as string
240
        arguments[0] = arguments[0].replace(/a href="([^"]+?)"/gi, function (matchString: string, matchedUrl: string) {
241
          let relativePath = path.relative(reqPath, matchedUrl)
242
          if (relativePath === '') {
243
            relativePath = currentFolder
244
          } else if (!relativePath.startsWith('.') && currentFolder !== '') {
245
            relativePath = currentFolder + '/' + relativePath
246
          } else {
247
            relativePath = relativePath.replace('..', '.')
248
          }
249
          return 'a href="' + relativePath + '"'
250
        })
251
      }
252
      // @ts-expect-error FIXME passed argument has wrong type
253
      origEnd.apply(this, arguments)
254
    }
255
    next()
256
  }
257

258
  // vuln-code-snippet start directoryListingChallenge accessLogDisclosureChallenge
259
  /* /ftp directory browsing and file download */ // vuln-code-snippet neutral-line directoryListingChallenge
260
  app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true })) // vuln-code-snippet vuln-line directoryListingChallenge
261
  app.use('/ftp(?!/quarantine)/:file', fileServer()) // vuln-code-snippet vuln-line directoryListingChallenge
262
  app.use('/ftp/quarantine/:file', quarantineServer()) // vuln-code-snippet neutral-line directoryListingChallenge
263

264
  app.use('/.well-known', serveIndexMiddleware, serveIndex('.well-known', { icons: true, view: 'details' }))
265
  app.use('/.well-known', express.static('.well-known'))
266

267
  /* /encryptionkeys directory browsing */
268
  app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
269
  app.use('/encryptionkeys/:file', keyServer())
270

271
  /* /logs directory browsing */ // vuln-code-snippet neutral-line accessLogDisclosureChallenge
272
  app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' })) // vuln-code-snippet vuln-line accessLogDisclosureChallenge
273
  app.use('/support/logs', verify.accessControlChallenges()) // vuln-code-snippet hide-line
274
  app.use('/support/logs/:file', logFileServer()) // vuln-code-snippet vuln-line accessLogDisclosureChallenge
275

276
  /* Swagger documentation for B2B v2 endpoints */
277
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
278

279
  app.use(express.static(path.resolve('frontend/dist/frontend')))
280
  app.use(cookieParser('kekse'))
281
  // vuln-code-snippet end directoryListingChallenge accessLogDisclosureChallenge
282

283
  /* Configure and enable backend-side i18n */
284
  i18n.configure({
285
    locales: locales.map((locale: { key: string }) => locale.key),
286
    directory: path.resolve('i18n'),
287
    cookie: 'language',
288
    defaultLocale: 'en',
289
    autoReload: true
290
  })
291
  app.use(i18n.init)
292

293
  app.use(bodyParser.urlencoded({ extended: true }))
294
  /* File Upload */
295
  app.post('/file-upload', uploadToMemory.single('file'), ensureFileIsPassed, metrics.observeFileUploadMetricsMiddleware(), handleZipFileUpload, checkUploadSize, checkFileType, handleXmlUpload)
296
  app.post('/profile/image/file', uploadToMemory.single('file'), ensureFileIsPassed, metrics.observeFileUploadMetricsMiddleware(), profileImageFileUpload())
297
  app.post('/profile/image/url', uploadToMemory.single('file'), profileImageUrlUpload())
298
  app.post('/rest/memories', uploadToDisk.single('image'), ensureFileIsPassed, security.appendUserId(), metrics.observeFileUploadMetricsMiddleware(), memory.addMemory())
299

300
  app.use(bodyParser.text({ type: '*/*' }))
301
  app.use(function jsonParser (req: Request, res: Response, next: NextFunction) {
302
    // @ts-expect-error FIXME intentionally saving original request in this property
303
    req.rawBody = req.body
304
    if (req.headers['content-type']?.includes('application/json')) {
305
      if (!req.body) {
306
        req.body = {}
307
      }
308
      if (req.body !== Object(req.body)) { // Expensive workaround for 500 errors during Frisby test run (see #640)
309
        req.body = JSON.parse(req.body)
310
      }
311
    }
312
    next()
313
  })
314

315
  /* HTTP request logging */
316
  const accessLogStream = require('file-stream-rotator').getStream({
317
    filename: path.resolve('logs/access.log'),
318
    frequency: 'daily',
319
    verbose: false,
320
    max_logs: '2d'
321
  })
322
  app.use(morgan('combined', { stream: accessLogStream }))
323

324
  // vuln-code-snippet start resetPasswordMortyChallenge
325
  /* Rate limiting */
326
  app.enable('trust proxy')
327
  app.use('/rest/user/reset-password', new RateLimit({
328
    windowMs: 5 * 60 * 1000,
329
    max: 100,
330
    keyGenerator ({ headers, ip }: { headers: any, ip: any }) { return headers['X-Forwarded-For'] ?? ip } // vuln-code-snippet vuln-line resetPasswordMortyChallenge
331
  }))
332
  // vuln-code-snippet end resetPasswordMortyChallenge
333

334
  // vuln-code-snippet start changeProductChallenge
335
  /** Authorization **/
336
  /* Checks on JWT in Authorization header */ // vuln-code-snippet hide-line
337
  app.use(verify.jwtChallenges()) // vuln-code-snippet hide-line
338
  /* Baskets: Unauthorized users are not allowed to access baskets */
339
  app.use('/rest/basket', security.isAuthorized(), security.appendUserId())
340
  /* BasketItems: API only accessible for authenticated users */
341
  app.use('/api/BasketItems', security.isAuthorized())
342
  app.use('/api/BasketItems/:id', security.isAuthorized())
343
  /* Feedbacks: GET allowed for feedback carousel, POST allowed in order to provide feedback without being logged in */
344
  app.use('/api/Feedbacks/:id', security.isAuthorized())
345
  /* Users: Only POST is allowed in order to register a new user */
346
  app.get('/api/Users', security.isAuthorized())
347
  app.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 changeProductChallenge
352
  app.post('/api/Products', security.isAuthorized()) // vuln-code-snippet neutral-line changeProductChallenge
353
  // app.put('/api/Products/:id', security.isAuthorized()) // vuln-code-snippet vuln-line changeProductChallenge
354
  app.delete('/api/Products/:id', security.denyAll())
355
  /* Challenges: GET list of challenges allowed. Everything else forbidden entirely */
356
  app.post('/api/Challenges', security.denyAll())
357
  app.use('/api/Challenges/:id', security.denyAll())
358
  /* Complaints: POST and GET allowed when logged in only */
359
  app.get('/api/Complaints', security.isAuthorized())
360
  app.post('/api/Complaints', security.isAuthorized())
361
  app.use('/api/Complaints/:id', security.denyAll())
362
  /* Recycles: POST and GET allowed when logged in only */
363
  app.get('/api/Recycles', recycles.blockRecycleItems())
364
  app.post('/api/Recycles', security.isAuthorized())
365
  /* Challenge evaluation before finale takes over */
366
  app.get('/api/Recycles/:id', recycles.getRecycleItem())
367
  app.put('/api/Recycles/:id', security.denyAll())
368
  app.delete('/api/Recycles/:id', security.denyAll())
369
  /* SecurityQuestions: Only GET list of questions allowed. */
370
  app.post('/api/SecurityQuestions', security.denyAll())
371
  app.use('/api/SecurityQuestions/:id', security.denyAll())
372
  /* SecurityAnswers: Only POST of answer allowed. */
373
  app.get('/api/SecurityAnswers', security.denyAll())
374
  app.use('/api/SecurityAnswers/:id', security.denyAll())
375
  /* REST API */
376
  app.use('/rest/user/authentication-details', security.isAuthorized())
377
  app.use('/rest/basket/:id', security.isAuthorized())
378
  app.use('/rest/basket/:id/order', security.isAuthorized())
379
  /* Challenge evaluation before finale takes over */ // vuln-code-snippet hide-start
380
  app.post('/api/Feedbacks', verify.forgedFeedbackChallenge())
381
  /* Captcha verification before finale takes over */
382
  app.post('/api/Feedbacks', captcha.verifyCaptcha())
383
  /* Captcha Bypass challenge verification */
384
  app.post('/api/Feedbacks', verify.captchaBypassChallenge())
385
  /* User registration challenge verifications before finale takes over */
386
  app.post('/api/Users', (req: Request, res: Response, next: NextFunction) => {
387
    if (req.body.email !== undefined && req.body.password !== undefined && req.body.passwordRepeat !== undefined) {
388
      if (req.body.email.length !== 0 && req.body.password.length !== 0) {
389
        req.body.email = req.body.email.trim()
390
        req.body.password = req.body.password.trim()
391
        req.body.passwordRepeat = req.body.passwordRepeat.trim()
392
      } else {
393
        res.status(400).send(res.__('Invalid email/password cannot be empty'))
394
      }
395
    }
396
    next()
397
  })
398
  app.post('/api/Users', verify.registerAdminChallenge())
399
  app.post('/api/Users', verify.passwordRepeatChallenge()) // vuln-code-snippet hide-end
400
  app.post('/api/Users', verify.emptyUserRegistration())
401
  /* Unauthorized users are not allowed to access B2B API */
402
  app.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 */
404
  app.put('/api/BasketItems/:id', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemUpdate())
405
  app.post('/api/BasketItems', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemAddition(), basketItems.addBasketItem())
406
  /* Accounting users are allowed to check and update quantities */
407
  app.delete('/api/Quantitys/:id', security.denyAll())
408
  app.post('/api/Quantitys', security.denyAll())
409
  app.use('/api/Quantitys/:id', security.isAccounting(), ipfilter(['123.456.789'], { mode: 'allow' }))
410
  /* Feedbacks: Do not allow changes of existing feedback */
411
  app.put('/api/Feedbacks/:id', security.denyAll())
412
  /* PrivacyRequests: Only allowed for authenticated users */
413
  app.use('/api/PrivacyRequests', security.isAuthorized())
414
  app.use('/api/PrivacyRequests/:id', security.isAuthorized())
415
  /* PaymentMethodRequests: Only allowed for authenticated users */
416
  app.post('/api/Cards', security.appendUserId())
417
  app.get('/api/Cards', security.appendUserId(), payment.getPaymentMethods())
418
  app.put('/api/Cards/:id', security.denyAll())
419
  app.delete('/api/Cards/:id', security.appendUserId(), payment.delPaymentMethodById())
420
  app.get('/api/Cards/:id', security.appendUserId(), payment.getPaymentMethodById())
421
  /* PrivacyRequests: Only POST allowed for authenticated users */
422
  app.post('/api/PrivacyRequests', security.isAuthorized())
423
  app.get('/api/PrivacyRequests', security.denyAll())
424
  app.use('/api/PrivacyRequests/:id', security.denyAll())
425

426
  app.post('/api/Addresss', security.appendUserId())
427
  app.get('/api/Addresss', security.appendUserId(), address.getAddress())
428
  app.put('/api/Addresss/:id', security.appendUserId())
429
  app.delete('/api/Addresss/:id', security.appendUserId(), address.delAddressById())
430
  app.get('/api/Addresss/:id', security.appendUserId(), address.getAddressById())
431
  app.get('/api/Deliverys', delivery.getDeliveryMethods())
432
  app.get('/api/Deliverys/:id', delivery.getDeliveryMethod())
433
  // vuln-code-snippet end changeProductChallenge
434

435
  /* Verify the 2FA Token */
436
  app.post('/rest/2fa/verify',
437
    new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }),
438
    twoFactorAuth.verify()
439
  )
440
  /* Check 2FA Status for the current User */
441
  app.get('/rest/2fa/status', security.isAuthorized(), twoFactorAuth.status())
442
  /* Enable 2FA for the current User */
443
  app.post('/rest/2fa/setup',
444
    new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }),
445
    security.isAuthorized(),
446
    twoFactorAuth.setup()
447
  )
448
  /* Disable 2FA Status for the current User */
449
  app.post('/rest/2fa/disable',
450
    new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }),
451
    security.isAuthorized(),
452
    twoFactorAuth.disable()
453
  )
454
  /* Verifying DB related challenges can be postponed until the next request for challenges is coming via finale */
455
  app.use(verify.databaseRelatedChallenges())
456

457
  // vuln-code-snippet start registerAdminChallenge
458
  /* Generated API endpoints */
459
  finale.initialize({ app, sequelize })
460

461
  const 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

477
  for (const { name, exclude, model } of autoModels) {
478
    const resource = finale.resource({
479
      model,
480
      endpoints: [`/api/${name}s`, `/api/${name}s/:id`],
481
      excludeAttributes: exclude,
482
      pagination: false
483
    })
484

485
    // create a wallet when a new user is registered using API
486
    if (name === 'User') { // vuln-code-snippet neutral-line registerAdminChallenge
487
      resource.create.send.before((req: Request, res: Response, context: { instance: { id: any }, continue: any }) => { // vuln-code-snippet vuln-line registerAdminChallenge
488
        WalletModel.create({ UserId: context.instance.id }).catch((err: unknown) => {
489
          console.log(err)
490
        })
491
        return context.continue // vuln-code-snippet neutral-line registerAdminChallenge
492
      }) // vuln-code-snippet neutral-line registerAdminChallenge
493
    } // vuln-code-snippet neutral-line registerAdminChallenge
494
    // vuln-code-snippet end registerAdminChallenge
495

496
    // translate challenge descriptions and hints on-the-fly
497
    if (name === 'Challenge') {
498
      resource.list.fetch.after((req: Request, res: Response, context: { instance: string | any[], continue: any }) => {
499
        for (let i = 0; i < context.instance.length; i++) {
500
          let description = context.instance[i].description
501
          if (utils.contains(description, '<em>(This challenge is <strong>')) {
502
            const warning = description.substring(description.indexOf(' <em>(This challenge is <strong>'))
503
            description = description.substring(0, description.indexOf(' <em>(This challenge is <strong>'))
504
            context.instance[i].description = req.__(description) + req.__(warning)
505
          } else {
506
            context.instance[i].description = req.__(description)
507
          }
508
          if (context.instance[i].hint) {
509
            context.instance[i].hint = req.__(context.instance[i].hint)
510
          }
511
        }
512
        return context.continue
513
      })
514
      resource.read.send.before((req: Request, res: Response, context: { instance: { description: string, hint: string }, continue: any }) => {
515
        context.instance.description = req.__(context.instance.description)
516
        if (context.instance.hint) {
517
          context.instance.hint = req.__(context.instance.hint)
518
        }
519
        return context.continue
520
      })
521
    }
522

523
    // translate security questions on-the-fly
524
    if (name === 'SecurityQuestion') {
525
      resource.list.fetch.after((req: Request, res: Response, context: { instance: string | any[], continue: any }) => {
526
        for (let i = 0; i < context.instance.length; i++) {
527
          context.instance[i].question = req.__(context.instance[i].question)
528
        }
529
        return context.continue
530
      })
531
      resource.read.send.before((req: Request, res: Response, context: { instance: { question: string }, continue: any }) => {
532
        context.instance.question = req.__(context.instance.question)
533
        return context.continue
534
      })
535
    }
536

537
    // translate product names and descriptions on-the-fly
538
    if (name === 'Product') {
539
      resource.list.fetch.after((req: Request, res: Response, context: { instance: any[], continue: any }) => {
540
        for (let i = 0; i < context.instance.length; i++) {
541
          context.instance[i].name = req.__(context.instance[i].name)
542
          context.instance[i].description = req.__(context.instance[i].description)
543
        }
544
        return context.continue
545
      })
546
      resource.read.send.before((req: Request, res: Response, context: { instance: { name: string, description: string }, continue: any }) => {
547
        context.instance.name = req.__(context.instance.name)
548
        context.instance.description = req.__(context.instance.description)
549
        return context.continue
550
      })
551
    }
552

553
    // fix the api difference between finale (fka epilogue) and previously used sequlize-restful
554
    resource.all.send.before((req: Request, res: Response, context: { instance: { status: string, data: any }, continue: any }) => {
555
      context.instance = {
556
        status: 'success',
557
        data: context.instance
558
      }
559
      return context.continue
560
    })
561
  }
562

563
  /* Custom Restful API */
564
  app.post('/rest/user/login', login())
565
  app.get('/rest/user/change-password', changePassword())
566
  app.post('/rest/user/reset-password', resetPassword())
567
  app.get('/rest/user/security-question', securityQuestion())
568
  app.get('/rest/user/whoami', security.updateAuthenticatedUsers(), currentUser())
569
  app.get('/rest/user/authentication-details', authenticatedUsers())
570
  app.get('/rest/products/search', search())
571
  app.get('/rest/basket/:id', basket())
572
  app.post('/rest/basket/:id/checkout', order())
573
  app.put('/rest/basket/:id/coupon/:coupon', coupon())
574
  app.get('/rest/admin/application-version', appVersion())
575
  app.get('/rest/admin/application-configuration', appConfiguration())
576
  app.get('/rest/repeat-notification', repeatNotification())
577
  app.get('/rest/continue-code', continueCode.continueCode())
578
  app.get('/rest/continue-code-findIt', continueCode.continueCodeFindIt())
579
  app.get('/rest/continue-code-fixIt', continueCode.continueCodeFixIt())
580
  app.put('/rest/continue-code-findIt/apply/:continueCode', restoreProgress.restoreProgressFindIt())
581
  app.put('/rest/continue-code-fixIt/apply/:continueCode', restoreProgress.restoreProgressFixIt())
582
  app.put('/rest/continue-code/apply/:continueCode', restoreProgress.restoreProgress())
583
  app.get('/rest/admin/application-version', appVersion())
584
  app.get('/rest/captcha', captcha())
585
  app.get('/rest/image-captcha', imageCaptcha())
586
  app.get('/rest/track-order/:id', trackOrder())
587
  app.get('/rest/country-mapping', countryMapping())
588
  app.get('/rest/saveLoginIp', saveLoginIp())
589
  app.post('/rest/user/data-export', security.appendUserId(), imageCaptcha.verifyCaptcha())
590
  app.post('/rest/user/data-export', security.appendUserId(), dataExport())
591
  app.get('/rest/languages', languageList())
592
  app.get('/rest/order-history', orderHistory.orderHistory())
593
  app.get('/rest/order-history/orders', security.isAccounting(), orderHistory.allOrders())
594
  app.put('/rest/order-history/:id/delivery-status', security.isAccounting(), orderHistory.toggleDeliveryStatus())
595
  app.get('/rest/wallet/balance', security.appendUserId(), wallet.getWalletBalance())
596
  app.put('/rest/wallet/balance', security.appendUserId(), wallet.addWalletBalance())
597
  app.get('/rest/deluxe-membership', deluxe.deluxeMembershipStatus())
598
  app.post('/rest/deluxe-membership', security.appendUserId(), deluxe.upgradeToDeluxe())
599
  app.get('/rest/memories', memory.getMemories())
600
  app.get('/rest/chatbot/status', chatbot.status())
601
  app.post('/rest/chatbot/respond', chatbot.process())
602
  /* NoSQL API endpoints */
603
  app.get('/rest/products/:id/reviews', showProductReviews())
604
  app.put('/rest/products/:id/reviews', createProductReviews())
605
  app.patch('/rest/products/reviews', security.isAuthorized(), updateProductReviews())
606
  app.post('/rest/products/reviews', security.isAuthorized(), likeProductReviews())
607

608
  /* Web3 API endpoints */
609
  app.post('/rest/web3/submitKey', checkKeys.checkKeys())
610
  app.get('/rest/web3/nftUnlocked', checkKeys.nftUnlocked())
611
  app.get('/rest/web3/nftMintListen', nftMint.nftMintListener())
612
  app.post('/rest/web3/walletNFTVerify', nftMint.walletNFTVerify())
613
  app.post('/rest/web3/walletExploitAddress', web3Wallet.contractExploitListener())
614

615
  /* B2B Order API */
616
  app.post('/b2b/v2/orders', b2bOrder())
617

618
  /* File Serving */
619
  app.get('/the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg', easterEgg())
620
  app.get('/this/page/is/hidden/behind/an/incredibly/high/paywall/that/could/only/be/unlocked/by/sending/1btc/to/us', premiumReward())
621
  app.get('/we/may/also/instruct/you/to/refuse/all/reasonably/necessary/responsibility', privacyPolicyProof())
622

623
  /* Route for dataerasure page */
624
  app.use('/dataerasure', dataErasure)
625

626
  /* Route for redirects */
627
  app.get('/redirect', redirect())
628

629
  /* Routes for promotion video page */
630
  app.get('/promotion', videoHandler.promotionVideo())
631
  app.get('/video', videoHandler.getVideo())
632

633
  /* Routes for profile page */
634
  app.get('/profile', security.updateAuthenticatedUsers(), userProfile())
635
  app.post('/profile', updateUserProfile())
636

637
  /* Route for vulnerable code snippets */
638
  app.get('/snippets', vulnCodeSnippet.serveChallengesWithCodeSnippet())
639
  app.get('/snippets/:challenge', vulnCodeSnippet.serveCodeSnippet())
640
  app.post('/snippets/verdict', vulnCodeSnippet.checkVulnLines())
641
  app.get('/snippets/fixes/:key', vulnCodeFixes.serveCodeFixes())
642
  app.post('/snippets/fixes', vulnCodeFixes.checkCorrectFix())
643

644
  app.use(angular())
645

646
  /* Error Handling */
647
  app.use(verify.errorHandlingChallenge())
648
  app.use(errorhandler())
649
}).catch((err) => {
650
  console.error(err)
651
})
652

653
const multer = require('multer')
654
const uploadToMemory = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } })
655
const mimeTypeMap: any = {
656
  'image/png': 'png',
657
  'image/jpeg': 'jpg',
658
  'image/jpg': 'jpg'
659
}
660
const uploadToDisk = multer({
661
  storage: multer.diskStorage({
662
    destination: (req: Request, file: any, cb: any) => {
663
      const isValid = mimeTypeMap[file.mimetype]
664
      let error: Error | null = new Error('Invalid mime type')
665
      if (isValid) {
666
        error = null
667
      }
668
      cb(error, path.resolve('frontend/dist/frontend/assets/public/images/uploads/'))
669
    },
670
    filename: (req: Request, file: any, cb: any) => {
671
      const name = security.sanitizeFilename(file.originalname)
672
        .toLowerCase()
673
        .split(' ')
674
        .join('-')
675
      const ext = mimeTypeMap[file.mimetype]
676
      cb(null, name + '-' + Date.now() + '.' + ext)
677
    }
678
  })
679
})
680

681
const expectedModels = ['Address', 'Basket', 'BasketItem', 'Captcha', 'Card', 'Challenge', 'Complaint', 'Delivery', 'Feedback', 'ImageCaptcha', 'Memory', 'PrivacyRequestModel', 'Product', 'Quantity', 'Recycle', 'SecurityAnswer', 'SecurityQuestion', 'User', 'Wallet']
682
while (!expectedModels.every(model => Object.keys(sequelize.models).includes(model))) {
683
  logger.info(`Entity models ${colors.bold(Object.keys(sequelize.models).length.toString())} of ${colors.bold(expectedModels.length.toString())} are initialized (${colors.yellow('WAITING')})`)
684
}
685
logger.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 */
689
let metricsUpdateLoop: any
690
const Metrics = metrics.observeMetrics() // vuln-code-snippet neutral-line exposedMetricsChallenge
691
app.get('/metrics', metrics.serveMetrics()) // vuln-code-snippet vuln-line exposedMetricsChallenge
692
errorhandler.title = `${config.get<string>('application.name')} (Express ${utils.version('express')})`
693

694
export async function start (readyCallback?: () => void) {
695
  const datacreatorEnd = startupGauge.startTimer({ task: 'datacreator' })
696
  await sequelize.sync({ force: true })
697
  await datacreator()
698
  datacreatorEnd()
699
  const port = process.env.PORT ?? config.get('server.port')
700
  process.env.BASE_PATH = process.env.BASE_PATH ?? config.get('server.basePath')
701

702
  metricsUpdateLoop = Metrics.updateLoop() // vuln-code-snippet neutral-line exposedMetricsChallenge
703

704
  server.listen(port, () => {
705
    logger.info(colors.cyan(`Server listening on port ${colors.bold(`${port}`)}`))
706
    startupGauge.set({ task: 'ready' }, (Date.now() - startTime) / 1000)
707
    if (process.env.BASE_PATH !== '') {
708
      logger.info(colors.cyan(`Server using proxy base path ${colors.bold(`${process.env.BASE_PATH}`)} for redirects`))
709
    }
710
    registerWebsocketEvents(server)
711
    if (readyCallback) {
712
      readyCallback()
713
    }
714
  })
715

716
  void collectDurationPromise('customizeApplication', customizeApplication)() // vuln-code-snippet hide-line
717
  void collectDurationPromise('customizeEasterEgg', customizeEasterEgg)() // vuln-code-snippet hide-line
718
}
719

720
export function close (exitCode: number | undefined) {
721
  if (server) {
722
    clearInterval(metricsUpdateLoop)
723
    server.close()
724
  }
725
  if (exitCode !== undefined) {
726
    process.exit(exitCode)
727
  }
728
}
729
// vuln-code-snippet end exposedMetricsChallenge
730

731
// stop server on sigint or sigterm signals
732
process.on('SIGINT', () => { close(0) })
733
process.on('SIGTERM', () => { close(0) })
734

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.