Skip to content

Commit

Permalink
Refactor safetyMode setting
Browse files Browse the repository at this point in the history
  • Loading branch information
J12934 committed Mar 10, 2024
1 parent 0e19095 commit d7a4dac
Show file tree
Hide file tree
Showing 20 changed files with 132 additions and 73 deletions.
9 changes: 6 additions & 3 deletions data/datacreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ async function createChallenges () {

await Promise.all(
challenges.map(async ({ name, category, description, difficulty, hint, hintUrl, mitigationUrl, key, disabledEnv, tutorial, tags }) => {
const effectiveDisabledEnv = utils.determineDisabledEnv(disabledEnv)
// todo(@J12934) change this to use a proper challenge model or something
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const { enabled: isChallengeEnabled, disabledBecause } = utils.getChallengeEnablementStatus({ disabledEnv: disabledEnv?.join(';') ?? '' } as ChallengeModel)
description = description.replace('juice-sh.op', config.get<string>('application.domain'))
description = description.replace('&lt;iframe width=&quot;100%&quot; height=&quot;166&quot; scrolling=&quot;no&quot; frameborder=&quot;no&quot; allow=&quot;autoplay&quot; src=&quot;https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/771984076&amp;color=%23ff5500&amp;auto_play=true&amp;hide_related=false&amp;show_comments=true&amp;show_user=true&amp;show_reposts=false&amp;show_teaser=true&quot;&gt;&lt;/iframe&gt;', entities.encode(config.get('challenges.xssBonusPayload')))
hint = hint.replace(/OWASP Juice Shop's/, `${config.get<string>('application.name')}'s`)
Expand All @@ -77,13 +79,14 @@ async function createChallenges () {
name,
category,
tags: (tags != null) ? tags.join(',') : undefined,
description: effectiveDisabledEnv ? (description + ' <em>(This challenge is <strong>' + (config.get('challenges.safetyMode') ? 'potentially harmful' : 'not available') + '</strong> on ' + effectiveDisabledEnv + '!)</em>') : description,
// todo(@J12934) currently missing the 'not available' text. Needs changes to the model and utils functions
description: isChallengeEnabled ? description : (description + ' <em>(This challenge is <strong>potentially harmful</strong> on ' + disabledBecause + '!)</em>'),
difficulty,
solved: false,
hint: showHints ? hint : null,
hintUrl: showHints ? hintUrl : null,
mitigationUrl: showMitigations ? mitigationUrl : null,
disabledEnv: effectiveDisabledEnv,
disabledEnv: disabledBecause,
tutorialOrder: (tutorial != null) ? tutorial.order : null,
codingChallengeStatus: 0
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
<i class="env-icon" *ngIf="disabledBecauseOfEnv !== 'Windows'" [ngClass]="'icon-' + disabledBecauseOfEnv.toString().toLowerCase()"></i>
<i class="env-icon" *ngIf="disabledBecauseOfEnv === 'Windows'" [ngClass]="'fab fa-' + disabledBecauseOfEnv.toString().toLowerCase()"></i>
</ng-container>
<span *ngIf="disabledBecauseOfEnv !=='safetyMode'" warning-text [innerHTML]="'INFO_DISABLED_CHALLENGES' | translate: {num: numberOfDisabledChallenges, env: disabledBecauseOfEnv}">
</span>
<span *ngIf="disabledBecauseOfEnv ==='safetyMode'" warning-text [innerHTML]="'INFO_DISABLED_CHALLENGES' | translate: {num: numberOfDisabledChallenges-numberOfDisabledChallengesOnWindows,env:'safety mode being turned on'}"></span>

<span *ngIf="disabledBecauseOfEnv !=='Safety Mode'" warning-text [innerHTML]="'INFO_DISABLED_CHALLENGES' | translate: {num: numberOfDisabledChallenges, env: disabledBecauseOfEnv}"></span>
<span *ngIf="disabledBecauseOfEnv ==='Safety Mode'" warning-text [innerHTML]="'INFO_DISABLED_CHALLENGES' | translate: {num: numberOfDisabledChallenges,env:'safety mode being turned on'}"></span>

<button warning-action mat-button color="accent" (click)="toggleShowDisabledChallenges()">
<ng-container *ngIf="filterSetting.showDisabledChallenges == false">
{{ 'SHOW_DISABLED_CHALLENGES' | translate }}
Expand Down
87 changes: 42 additions & 45 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import jsSHA from 'jssha'
import download from 'download'
import crypto from 'crypto'
import clarinet from 'clarinet'
import type { Challenge } from 'data/types'

import isHeroku from './is-heroku'
import isDocker from './is-docker'
import isWindows from './is-windows'
import isHeroku from './is-heroku'

// import isGitpod from 'is-gitpod') // FIXME Roll back to this when https://github.com/dword-design/is-gitpod/issues/94 is resolve
const isGitpod = () => false

Expand Down Expand Up @@ -143,57 +145,52 @@ export const randomHexString = (length: number): string => {
return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length)
}

export const disableOnContainerEnv = () => {
if (config.get('challenges.safetyMode') === 'enabled') {
return true
} else if (config.get('challenges.safetyMode') === 'disabled') {
return false
} else if (config.get('challenges.safetyMode') === 'auto') {
return (isDocker() || isGitpod() || isHeroku())
}
export interface ChallengeEnablementStatus {
enabled: boolean
disabledBecause: string | null
}

export const disableOnWindowsEnv = (): boolean => {
return isWindows()
}
export const safetyModeEnabled = (disabledEnv: string | string[] | undefined) => {
if (disabledEnv != null && (disabledEnv === 'Docker' || disabledEnv?.includes('Docker'))) {
return 'safetyMode'
} else if (disabledEnv != null && (disabledEnv === 'Heroku' || disabledEnv?.includes('Heroku'))) {
return 'safetyMode'
} else if (disabledEnv != null && (disabledEnv === 'Gitpod' || disabledEnv?.includes('Gitpod'))) {
return 'safetyMode'
} else if (isWindows()) {
return disabledEnv != null && (disabledEnv === 'Windows' || disabledEnv?.includes('Windows')) ? 'Windows' : null
type SafetyModeSetting = 'enabled' | 'disabled' | 'auto'

type isEnvironmentFunction = () => boolean

export function getChallengeEnablementStatus (challenge: Challenge,
safetyModeSetting: SafetyModeSetting = config.get<SafetyModeSetting>('challenges.safetyMode'),
isEnvironmentFunctions: {
isDocker: isEnvironmentFunction
isHeroku: isEnvironmentFunction
isWindows: isEnvironmentFunction
isGitpod: isEnvironmentFunction
} = { isDocker, isHeroku, isWindows, isGitpod }): ChallengeEnablementStatus {
if (!challenge?.disabledEnv) {
return { enabled: true, disabledBecause: null }
}
return null
}
export const safetyModeTurnedAuto = (disabledEnv: string | string[] | undefined) => {
if (isDocker()) {
return disabledEnv != null && (disabledEnv === 'Docker' || disabledEnv?.includes('Docker')) ? 'Docker' : null
} else if (isHeroku()) {
return disabledEnv != null && (disabledEnv === 'Heroku' || disabledEnv?.includes('Heroku')) ? 'Heroku' : null
} else if (isWindows()) {
return disabledEnv != null && (disabledEnv === 'Windows' || disabledEnv?.includes('Windows')) ? 'Windows' : null
} else if (isGitpod()) {
return disabledEnv && (disabledEnv === 'Gitpod' || disabledEnv.includes('Gitpod')) ? 'Gitpod' : null

if (safetyModeSetting === 'disabled') {
return { enabled: true, disabledBecause: null }
}
}

export const safetyModeDisabled = (disabledEnv: string | string[] | undefined) => {
if (isWindows()) {
return disabledEnv != null && (disabledEnv === 'Windows' || disabledEnv?.includes('Windows')) ? 'Windows' : null
if (challenge.disabledEnv?.includes('Docker') && isEnvironmentFunctions.isDocker()) {
return { enabled: false, disabledBecause: 'Docker' }
}
return null
}
export const determineDisabledEnv = (disabledEnv: string | string[] | undefined) => {
if (config.get('challenges.safetyMode') === 'disabled') {
return safetyModeDisabled(disabledEnv)
} else if (config.get('challenges.safetyMode') === 'enabled') {
return safetyModeEnabled(disabledEnv)
} else if (config.get('challenges.safetyMode') === 'auto') {
return safetyModeTurnedAuto(disabledEnv)
if (challenge.disabledEnv?.includes('Heroku') && isEnvironmentFunctions.isHeroku()) {
return { enabled: false, disabledBecause: 'Heroku' }
}
if (challenge.disabledEnv?.includes('Windows') && isEnvironmentFunctions.isWindows()) {
return { enabled: false, disabledBecause: 'Windows' }
}
if (challenge.disabledEnv?.includes('Gitpod') && isEnvironmentFunctions.isGitpod()) {
return { enabled: false, disabledBecause: 'Gitpod' }
}
if (challenge.disabledEnv && safetyModeSetting === 'enabled') {
return { enabled: false, disabledBecause: 'Safety Mode' }
}

return { enabled: true, disabledBecause: null }
}
export function isChallengeEnabled (challenge: Challenge): boolean {
const { enabled } = getChallengeEnablementStatus(challenge)
return enabled
}

export const parseJsonCustom = (jsonString: string) => {
Expand Down
2 changes: 1 addition & 1 deletion models/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const FeedbackModelInit = (sequelize: Sequelize) => {
type: DataTypes.STRING,
set (comment: string) {
let sanitizedComment: string
if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.persistedXssFeedbackChallenge)) {
sanitizedComment = security.sanitizeHtml(comment)
challengeUtils.solveIf(challenges.persistedXssFeedbackChallenge, () => {
return utils.contains(
Expand Down
2 changes: 1 addition & 1 deletion models/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const ProductModelInit = (sequelize: Sequelize) => {
description: {
type: DataTypes.STRING,
set (description: string) {
if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.restfulXssChallenge)) {
challengeUtils.solveIf(challenges.restfulXssChallenge, () => {
return utils.contains(
description,
Expand Down
4 changes: 2 additions & 2 deletions models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const UserModelInit = (sequelize: Sequelize) => { // vuln-code-snippet start wea
type: DataTypes.STRING,
defaultValue: '',
set (username: string) {
if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.persistedXssUserChallenge)) {
username = security.sanitizeLegacy(username)
} else {
username = security.sanitizeSecure(username)
Expand All @@ -58,7 +58,7 @@ const UserModelInit = (sequelize: Sequelize) => { // vuln-code-snippet start wea
type: DataTypes.STRING,
unique: true,
set (email: string) {
if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.persistedXssUserChallenge)) {
challengeUtils.solveIf(challenges.persistedXssUserChallenge, () => {
return utils.contains(
email,
Expand Down
2 changes: 1 addition & 1 deletion routes/b2bOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const safeEval = require('notevil')

module.exports = function b2bOrder () {
return ({ body }: Request, res: Response, next: NextFunction) => {
if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.rceChallenge) || utils.isChallengeEnabled(challenges.rceOccupyChallenge)) {
const orderLinesData = body.orderLinesData || ''
try {
const sandbox = { safeEval, orderLinesData }
Expand Down
4 changes: 2 additions & 2 deletions routes/fileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function ensureFileIsPassed ({ file }: Request, res: Response, next: NextFunctio

function handleZipFileUpload ({ file }: Request, res: Response, next: NextFunction) {
if (utils.endsWith(file?.originalname.toLowerCase(), '.zip')) {
if (((file?.buffer) != null) && !utils.disableOnContainerEnv()) {
if (((file?.buffer) != null) && utils.isChallengeEnabled(challenges.fileWriteChallenge)) {
const buffer = file.buffer
const filename = file.originalname.toLowerCase()
const tempFile = path.join(os.tmpdir(), filename)
Expand Down Expand Up @@ -72,7 +72,7 @@ function checkFileType ({ file }: Request, res: Response, next: NextFunction) {
function handleXmlUpload ({ file }: Request, res: Response, next: NextFunction) {
if (utils.endsWith(file?.originalname.toLowerCase(), '.xml')) {
challengeUtils.solveIf(challenges.deprecatedInterfaceChallenge, () => { return true })
if (((file?.buffer) != null) && !utils.disableOnContainerEnv()) { // XXE attacks in Docker/Heroku containers regularly cause "segfault" crashes
if (((file?.buffer) != null) && utils.isChallengeEnabled(challenges.deprecatedInterfaceChallenge)) { // XXE attacks in Docker/Heroku containers regularly cause "segfault" crashes
const data = file.buffer.toString()
try {
const sandbox = { libxml, data }
Expand Down
2 changes: 1 addition & 1 deletion routes/saveLoginIp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = function saveLoginIp () {
const loggedInUser = security.authenticatedUsers.from(req)
if (loggedInUser !== undefined) {
let lastLoginIp = req.headers['true-client-ip']
if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.httpHeaderXssChallenge)) {
challengeUtils.solveIf(challenges.httpHeaderXssChallenge, () => { return lastLoginIp === '<iframe src="javascript:alert(`xss`)">' })
} else {
lastLoginIp = security.sanitizeSecure(lastLoginIp)
Expand Down
2 changes: 1 addition & 1 deletion routes/showProductReviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ global.sleep = (time: number) => {

module.exports = function productReviews () {
return (req: Request, res: Response, next: NextFunction) => {
const id = utils.disableOnContainerEnv() ? Number(req.params.id) : req.params.id
const id = !utils.isChallengeEnabled(challenges.noSqlCommandChallenge) ? Number(req.params.id) : req.params.id

// Measure how long the query takes, to check if there was a nosql dos attack
const t0 = new Date().getTime()
Expand Down
2 changes: 1 addition & 1 deletion routes/trackOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { challenges } from '../data/datacache'

module.exports = function trackOrder () {
return (req: Request, res: Response) => {
const id = utils.disableOnContainerEnv() ? String(req.params.id).replace(/[^\w-]+/g, '') : req.params.id
const id = !utils.isChallengeEnabled(challenges.reflectedXssChallenge) ? String(req.params.id).replace(/[^\w-]+/g, '') : req.params.id

challengeUtils.solveIf(challenges.reflectedXssChallenge, () => { return utils.contains(id, '<iframe src="javascript:alert(`xss`)">') })
db.ordersCollection.find({ $where: `this.orderId === '${id}'` }).then((order: any) => {
Expand Down
2 changes: 1 addition & 1 deletion routes/userProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = function getUserProfile () {
UserModel.findByPk(loggedInUser.data.id).then((user: UserModel | null) => {
let template = buf.toString()
let username = user?.username
if (username?.match(/#{(.*)}/) !== null && !utils.disableOnContainerEnv()) {
if (username?.match(/#{(.*)}/) !== null && utils.isChallengeEnabled(challenges.usernameXssChallenge)) {
req.app.locals.abused_ssti_bug = true
const code = username?.substring(2, username.length - 1)
try {
Expand Down
2 changes: 1 addition & 1 deletion routes/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ 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)) {
if (utils.isChallengeEnabled(challenges.jwtForgedChallenge) && challengeUtils.notSolved(challenges.jwtForgedChallenge)) {
jwtChallenge(challenges.jwtForgedChallenge, req, 'HS256', /rsa_lord@/)
}
next()
Expand Down
3 changes: 2 additions & 1 deletion test/api/b2bOrderSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: MIT
*/

import { challenges } from '../../data/datacache'
import frisby = require('frisby')
const Joi = frisby.Joi
const utils = require('../../lib/utils')
Expand All @@ -13,7 +14,7 @@ const API_URL = 'http://localhost:3000/b2b/v2/orders'
const authHeader = { Authorization: 'Bearer ' + security.authorize(), 'content-type': 'application/json' }

describe('/b2b/v2/orders', () => {
if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.rceChallenge) || utils.isChallengeEnabled(challenges.rceOccupyChallenge)) {
it('POST endless loop exploit in "orderLinesData" will raise explicit error', () => {
return frisby.post(API_URL, {
headers: authHeader,
Expand Down
4 changes: 2 additions & 2 deletions test/api/feedbackApiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
* SPDX-License-Identifier: MIT
*/

import { challenges } from '../../data/datacache'
import frisby = require('frisby')
import { expect } from '@jest/globals'
const Joi = frisby.Joi
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('/api/Feedbacks', () => {
})
})

if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.persistedXssFeedbackChallenge)) {
it('POST fails to sanitize masked XSS-attack by not applying sanitization recursively', () => {
return frisby.get(REST_URL + '/captcha')
.expect('status', 200)
Expand Down
5 changes: 3 additions & 2 deletions test/api/fileUploadSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* SPDX-License-Identifier: MIT
*/

import frisby = require('frisby')
import { challenges } from '../../data/datacache'
import { expect } from '@jest/globals'
import frisby = require('frisby')
import path from 'path'
const fs = require('fs')
const utils = require('../../lib/utils')
Expand Down Expand Up @@ -62,7 +63,7 @@ describe('/file-upload', () => {
.expect('status', 410)
})

if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.xxeFileDisclosureChallenge) || utils.isChallengeEnabled(challenges.xxeDosChallenge)) {
it('POST file type XML with XXE attack against Windows', () => {
const file = path.resolve(__dirname, '../files/xxeForWindows.xml')
const form = frisby.formData()
Expand Down
5 changes: 3 additions & 2 deletions test/api/productApiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* SPDX-License-Identifier: MIT
*/

import frisby = require('frisby')
import type { Product as ProductConfig } from '../../lib/config.types'
import { challenges } from '../../data/datacache'
import config from 'config'
import frisby = require('frisby')
const Joi = frisby.Joi
const utils = require('../../lib/utils')
const security = require('../../lib/insecurity')
Expand Down Expand Up @@ -43,7 +44,7 @@ describe('/api/Products', () => {
.expect('status', 401)
})

if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.restfulXssChallenge)) {
it('POST new product does not filter XSS attacks', () => {
return frisby.post(API_URL + '/Products', {
headers: authHeader,
Expand Down
5 changes: 3 additions & 2 deletions test/api/userApiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* SPDX-License-Identifier: MIT
*/

import frisby = require('frisby')
import { challenges } from '../../data/datacache'
import { expect } from '@jest/globals'
import frisby = require('frisby')
const Joi = frisby.Joi
const utils = require('../../lib/utils')
const security = require('../../lib/insecurity')
Expand Down Expand Up @@ -190,7 +191,7 @@ describe('/api/Users', () => {
})
})

if (!utils.disableOnContainerEnv()) {
if (utils.isChallengeEnabled(challenges.usernameXssChallenge)) {
it('POST new user with XSS attack in email address', () => {
return frisby.post(`${API_URL}/Users`, {
headers: jsonHeader,
Expand Down
Loading

0 comments on commit d7a4dac

Please sign in to comment.