-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeScript example #17
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,381 @@ | ||
/* | ||
* Copyright (c) 2014-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
import { Request, Response, NextFunction } from 'express' | ||
import { Challenge, Product } from '../data/types' | ||
import { JwtPayload, VerifyErrors } from 'jsonwebtoken' | ||
import { FeedbackModel } from '../models/feedback' | ||
import { ComplaintModel } from '../models/complaint' | ||
import { Op } from 'sequelize' | ||
import challengeUtils = require('../lib/challengeUtils') | ||
|
||
const utils = require('../lib/utils') | ||
const security = require('../lib/insecurity') | ||
const jwt = require('jsonwebtoken') | ||
const jws = require('jws') | ||
const cache = require('../data/datacache') | ||
const challenges = cache.challenges | ||
const products = cache.products | ||
const config = require('config') | ||
|
||
exports.forgedFeedbackChallenge = () => (req: Request, res: Response, next: NextFunction) => { | ||
challengeUtils.solveIf(challenges.forgedFeedbackChallenge, () => { | ||
const user = security.authenticatedUsers.from(req) | ||
const userId = user?.data ? user.data.id : undefined | ||
return req.body?.UserId && req.body.UserId != userId // eslint-disable-line eqeqeq | ||
}) | ||
next() | ||
} | ||
|
||
exports.captchaBypassChallenge = () => (req: Request, res: Response, next: NextFunction) => { | ||
if (challengeUtils.notSolved(challenges.captchaBypassChallenge)) { | ||
if (req.app.locals.captchaReqId >= 10) { | ||
if ((new Date().getTime() - req.app.locals.captchaBypassReqTimes[req.app.locals.captchaReqId - 10]) <= 20000) { | ||
challengeUtils.solve(challenges.captchaBypassChallenge) | ||
} | ||
} | ||
req.app.locals.captchaBypassReqTimes[req.app.locals.captchaReqId - 1] = new Date().getTime() | ||
req.app.locals.captchaReqId++ | ||
} | ||
next() | ||
} | ||
|
||
exports.registerAdminChallenge = () => (req: Request, res: Response, next: NextFunction) => { | ||
challengeUtils.solveIf(challenges.registerAdminChallenge, () => { return req.body && req.body.role === security.roles.admin }) | ||
next() | ||
} | ||
|
||
exports.passwordRepeatChallenge = () => (req: Request, res: Response, next: NextFunction) => { | ||
challengeUtils.solveIf(challenges.passwordRepeatChallenge, () => { return req.body && req.body.passwordRepeat !== req.body.password }) | ||
next() | ||
} | ||
|
||
exports.accessControlChallenges = () => ({ url }: Request, res: Response, next: NextFunction) => { | ||
challengeUtils.solveIf(challenges.scoreBoardChallenge, () => { return utils.endsWith(url, '/1px.png') }) | ||
challengeUtils.solveIf(challenges.adminSectionChallenge, () => { return utils.endsWith(url, '/19px.png') }) | ||
challengeUtils.solveIf(challenges.tokenSaleChallenge, () => { return utils.endsWith(url, '/56px.png') }) | ||
challengeUtils.solveIf(challenges.privacyPolicyChallenge, () => { return utils.endsWith(url, '/81px.png') }) | ||
challengeUtils.solveIf(challenges.extraLanguageChallenge, () => { return utils.endsWith(url, '/tlh_AA.json') }) | ||
challengeUtils.solveIf(challenges.retrieveBlueprintChallenge, () => { return utils.endsWith(url, cache.retrieveBlueprintChallengeFile) }) | ||
challengeUtils.solveIf(challenges.securityPolicyChallenge, () => { return utils.endsWith(url, '/security.txt') }) | ||
challengeUtils.solveIf(challenges.missingEncodingChallenge, () => { return utils.endsWith(url.toLowerCase(), '%f0%9f%98%bc-%23zatschi-%23whoneedsfourlegs-1572600969477.jpg') }) | ||
challengeUtils.solveIf(challenges.accessLogDisclosureChallenge, () => { return url.match(/access\.log(0-9-)*/) }) | ||
next() | ||
} | ||
|
||
exports.errorHandlingChallenge = () => (err: unknown, req: Request, { statusCode }: Response, next: NextFunction) => { | ||
challengeUtils.solveIf(challenges.errorHandlingChallenge, () => { return err && (statusCode === 200 || statusCode > 401) }) | ||
next(err) | ||
} | ||
|
||
exports.jwtChallenges = () => (req: Request, res: Response, next: NextFunction) => { | ||
if (challengeUtils.notSolved(challenges.jwtUnsignedChallenge)) { | ||
jwtChallenge(challenges.jwtUnsignedChallenge, req, 'none', /jwtn3d@/) | ||
} | ||
if (!utils.disableOnWindowsEnv() && challengeUtils.notSolved(challenges.jwtForgedChallenge)) { | ||
jwtChallenge(challenges.jwtForgedChallenge, req, 'HS256', /rsa_lord@/) | ||
} | ||
next() | ||
} | ||
|
||
exports.serverSideChallenges = () => (req: Request, res: Response, next: NextFunction) => { | ||
if (req.query.key === 'tRy_H4rd3r_n0thIng_iS_Imp0ssibl3') { | ||
if (challengeUtils.notSolved(challenges.sstiChallenge) && req.app.locals.abused_ssti_bug === true) { | ||
challengeUtils.solve(challenges.sstiChallenge) | ||
res.status(204).send() | ||
return | ||
} | ||
|
||
if (challengeUtils.notSolved(challenges.ssrfChallenge) && req.app.locals.abused_ssrf_bug === true) { | ||
challengeUtils.solve(challenges.ssrfChallenge) | ||
res.status(204).send() | ||
return | ||
} | ||
} | ||
next() | ||
} | ||
|
||
function jwtChallenge (challenge: Challenge, req: Request, algorithm: string, email: string | RegExp) { | ||
const token = utils.jwtFrom(req) | ||
if (token) { | ||
const decoded = jws.decode(token) ? jwt.decode(token) : null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSON Web Tokens (JWT) signature verification bypass
|
||
jwt.verify(token, security.publicKey, (err: VerifyErrors | null, verified: JwtPayload) => { | ||
if (err === null) { | ||
challengeUtils.solveIf(challenge, () => { return hasAlgorithm(token, algorithm) && hasEmail(decoded, email) }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Observable timing discrepancy
|
||
} | ||
}) | ||
} | ||
} | ||
|
||
function hasAlgorithm (token: string, algorithm: string) { | ||
const header = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString()) | ||
return token && header && header.alg === algorithm | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Observable timing discrepancy
|
||
} | ||
|
||
function hasEmail (token: { data: { email: string } }, email: string | RegExp) { | ||
return token?.data?.email?.match(email) | ||
} | ||
|
||
exports.databaseRelatedChallenges = () => (req: Request, res: Response, next: NextFunction) => { | ||
if (challengeUtils.notSolved(challenges.changeProductChallenge) && products.osaft) { | ||
changeProductChallenge(products.osaft) | ||
} | ||
if (challengeUtils.notSolved(challenges.feedbackChallenge)) { | ||
feedbackChallenge() | ||
} | ||
if (challengeUtils.notSolved(challenges.knownVulnerableComponentChallenge)) { | ||
knownVulnerableComponentChallenge() | ||
} | ||
if (challengeUtils.notSolved(challenges.weirdCryptoChallenge)) { | ||
weirdCryptoChallenge() | ||
} | ||
if (challengeUtils.notSolved(challenges.typosquattingNpmChallenge)) { | ||
typosquattingNpmChallenge() | ||
} | ||
if (challengeUtils.notSolved(challenges.typosquattingAngularChallenge)) { | ||
typosquattingAngularChallenge() | ||
} | ||
if (challengeUtils.notSolved(challenges.hiddenImageChallenge)) { | ||
hiddenImageChallenge() | ||
} | ||
if (challengeUtils.notSolved(challenges.supplyChainAttackChallenge)) { | ||
supplyChainAttackChallenge() | ||
} | ||
if (challengeUtils.notSolved(challenges.dlpPastebinDataLeakChallenge)) { | ||
dlpPastebinDataLeakChallenge() | ||
} | ||
next() | ||
} | ||
|
||
function changeProductChallenge (osaft: Product) { | ||
let urlForProductTamperingChallenge: string | null = null | ||
void osaft.reload().then(() => { | ||
for (const product of config.products) { | ||
if (product.urlForProductTamperingChallenge !== undefined) { | ||
urlForProductTamperingChallenge = product.urlForProductTamperingChallenge | ||
break | ||
} | ||
} | ||
if (urlForProductTamperingChallenge) { | ||
if (!utils.contains(osaft.description, `${urlForProductTamperingChallenge}`)) { | ||
if (utils.contains(osaft.description, `<a href="${config.get('challenges.overwriteUrlForProductTamperingChallenge')}" target="_blank">More...</a>`)) { | ||
challengeUtils.solve(challenges.changeProductChallenge) | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
|
||
function feedbackChallenge () { | ||
FeedbackModel.findAndCountAll({ where: { rating: 5 } }).then(({ count }: { count: number }) => { | ||
if (count === 0) { | ||
challengeUtils.solve(challenges.feedbackChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to retrieve feedback details. Please try again') | ||
}) | ||
} | ||
|
||
function knownVulnerableComponentChallenge () { | ||
FeedbackModel.findAndCountAll({ | ||
where: { | ||
comment: { | ||
[Op.or]: knownVulnerableComponents() | ||
} | ||
} | ||
}).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.knownVulnerableComponentChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
ComplaintModel.findAndCountAll({ | ||
where: { | ||
message: { | ||
[Op.or]: knownVulnerableComponents() | ||
} | ||
} | ||
}).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.knownVulnerableComponentChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
} | ||
|
||
function knownVulnerableComponents () { | ||
return [ | ||
{ | ||
[Op.and]: [ | ||
{ [Op.like]: '%sanitize-html%' }, | ||
{ [Op.like]: '%1.4.2%' } | ||
] | ||
}, | ||
{ | ||
[Op.and]: [ | ||
{ [Op.like]: '%express-jwt%' }, | ||
{ [Op.like]: '%0.1.3%' } | ||
] | ||
} | ||
] | ||
} | ||
|
||
function weirdCryptoChallenge () { | ||
FeedbackModel.findAndCountAll({ | ||
where: { | ||
comment: { | ||
[Op.or]: weirdCryptos() | ||
} | ||
} | ||
}).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.weirdCryptoChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
ComplaintModel.findAndCountAll({ | ||
where: { | ||
message: { | ||
[Op.or]: weirdCryptos() | ||
} | ||
} | ||
}).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.weirdCryptoChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
} | ||
|
||
function weirdCryptos () { | ||
return [ | ||
{ [Op.like]: '%z85%' }, | ||
{ [Op.like]: '%base85%' }, | ||
{ [Op.like]: '%hashids%' }, | ||
{ [Op.like]: '%md5%' }, | ||
{ [Op.like]: '%base64%' } | ||
] | ||
} | ||
|
||
function typosquattingNpmChallenge () { | ||
FeedbackModel.findAndCountAll({ where: { comment: { [Op.like]: '%epilogue-js%' } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.typosquattingNpmChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
ComplaintModel.findAndCountAll({ where: { message: { [Op.like]: '%epilogue-js%' } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.typosquattingNpmChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
} | ||
|
||
function typosquattingAngularChallenge () { | ||
FeedbackModel.findAndCountAll({ where: { comment: { [Op.like]: '%anuglar2-qrcode%' } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.typosquattingAngularChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
ComplaintModel.findAndCountAll({ where: { message: { [Op.like]: '%anuglar2-qrcode%' } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.typosquattingAngularChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
} | ||
|
||
function hiddenImageChallenge () { | ||
FeedbackModel.findAndCountAll({ where: { comment: { [Op.like]: '%pickle rick%' } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.hiddenImageChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
ComplaintModel.findAndCountAll({ where: { message: { [Op.like]: '%pickle rick%' } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.hiddenImageChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
} | ||
|
||
function supplyChainAttackChallenge () { | ||
FeedbackModel.findAndCountAll({ where: { comment: { [Op.or]: eslintScopeVulnIds() } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.supplyChainAttackChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
ComplaintModel.findAndCountAll({ where: { message: { [Op.or]: eslintScopeVulnIds() } } } | ||
).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.supplyChainAttackChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
} | ||
|
||
function eslintScopeVulnIds () { | ||
return [ | ||
{ [Op.like]: '%eslint-scope/issues/39%' }, | ||
{ [Op.like]: '%npm:eslint-scope:20180712%' } | ||
] | ||
} | ||
|
||
function dlpPastebinDataLeakChallenge () { | ||
FeedbackModel.findAndCountAll({ | ||
where: { | ||
comment: { [Op.and]: dangerousIngredients() } | ||
} | ||
}).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.dlpPastebinDataLeakChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
ComplaintModel.findAndCountAll({ | ||
where: { | ||
message: { [Op.and]: dangerousIngredients() } | ||
} | ||
}).then(({ count }: { count: number }) => { | ||
if (count > 0) { | ||
challengeUtils.solve(challenges.dlpPastebinDataLeakChallenge) | ||
} | ||
}).catch(() => { | ||
throw new Error('Unable to get data for known vulnerabilities. Please try again') | ||
}) | ||
} | ||
|
||
function dangerousIngredients () { | ||
const ingredients: Array<{ [op: symbol]: string }> = [] | ||
const dangerousProduct = config.get('products').filter((product: Product) => product.keywordsForPastebinDataLeakChallenge)[0] | ||
dangerousProduct.keywordsForPastebinDataLeakChallenge.forEach((keyword: string) => { | ||
ingredients.push({ [Op.like]: `%${keyword}%` }) | ||
}) | ||
return ingredients | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Observable timing discrepancy
File: verify.ts | Checkov ID: CKV3_SAST_161
Description
CWE:
CWE-208: Observable Timing Discrepancy
This SAST policy detects string comparisons using the
===
,!==
,==
, or!=
operators against security-sensitive values. These string comparisons are not constant time, meaning that the conditional statement will immediately exit as soon as the first character that does not match is found. This can lead to observable timing discrepancies and potentially allow an adversary to calculate or observe small timing differences to brute force a string that matches the expected value.Vulnerable code example:
The above code is vulnerable because it compares the user's input with a security-sensitive value,
password
, using the==
operator. This type of comparison is not constant time and allows an adversary to potentially guess the correct value by monitoring the timing differences.