diff --git a/test.js b/test.js
new file mode 100644
index 0000000..1038619
--- /dev/null
+++ b/test.js
@@ -0,0 +1,320 @@
+/*
+ * Copyright (c) 2014-2020 Bjoern Kimminich.
+ * SPDX-License-Identifier: MIT
+ */
+
+const sinon = require('sinon')
+const chai = require('chai')
+const sinonChai = require('sinon-chai')
+const expect = chai.expect
+chai.use(sinonChai)
+const cache = require('../../data/datacache')
+const insecurity = require('../../lib/insecurity')
+const config = require('config')
+const utils = require('../../lib/utils')
+
+describe('verify', () => {
+ const verify = require('../../routes/verify')
+ const challenges = require('../../data/datacache').challenges
+
+ beforeEach(() => {
+ this.req = { body: {}, headers: {} }
+ this.res = { json: sinon.spy() }
+ this.next = sinon.spy()
+ this.save = () => ({
+ then () { }
+ })
+ })
+
+ describe('"forgedFeedbackChallenge"', () => {
+ beforeEach(() => {
+ insecurity.authenticatedUsers.put('token12345', {
+ data: {
+ id: 42,
+ email: 'test@juice-sh.op'
+ }
+ })
+ challenges.forgedFeedbackChallenge = { solved: false, save: this.save }
+ })
+
+ it('is not solved when an authenticated user passes his own ID when writing feedback', () => {
+ this.req.body.UserId = 42
+ this.req.headers = { authorization: 'Bearer token12345' }
+
+ verify.forgedFeedbackChallenge()(this.req, this.res, this.next)
+
+ expect(challenges.forgedFeedbackChallenge.solved).to.equal(false)
+ })
+
+ it('is not solved when an authenticated user passes no ID when writing feedback', () => {
+ this.req.body.UserId = undefined
+ this.req.headers = { authorization: 'Bearer token12345' }
+
+ verify.forgedFeedbackChallenge()(this.req, this.res, this.next)
+
+ expect(challenges.forgedFeedbackChallenge.solved).to.equal(false)
+ })
+
+ it('is solved when an authenticated user passes someone elses ID when writing feedback', () => {
+ this.req.body.UserId = 1
+ this.req.headers = { authorization: 'Bearer token12345' }
+
+ verify.forgedFeedbackChallenge()(this.req, this.res, this.next)
+
+ expect(challenges.forgedFeedbackChallenge.solved).to.equal(true)
+ })
+
+ it('is solved when an unauthenticated user passes someones ID when writing feedback', () => {
+ this.req.body.UserId = 1
+ this.req.headers = {}
+
+ verify.forgedFeedbackChallenge()(this.req, this.res, this.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: this.save }
+ this.req.url = 'http://juice-sh.op/public/images/padding/1px.png'
+
+ verify.accessControlChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.scoreBoardChallenge.solved).to.equal(true)
+ })
+
+ it('"adminSectionChallenge" is solved when the 19px.png transpixel is requested', () => {
+ challenges.adminSectionChallenge = { solved: false, save: this.save }
+ this.req.url = 'http://juice-sh.op/public/images/padding/19px.png'
+
+ verify.accessControlChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.adminSectionChallenge.solved).to.equal(true)
+ })
+
+ it('"tokenSaleChallenge" is solved when the 56px.png transpixel is requested', () => {
+ challenges.tokenSaleChallenge = { solved: false, save: this.save }
+ this.req.url = 'http://juice-sh.op/public/images/padding/56px.png'
+
+ verify.accessControlChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.tokenSaleChallenge.solved).to.equal(true)
+ })
+
+ it('"extraLanguageChallenge" is solved when the Klingon translation file is requested', () => {
+ challenges.extraLanguageChallenge = { solved: false, save: this.save }
+ this.req.url = 'http://juice-sh.op/public/i18n/tlh_AA.json'
+
+ verify.accessControlChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.extraLanguageChallenge.solved).to.equal(true)
+ })
+
+ it('"retrieveBlueprintChallenge" is solved when the blueprint file is requested', () => {
+ challenges.retrieveBlueprintChallenge = { solved: false, save: this.save }
+ cache.retrieveBlueprintChallengeFile = 'test.dxf'
+ this.req.url = 'http://juice-sh.op/public/images/products/test.dxf'
+
+ verify.accessControlChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.retrieveBlueprintChallenge.solved).to.equal(true)
+ })
+
+ it('"missingEncodingChallenge" is solved when the crazy cat photo is requested', () => {
+ challenges.missingEncodingChallenge = { solved: false, save: this.save }
+ this.req.url = 'http://juice-sh.op/public/images/uploads/%F0%9F%98%BC-%23zatschi-%23whoneedsfourlegs-1572600969477.jpg'
+
+ verify.accessControlChallenges()(this.req, this.res, this.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: this.save }
+ this.req.url = 'http://juice-sh.op/support/logs/access.log.2019-01-15'
+
+ verify.accessControlChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.accessLogDisclosureChallenge.solved).to.equal(true)
+ })
+ })
+
+ describe('"errorHandlingChallenge"', () => {
+ beforeEach(() => {
+ challenges.errorHandlingChallenge = { solved: false, save: this.save }
+ })
+
+ it('is solved when an error occurs on a this.response with OK 200 status code', () => {
+ this.res.statusCode = 200
+ this.err = new Error()
+
+ verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next)
+
+ expect(challenges.errorHandlingChallenge.solved).to.equal(true)
+ })
+
+ describe('is solved when an error occurs on a this.response with error', () => {
+ const httpStatus = [402, 403, 404, 500]
+ httpStatus.forEach(statusCode => {
+ it(statusCode + ' status code', () => {
+ this.res.statusCode = statusCode
+ this.err = new Error()
+
+ verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next)
+
+ expect(challenges.errorHandlingChallenge.solved).to.equal(true)
+ })
+ })
+ })
+
+ it('is not solved when no error occurs on a this.response with OK 200 status code', () => {
+ this.res.statusCode = 200
+ this.err = undefined
+
+ verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next)
+
+ expect(challenges.errorHandlingChallenge.solved).to.equal(false)
+ })
+
+ describe('is not solved when no error occurs on a this.response with error', () => {
+ const httpStatus = [401, 402, 404, 500]
+ httpStatus.forEach(statusCode => {
+ it(statusCode + ' status code', () => {
+ this.res.statusCode = statusCode
+ this.err = undefined
+
+ verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next)
+
+ expect(challenges.errorHandlingChallenge.solved).to.equal(false)
+ })
+ })
+ })
+
+ it('should pass occured error on to this.next route', () => {
+ this.res.statusCode = 500
+ this.err = new Error()
+
+ verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next)
+
+ expect(this.next).to.have.been.calledWith(this.err)
+ })
+ })
+
+ describe('databaseRelatedChallenges', () => {
+ describe('"changeProductChallenge"', () => {
+ const products = require('../../data/datacache').products
+
+ beforeEach(() => {
+ challenges.changeProductChallenge = { solved: false, save: this.save }
+ products.osaft = { reload () { return { then (cb) { cb() } } } }
+ })
+
+ it(`is solved when the link in the O-Saft product goes to ${config.get('challenges.overwriteUrlForProductTamperingChallenge')}`, () => {
+ products.osaft.description = `O-Saft, yeah! More...`
+
+ verify.databaseRelatedChallenges()(this.req, this.res, this.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! More...'
+
+ verify.databaseRelatedChallenges()(this.req, this.res, this.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.products) {
+ if (product.urlForProductTamperingChallenge !== undefined) {
+ urlForProductTamperingChallenge = product.urlForProductTamperingChallenge
+ break
+ }
+ }
+ products.osaft.description = `Vanilla O-Saft! More...`
+
+ verify.databaseRelatedChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.changeProductChallenge.solved).to.equal(false)
+ })
+ })
+ })
+
+ describe('jwtChallenges', () => {
+ beforeEach(() => {
+ challenges.jwtUnsignedChallenge = { solved: false, save: this.save }
+ challenges.jwtForgedChallenge = { solved: false, save: this.save }
+ })
+
+ it('"jwtUnsignedChallenge" is solved when forged unsigned token has email jwtn3d@juice-sh.op in the payload', () => {
+ /*
+ Header: { "alg": "none", "typ": "JWT" }
+ Payload: { "data": { "email": "jwtn3d@juice-sh.op" }, "iat": 1508639612, "exp": 9999999999 }
+ */
+ this.req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQGp1aWNlLXNoLm9wIn0sImlhdCI6MTUwODYzOTYxMiwiZXhwIjo5OTk5OTk5OTk5fQ.' }
+
+ verify.jwtChallenges()(this.req, this.res, this.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 }
+ */
+ this.req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQCJ9LCJpYXQiOjE1MDg2Mzk2MTIsImV4cCI6OTk5OTk5OTk5OX0.' }
+
+ verify.jwtChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.jwtUnsignedChallenge.solved).to.equal(true)
+ })
+
+ it('"jwtUnsignedChallenge" is not solved via regularly signed token even with email jwtn3d@juice-sh.op in the payload', () => {
+ const token = insecurity.authorize({ data: { email: 'jwtn3d@juice-sh.op' } })
+ this.req.headers = { authorization: 'Bearer ' + token }
+
+ verify.jwtChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.jwtForgedChallenge.solved).to.equal(false)
+ })
+
+ if (!utils.disableOnWindowsEnv()) {
+ it('"jwtForgedChallenge" is solved when forged token HMAC-signed with public RSA-key has email rsa_lord@juice-sh.op in the payload', () => {
+ /*
+ Header: { "alg": "HS256", "typ": "JWT" }
+ Payload: { "data": { "email": "rsa_lord@juice-sh.op" }, "iat": 1508639612, "exp": 9999999999 }
+ */
+ this.req.headers = { authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAanVpY2Utc2gub3AifSwiaWF0IjoxNTgyMjIxNTc1fQ.ycFwtqh4ht4Pq9K5rhiPPY256F9YCTIecd4FHFuSEAg' }
+
+ verify.jwtChallenges()(this.req, this.res, this.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 }
+ */
+ this.req.headers = { authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAIn0sImlhdCI6MTU4MjIyMTY3NX0.50f6VAIQk2Uzpf3sgH-1JVrrTuwudonm2DKn2ec7Tg8' }
+
+ verify.jwtChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.jwtForgedChallenge.solved).to.equal(true)
+ })
+
+ it('"jwtForgedChallenge" is not solved when token regularly signed with private RSA-key has email rsa_lord@juice-sh.op in the payload', () => {
+ const token = insecurity.authorize({ data: { email: 'rsa_lord@juice-sh.op' } })
+ this.req.headers = { authorization: 'Bearer ' + token }
+
+ verify.jwtChallenges()(this.req, this.res, this.next)
+
+ expect(challenges.jwtForgedChallenge.solved).to.equal(false)
+ })
+ }
+ })
+})
\ No newline at end of file