juice-shop
201 строка · 7.8 Кб
1/*
2* Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
3* SPDX-License-Identifier: MIT
4*/
5
6import fs from 'fs'
7import crypto from 'crypto'
8import { type Request, type Response, type NextFunction } from 'express'
9import { type UserModel } from 'models/user'
10import expressJwt from 'express-jwt'
11import jwt from 'jsonwebtoken'
12import jws from 'jws'
13import sanitizeHtmlLib from 'sanitize-html'
14import sanitizeFilenameLib from 'sanitize-filename'
15import * as utils from './utils'
16
17/* jslint node: true */
18// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
19// @ts-expect-error FIXME no typescript definitions for z85 :(
20import * as z85 from 'z85'
21
22export const publicKey = fs ? fs.readFileSync('encryptionkeys/jwt.pub', 'utf8') : 'placeholder-public-key'
23const privateKey = '-----BEGIN RSA PRIVATE KEY-----\r\nMIICXAIBAAKBgQDNwqLEe9wgTXCbC7+RPdDbBbeqjdbs4kOPOIGzqLpXvJXlxxW8iMz0EaM4BKUqYsIa+ndv3NAn2RxCd5ubVdJJcX43zO6Ko0TFEZx/65gY3BE0O6syCEmUP4qbSd6exou/F+WTISzbQ5FBVPVmhnYhG/kpwt/cIxK5iUn5hm+4tQIDAQABAoGBAI+8xiPoOrA+KMnG/T4jJsG6TsHQcDHvJi7o1IKC/hnIXha0atTX5AUkRRce95qSfvKFweXdJXSQ0JMGJyfuXgU6dI0TcseFRfewXAa/ssxAC+iUVR6KUMh1PE2wXLitfeI6JLvVtrBYswm2I7CtY0q8n5AGimHWVXJPLfGV7m0BAkEA+fqFt2LXbLtyg6wZyxMA/cnmt5Nt3U2dAu77MzFJvibANUNHE4HPLZxjGNXN+a6m0K6TD4kDdh5HfUYLWWRBYQJBANK3carmulBwqzcDBjsJ0YrIONBpCAsXxk8idXb8jL9aNIg15Wumm2enqqObahDHB5jnGOLmbasizvSVqypfM9UCQCQl8xIqy+YgURXzXCN+kwUgHinrutZms87Jyi+D8Br8NY0+Nlf+zHvXAomD2W5CsEK7C+8SLBr3k/TsnRWHJuECQHFE9RA2OP8WoaLPuGCyFXaxzICThSRZYluVnWkZtxsBhW2W8z1b8PvWUE7kMy7TnkzeJS2LSnaNHoyxi7IaPQUCQCwWU4U+v4lD7uYBw00Ga/xt+7+UqFPlPVdz1yyr4q24Zxaw0LgmuEvgU5dycq8N7JxjTubX0MIRR+G9fmDBBl8=\r\n-----END RSA PRIVATE KEY-----'
24
25interface ResponseWithUser {
26status: string
27data: UserModel
28iat: number
29exp: number
30bid: number
31}
32
33interface IAuthenticatedUsers {
34tokenMap: Record<string, ResponseWithUser>
35idMap: Record<string, string>
36put: (token: string, user: ResponseWithUser) => void
37get: (token: string) => ResponseWithUser | undefined
38tokenOf: (user: UserModel) => string | undefined
39from: (req: Request) => ResponseWithUser | undefined
40updateFrom: (req: Request, user: ResponseWithUser) => any
41}
42
43export const hash = (data: string) => crypto.createHash('md5').update(data).digest('hex')
44export const hmac = (data: string) => crypto.createHmac('sha256', 'pa4qacea4VK9t9nGv7yZtwmj').update(data).digest('hex')
45
46export const cutOffPoisonNullByte = (str: string) => {
47const nullByte = '%00'
48if (utils.contains(str, nullByte)) {
49return str.substring(0, str.indexOf(nullByte))
50}
51return str
52}
53
54export const isAuthorized = () => expressJwt(({ secret: publicKey }) as any)
55export const denyAll = () => expressJwt({ secret: '' + Math.random() } as any)
56export const authorize = (user = {}) => jwt.sign(user, privateKey, { expiresIn: '6h', algorithm: 'RS256' })
57export const verify = (token: string) => token ? (jws.verify as ((token: string, secret: string) => boolean))(token, publicKey) : false
58export const decode = (token: string) => { return jws.decode(token)?.payload }
59
60export const sanitizeHtml = (html: string) => sanitizeHtmlLib(html)
61export const sanitizeLegacy = (input = '') => input.replace(/<(?:\w+)\W+?[\w]/gi, '')
62export const sanitizeFilename = (filename: string) => sanitizeFilenameLib(filename)
63export const sanitizeSecure = (html: string): string => {
64const sanitized = sanitizeHtml(html)
65if (sanitized === html) {
66return html
67} else {
68return sanitizeSecure(sanitized)
69}
70}
71
72export const authenticatedUsers: IAuthenticatedUsers = {
73tokenMap: {},
74idMap: {},
75put: function (token: string, user: ResponseWithUser) {
76this.tokenMap[token] = user
77this.idMap[user.data.id] = token
78},
79get: function (token: string) {
80return token ? this.tokenMap[utils.unquote(token)] : undefined
81},
82tokenOf: function (user: UserModel) {
83return user ? this.idMap[user.id] : undefined
84},
85from: function (req: Request) {
86const token = utils.jwtFrom(req)
87return token ? this.get(token) : undefined
88},
89updateFrom: function (req: Request, user: ResponseWithUser) {
90const token = utils.jwtFrom(req)
91this.put(token, user)
92}
93}
94
95export const userEmailFrom = ({ headers }: any) => {
96return headers ? headers['x-user-email'] : undefined
97}
98
99export const generateCoupon = (discount: number, date = new Date()) => {
100const coupon = utils.toMMMYY(date) + '-' + discount
101return z85.encode(coupon)
102}
103
104export const discountFromCoupon = (coupon: string) => {
105if (coupon) {
106const decoded = z85.decode(coupon)
107if (decoded && (hasValidFormat(decoded.toString()) != null)) {
108const parts = decoded.toString().split('-')
109const validity = parts[0]
110if (utils.toMMMYY(new Date()) === validity) {
111const discount = parts[1]
112return parseInt(discount)
113}
114}
115}
116return undefined
117}
118
119function hasValidFormat (coupon: string) {
120return coupon.match(/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)[0-9]{2}-[0-9]{2}/)
121}
122
123// vuln-code-snippet start redirectCryptoCurrencyChallenge redirectChallenge
124export const redirectAllowlist = new Set([
125'https://github.com/juice-shop/juice-shop',
126'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm', // vuln-code-snippet vuln-line redirectCryptoCurrencyChallenge
127'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW', // vuln-code-snippet vuln-line redirectCryptoCurrencyChallenge
128'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6', // vuln-code-snippet vuln-line redirectCryptoCurrencyChallenge
129'http://shop.spreadshirt.com/juiceshop',
130'http://shop.spreadshirt.de/juiceshop',
131'https://www.stickeryou.com/products/owasp-juice-shop/794',
132'http://leanpub.com/juice-shop'
133])
134
135export const isRedirectAllowed = (url: string) => {
136let allowed = false
137for (const allowedUrl of redirectAllowlist) {
138allowed = allowed || url.includes(allowedUrl) // vuln-code-snippet vuln-line redirectChallenge
139}
140return allowed
141}
142// vuln-code-snippet end redirectCryptoCurrencyChallenge redirectChallenge
143
144export const roles = {
145customer: 'customer',
146deluxe: 'deluxe',
147accounting: 'accounting',
148admin: 'admin'
149}
150
151export const deluxeToken = (email: string) => {
152const hmac = crypto.createHmac('sha256', privateKey)
153return hmac.update(email + roles.deluxe).digest('hex')
154}
155
156export const isAccounting = () => {
157return (req: Request, res: Response, next: NextFunction) => {
158const decodedToken = verify(utils.jwtFrom(req)) && decode(utils.jwtFrom(req))
159if (decodedToken?.data?.role === roles.accounting) {
160next()
161} else {
162res.status(403).json({ error: 'Malicious activity detected' })
163}
164}
165}
166
167export const isDeluxe = (req: Request) => {
168const decodedToken = verify(utils.jwtFrom(req)) && decode(utils.jwtFrom(req))
169return decodedToken?.data?.role === roles.deluxe && decodedToken?.data?.deluxeToken && decodedToken?.data?.deluxeToken === deluxeToken(decodedToken?.data?.email)
170}
171
172export const isCustomer = (req: Request) => {
173const decodedToken = verify(utils.jwtFrom(req)) && decode(utils.jwtFrom(req))
174return decodedToken?.data?.role === roles.customer
175}
176
177export const appendUserId = () => {
178return (req: Request, res: Response, next: NextFunction) => {
179try {
180req.body.UserId = authenticatedUsers.tokenMap[utils.jwtFrom(req)].data.id
181next()
182} catch (error: any) {
183res.status(401).json({ status: 'error', message: error })
184}
185}
186}
187
188export const updateAuthenticatedUsers = () => (req: Request, res: Response, next: NextFunction) => {
189const token = req.cookies.token || utils.jwtFrom(req)
190if (token) {
191jwt.verify(token, publicKey, (err: Error | null, decoded: any) => {
192if (err === null) {
193if (authenticatedUsers.get(token) === undefined) {
194authenticatedUsers.put(token, decoded)
195res.cookie('token', token)
196}
197}
198})
199}
200next()
201}
202