mirror of
https://github.com/KeygraphHQ/juice-shop.git
synced 2026-11-10 10:41:36 -10:00
327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
/*
|
|
* Copyright (c) 2014-2025 Bjoern Kimminich & the OWASP Juice Shop contributors.
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
import chai from 'chai'
|
|
import sinon from 'sinon'
|
|
import config from 'config'
|
|
import sinonChai from 'sinon-chai'
|
|
import { challenges, products, setRetrieveBlueprintChallengeFile } from '../../data/datacache'
|
|
import type { Product, Challenge } from 'data/types'
|
|
import type { Product as ProductConfig } from '../../lib/config.types'
|
|
import * as security from '../../lib/insecurity'
|
|
import { type UserModel } from 'models/user'
|
|
import * as utils from '../../lib/utils'
|
|
import * as verify from '../../routes/verify'
|
|
const expect = chai.expect
|
|
|
|
chai.use(sinonChai)
|
|
|
|
describe('verify', () => {
|
|
let req: any
|
|
let res: any
|
|
let next: any
|
|
let save: any
|
|
let err: any
|
|
|
|
beforeEach(() => {
|
|
req = { body: {}, headers: {} }
|
|
res = { json: sinon.spy() }
|
|
next = sinon.spy()
|
|
save = () => ({
|
|
then () { }
|
|
})
|
|
})
|
|
|
|
describe('"forgedFeedbackChallenge"', () => {
|
|
beforeEach(() => {
|
|
security.authenticatedUsers.put('token12345', {
|
|
data: {
|
|
id: 42,
|
|
email: '[email protected]'
|
|
} as unknown as UserModel
|
|
})
|
|
challenges.forgedFeedbackChallenge = { solved: false, save } as unknown as Challenge
|
|
})
|
|
|
|
it('is not solved when an authenticated user passes his own ID when writing feedback', () => {
|
|
req.body.UserId = 42
|
|
req.headers = { authorization: 'Bearer token12345' }
|
|
|
|
verify.forgedFeedbackChallenge()(req, res, next)
|
|
|
|
expect(challenges.forgedFeedbackChallenge.solved).to.equal(false)
|
|
})
|
|
|
|
it('is not solved when an authenticated user passes no ID when writing feedback', () => {
|
|
req.body.UserId = undefined
|
|
req.headers = { authorization: 'Bearer token12345' }
|
|
|
|
verify.forgedFeedbackChallenge()(req, res, next)
|
|
|
|
expect(challenges.forgedFeedbackChallenge.solved).to.equal(false)
|
|
})
|
|
|
|
it('is solved when an authenticated user passes someone elses ID when writing feedback', () => {
|
|
req.body.UserId = 1
|
|
req.headers = { authorization: 'Bearer token12345' }
|
|
|
|
verify.forgedFeedbackChallenge()(req, res, next)
|
|
|
|
expect(challenges.forgedFeedbackChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('is solved when an unauthenticated user passes someones ID when writing feedback', () => {
|
|
req.body.UserId = 1
|
|
req.headers = {}
|
|
|
|
verify.forgedFeedbackChallenge()(req, res, next)
|
|
|
|
expect(challenges.forgedFeedbackChallenge.solved).to.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('accessControlChallenges', () => {
|
|
it('"scoreBoardChallenge" is solved when the 1px.png transpixel is requested', () => {
|
|
challenges.scoreBoardChallenge = { solved: false, save } as unknown as Challenge
|
|
req.url = 'http://juice-sh.op/public/images/padding/1px.png'
|
|
|
|
verify.accessControlChallenges()(req, res, next)
|
|
|
|
expect(challenges.scoreBoardChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"adminSectionChallenge" is solved when the 19px.png transpixel is requested', () => {
|
|
challenges.adminSectionChallenge = { solved: false, save } as unknown as Challenge
|
|
req.url = 'http://juice-sh.op/public/images/padding/19px.png'
|
|
|
|
verify.accessControlChallenges()(req, res, next)
|
|
|
|
expect(challenges.adminSectionChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"tokenSaleChallenge" is solved when the 56px.png transpixel is requested', () => {
|
|
challenges.tokenSaleChallenge = { solved: false, save } as unknown as Challenge
|
|
req.url = 'http://juice-sh.op/public/images/padding/56px.png'
|
|
|
|
verify.accessControlChallenges()(req, res, next)
|
|
|
|
expect(challenges.tokenSaleChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"extraLanguageChallenge" is solved when the Klingon translation file is requested', () => {
|
|
challenges.extraLanguageChallenge = { solved: false, save } as unknown as Challenge
|
|
req.url = 'http://juice-sh.op/public/i18n/tlh_AA.json'
|
|
|
|
verify.accessControlChallenges()(req, res, next)
|
|
|
|
expect(challenges.extraLanguageChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"retrieveBlueprintChallenge" is solved when the blueprint file is requested', () => {
|
|
challenges.retrieveBlueprintChallenge = { solved: false, save } as unknown as Challenge
|
|
setRetrieveBlueprintChallengeFile('test.dxf')
|
|
req.url = 'http://juice-sh.op/public/images/products/test.dxf'
|
|
|
|
verify.accessControlChallenges()(req, res, next)
|
|
|
|
expect(challenges.retrieveBlueprintChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"missingEncodingChallenge" is solved when the crazy cat photo is requested', () => {
|
|
challenges.missingEncodingChallenge = { solved: false, save } as unknown as Challenge
|
|
req.url = 'http://juice-sh.op/public/images/uploads/%E1%93%9A%E1%98%8F%E1%97%A2-%23zatschi-%23whoneedsfourlegs-1572600969477.jpg'
|
|
|
|
verify.accessControlChallenges()(req, res, next)
|
|
|
|
expect(challenges.missingEncodingChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"accessLogDisclosureChallenge" is solved when any server access log file is requested', () => {
|
|
challenges.accessLogDisclosureChallenge = { solved: false, save } as unknown as Challenge
|
|
req.url = 'http://juice-sh.op/support/logs/access.log.2019-01-15'
|
|
|
|
verify.accessControlChallenges()(req, res, next)
|
|
|
|
expect(challenges.accessLogDisclosureChallenge.solved).to.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('"errorHandlingChallenge"', () => {
|
|
beforeEach(() => {
|
|
challenges.errorHandlingChallenge = { solved: false, save } as unknown as Challenge
|
|
})
|
|
|
|
it('is solved when an error occurs on a response with OK 200 status code', () => {
|
|
res.statusCode = 200
|
|
err = new Error()
|
|
|
|
verify.errorHandlingChallenge()(err, req, res, next)
|
|
|
|
expect(challenges.errorHandlingChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
describe('is solved when an error occurs on a response with error', () => {
|
|
const httpStatus = [402, 403, 404, 500]
|
|
httpStatus.forEach(statusCode => {
|
|
it(`${statusCode} status code`, () => {
|
|
res.statusCode = statusCode
|
|
err = new Error()
|
|
|
|
verify.errorHandlingChallenge()(err, req, res, next)
|
|
|
|
expect(challenges.errorHandlingChallenge.solved).to.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
it('is not solved when no error occurs on a response with OK 200 status code', () => {
|
|
res.statusCode = 200
|
|
err = undefined
|
|
|
|
verify.errorHandlingChallenge()(err, req, res, next)
|
|
|
|
expect(challenges.errorHandlingChallenge.solved).to.equal(false)
|
|
})
|
|
|
|
describe('is not solved when no error occurs on a response with error', () => {
|
|
const httpStatus = [401, 402, 404, 500]
|
|
httpStatus.forEach(statusCode => {
|
|
it(`${statusCode} status code`, () => {
|
|
res.statusCode = statusCode
|
|
err = undefined
|
|
|
|
verify.errorHandlingChallenge()(err, req, res, next)
|
|
|
|
expect(challenges.errorHandlingChallenge.solved).to.equal(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should pass occurred error on to next route', () => {
|
|
res.statusCode = 500
|
|
err = new Error()
|
|
|
|
verify.errorHandlingChallenge()(err, req, res, next)
|
|
|
|
expect(next).to.have.been.calledWith(err)
|
|
})
|
|
})
|
|
|
|
describe('databaseRelatedChallenges', () => {
|
|
describe('"changeProductChallenge"', () => {
|
|
beforeEach(() => {
|
|
challenges.changeProductChallenge = { solved: false, save } as unknown as Challenge
|
|
products.osaft = { reload () { return { then (cb: any) { cb() } } } } as unknown as Product
|
|
})
|
|
|
|
it(`is solved when the link in the O-Saft product goes to ${config.get<string>('challenges.overwriteUrlForProductTamperingChallenge')}`, () => {
|
|
products.osaft.description = `O-Saft, yeah! <a href="${config.get<string>('challenges.overwriteUrlForProductTamperingChallenge')}" target="_blank">More...</a>`
|
|
|
|
verify.databaseRelatedChallenges()(req, res, next)
|
|
|
|
expect(challenges.changeProductChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('is not solved when the link in the O-Saft product is changed to an arbitrary URL', () => {
|
|
products.osaft.description = 'O-Saft, nooo! <a href="http://arbitrary.url" target="_blank">More...</a>'
|
|
|
|
verify.databaseRelatedChallenges()(req, res, next)
|
|
|
|
expect(challenges.changeProductChallenge.solved).to.equal(false)
|
|
})
|
|
|
|
it('is not solved when the link in the O-Saft product remained unchanged', () => {
|
|
let urlForProductTamperingChallenge = null
|
|
for (const product of config.get<ProductConfig[]>('products')) {
|
|
if (product.urlForProductTamperingChallenge !== undefined) {
|
|
urlForProductTamperingChallenge = product.urlForProductTamperingChallenge
|
|
break
|
|
}
|
|
}
|
|
products.osaft.description = `Vanilla O-Saft! <a href="${urlForProductTamperingChallenge}" target="_blank">More...</a>`
|
|
|
|
verify.databaseRelatedChallenges()(req, res, next)
|
|
|
|
expect(challenges.changeProductChallenge.solved).to.equal(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('jwtChallenges', () => {
|
|
beforeEach(() => {
|
|
challenges.jwtUnsignedChallenge = { solved: false, save } as unknown as Challenge
|
|
challenges.jwtForgedChallenge = { solved: false, save } as unknown as Challenge
|
|
})
|
|
|
|
it('"jwtUnsignedChallenge" is solved when forged unsigned token has email [email protected] in the payload', () => {
|
|
/*
|
|
Header: { "alg": "none", "typ": "JWT" }
|
|
Payload: { "data": { "email": "[email protected]" }, "iat": 1508639612, "exp": 9999999999 }
|
|
*/
|
|
req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQGp1aWNlLXNoLm9wIn0sImlhdCI6MTUwODYzOTYxMiwiZXhwIjo5OTk5OTk5OTk5fQ.' }
|
|
|
|
verify.jwtChallenges()(req, res, next)
|
|
|
|
expect(challenges.jwtUnsignedChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"jwtUnsignedChallenge" is solved when forged unsigned token has string "jwtn3d@" in the payload', () => {
|
|
/*
|
|
Header: { "alg": "none", "typ": "JWT" }
|
|
Payload: { "data": { "email": "jwtn3d@" }, "iat": 1508639612, "exp": 9999999999 }
|
|
*/
|
|
req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQCJ9LCJpYXQiOjE1MDg2Mzk2MTIsImV4cCI6OTk5OTk5OTk5OX0.' }
|
|
|
|
verify.jwtChallenges()(req, res, next)
|
|
|
|
expect(challenges.jwtUnsignedChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"jwtUnsignedChallenge" is not solved via regularly signed token even with email [email protected] in the payload', () => {
|
|
const token = security.authorize({ data: { email: '[email protected]' } })
|
|
req.headers = { authorization: `Bearer ${token}` }
|
|
|
|
verify.jwtChallenges()(req, res, next)
|
|
|
|
expect(challenges.jwtForgedChallenge.solved).to.equal(false)
|
|
})
|
|
|
|
if (utils.isChallengeEnabled(challenges.jwtForgedChallenge)) {
|
|
it('"jwtForgedChallenge" is solved when forged token HMAC-signed with public RSA-key has email [email protected] in the payload', () => {
|
|
/*
|
|
Header: { "alg": "HS256", "typ": "JWT" }
|
|
Payload: { "data": { "email": "[email protected]" }, "iat": 1508639612, "exp": 9999999999 }
|
|
*/
|
|
req.headers = { authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAanVpY2Utc2gub3AifSwiaWF0IjoxNTgyMjIxNTc1fQ.ycFwtqh4ht4Pq9K5rhiPPY256F9YCTIecd4FHFuSEAg' }
|
|
|
|
verify.jwtChallenges()(req, res, next)
|
|
|
|
expect(challenges.jwtForgedChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"jwtForgedChallenge" is solved when forged token HMAC-signed with public RSA-key has string "rsa_lord@" in the payload', () => {
|
|
/*
|
|
Header: { "alg": "HS256", "typ": "JWT" }
|
|
Payload: { "data": { "email": "rsa_lord@" }, "iat": 1508639612, "exp": 9999999999 }
|
|
*/
|
|
req.headers = { authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAIn0sImlhdCI6MTU4MjIyMTY3NX0.50f6VAIQk2Uzpf3sgH-1JVrrTuwudonm2DKn2ec7Tg8' }
|
|
|
|
verify.jwtChallenges()(req, res, next)
|
|
|
|
expect(challenges.jwtForgedChallenge.solved).to.equal(true)
|
|
})
|
|
|
|
it('"jwtForgedChallenge" is not solved when token regularly signed with private RSA-key has email [email protected] in the payload', () => {
|
|
const token = security.authorize({ data: { email: '[email protected]' } })
|
|
req.headers = { authorization: `Bearer ${token}` }
|
|
|
|
verify.jwtChallenges()(req, res, next)
|
|
|
|
expect(challenges.jwtForgedChallenge.solved).to.equal(false)
|
|
})
|
|
}
|
|
})
|
|
})
|