diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 329469c5d4e..00000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: '{build}' -init: - - 'git config --global core.autocrlf true' -branches: - except: - - gh-pages - - l10n_develop -skip_commits: - files: - - '*.md' - - LICENSE - - .travis.yml -environment: - matrix: - - nodejs_version: 10 - - nodejs_version: 8 - - nodejs_version: 9 - - nodejs_version: 11 -platform: - - x64 -install: - - ps: 'Install-Product node $env:nodejs_version $env:platform' - - 'node --version && npm --version' - - 'if exist node_modules rd /Q /S node_modules' - - 'if exist frontend\node_modules rd /Q /S frontend\node_modules' - - 'npm install --production' - - 'npm run package' -build: off -test: off -matrix: - fast_finish: true -artifacts: - - - path: 'dist\*' -deploy: - - - provider: GitHub - draft: true - auth_token: - secure: bFkucwU1Zoh4EgzKmTAwONzQxuWPWrPGa+yXgadKQRd2jz5JPDZEw1f1vz2r+7i1 - on: - appveyor_repo_tag: true -notifications: - - - provider: Slack - incoming_webhook: - secure: KzO8e88B0LKqAI0BQM6lNhCIn9rxAava3AcdVJDyTw420OLIAlK+qzzbLXaR0jSH9zIJz9zu0iGS1iaqu9Co+6owYUrHJlBGrUZ/lZNCsDo= - on_build_success: false - on_build_failure: false - on_build_status_changed: true diff --git a/.codeclimate.yml b/.codeclimate.yml index e392ef5668e..789490203ba 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,17 +1,16 @@ -engines: - eslint: - enabled: true - csslint: - enabled: true +version: "2" +plugins: fixme: enabled: true -ratings: - paths: - - '**.ts' - - '**.js' - - '**.css' - - '**.scss' + duplication: + enabled: true checks: + file-lines: + config: + threshold: 300 + method-lines: + config: + threshold: 30 method-complexity: config: threshold: 7 @@ -19,8 +18,11 @@ checks: enabled: false identical-code: enabled: false -exclude_paths: - - 'data/datacreator.js' - - 'frontend/src/assets/private/**/*' - - 'Gruntfile.js' +exclude_patterns: - '**/*conf.js' + - 'Gruntfile.js' + - 'data/datacreator.ts' + - 'frontend/src/hacking-instructor/**/*.ts' + - 'frontend/src/assets/private/*.js' + - 'lib/logger.ts' + - 'data/static/codefixes/**' diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 00000000000..ed8eba07571 --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,31 @@ +version: 1 +update_configs: + - package_manager: "javascript" + directory: "/" + update_schedule: "live" + target_branch: "develop" + default_reviewers: + - "bkimminich" + default_labels: + - "dependencies" + ignored_updates: + - match: + dependency_name: "express-jwt" + version_requirement: "0.1.3" + - match: + dependency_name: "sanitize-html" + version_requirement: "1.4.2" + - match: + dependency_name: "unzipper" + version_requirement: "0.9.15" + - match: + dependency_name: "jsonwebtoken" + version_requirement: "0.4.0" + - package_manager: "javascript" + directory: "/frontend" + update_schedule: "live" + target_branch: "develop" + default_reviewers: + - "bkimminich" + default_labels: + - "dependencies" diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 00000000000..d7ae27d0081 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,14 @@ +{ + "extensions": [ + "eg2.vscode-npm-script", + "angular.ng-template", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint" + ], + "settings": { + "eslint.workingDirectories": [ + { "mode": "auto" } + ] + }, + "postCreateCommand": "export NG_CLI_ANALYTICS=ci && npm i -g @angular/cli && npm install" +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 91c62ada2a3..5f114213be6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,15 @@ .git/ +monitoring/ node_modules/ screenshots/ test/ -build/ +build/reports/ dist/ vagrant/ +logs/ +Dockerfile +.npmrc + +# Pattern is *not covered* by node_modules/ above no matter what IntelliJ says! frontend/node_modules/ -Dockerfile \ No newline at end of file +frontend/dist/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000..302ef9949fd --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +module.exports = { + extends: 'standard-with-typescript', + env: { + browser: true, + node: true, + jasmine: true, + mocha: true, + jest: true + }, + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly' + }, + parserOptions: { + ecmaVersion: 2018, + project: './tsconfig.json' + }, + ignorePatterns: [ + 'app/private/**', + 'vagrant/**', + 'frontend/**', + 'data/static/codefixes/**', + 'dist/**' + ], + overrides: [ + { + files: ['**/*.ts'], + parser: '@typescript-eslint/parser', + rules: { + 'no-void': 'off', // conflicting with recommendation from @typescript-eslint/no-floating-promises + // FIXME warnings below this line need to be checked and fixed. Line end comments below are number of findings per rule on 02.05.2022 + '@typescript-eslint/no-misused-promises': 'off', // 1 + '@typescript-eslint/explicit-function-return-type': 'off', // 197 + '@typescript-eslint/restrict-plus-operands': 'off', // 250 + '@typescript-eslint/strict-boolean-expressions': 'off', // 337 + '@typescript-eslint/restrict-template-expressions': 'off', // 395 + '@typescript-eslint/no-var-requires': 'off' // 509 + } + } + ] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..7afa2d618ca --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +/vagrant/ @wurstbrot diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..94dbba403be --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: https://sponsor.owasp-juice.shop +github: OWASP diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000000..7f57b910d83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,63 @@ +--- +name: "\U0001F41BBug report" +about: Report a bug in OWASP Juice Shop +title: '[🐛] ' +labels: bug +assignees: '' + +--- + + + +# :bug: Bug report + +## Description + + +A clear and concise description of the problem... + + +### Is this a regression? + + + +Yes, the previous version in which this bug was not present was: `x.y.z` + + +## :microscope: Minimal Reproduction + + + + +## :fire: Exception or Error + +
+
+
+
+
+
+
+## :deciduous_tree: Your Environment
+
+
+
+
+
+
+
+
+### Additional Information
+
+
+
diff --git a/.github/ISSUE_TEMPLATE/challenge-idea.md b/.github/ISSUE_TEMPLATE/challenge-idea.md
new file mode 100644
index 00000000000..c1317a02b91
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/challenge-idea.md
@@ -0,0 +1,43 @@
+---
+name: "⭐Challenge idea"
+about: Idea for a new hacking challenge in OWASP Juice Shop
+title: '[⭐] '
+labels: challenge
+assignees: ''
+
+---
+
+
+
+# :star: Challenge idea
+
+### Description
+
+ A clear and concise description of the new hacking challenge and why the Juice Shop needs it...
+
+### Underlying vulnerability/ies
+
+ Security vulnerabilities or design flaws this challenge will be based on. Optimally include CWE, OWASP or similar references.
+
+### Expected difficulty
+
+
+
+
+| :heavy_check_mark: / :x: | Difficulty |
+|:------------------------:|:-------------------------------------|
+| :grey_question: | :star: |
+| :grey_question: | :star::star: |
+| :grey_question: | :star::star::star: |
+| :grey_question: | :star::star::star::star: |
+| :grey_question: | :star::star::star::star::star: |
+| :grey_question: | :star::star::star::star::star::star: |
+
+### Possible attack flow
+
+ Have you considered how the challenge could be exploited by the attacker?
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000000..b51d1946571
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,4 @@
+contact_links:
+ - name: ❓Support request
+ url: https://gitter.im/bkimminich/juice-shop
+ about: Questions and requests for support
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100644
index 00000000000..72ea7b6a555
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -0,0 +1,31 @@
+---
+name: "\U0001F680Feature request"
+about: Suggest a feature for OWASP Juice Shop
+title: '[🚀] '
+labels: feature
+assignees: ''
+
+---
+
+
+
+# :rocket: Feature request
+
+### Description
+
+ A clear and concise description of the problem or missing capability...
+
+
+### Solution ideas
+
+ If you have a solution in mind, please describe it.
+
+
+### Possible alternatives
+
+ Have you considered any alternative solutions or workarounds?
\ No newline at end of file
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000000..a9e46ed8cd7
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,23 @@
+
+
+### Description
+
+
+A clear and concise summary of the change and which issue (if any) it fixes. Should also include relevant motivation and context.
+
+Resolved or fixed issue:
+
+### Affirmation
+
+- [ ] My code follows the [CONTRIBUTING.md](https://github.com/juice-shop/juice-shop/blob/master/CONTRIBUTING.md) guidelines
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index ce918b1b719..00000000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,14 +0,0 @@
----
-daysUntilStale: 84
-daysUntilClose: 14
-exemptLabels:
- - bounty
- - challenge
- - enhancement
- - technical debt
-staleLabel: stale
-markComment: >
- This issue has been automatically marked as stale because it has not had
- recent activity. It will be _closed in two weeks_ if no further activity occurs.
- :heart: Thank you for your contributions to [OWASP Juice Shop](http://owasp-juice.shop)!
-closeComment: false
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000000..8641eb638f6
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,387 @@
+name: "CI/CD Pipeline"
+on:
+ push:
+ branches-ignore:
+ - l10n_develop
+ - gh-pages
+ paths-ignore:
+ - '*.md'
+ - 'LICENSE'
+ - 'monitoring/grafana-dashboard.json'
+ - 'screenshots/**'
+ tags-ignore:
+ - '*'
+ pull_request:
+ paths-ignore:
+ - '*.md'
+ - 'LICENSE'
+ - 'data/static/i18n/*.json'
+ - 'frontend/src/assets/i18n/*.json'
+env:
+ ANGULAR_CLI_VERSION: 13
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js 16"
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: 16
+ - name: "Install CLI tools"
+ run: npm install -g @angular/cli@$ANGULAR_CLI_VERSION
+ - name: "Install application minimalistically"
+ run: |
+ npm install --ignore-scripts
+ cd frontend
+ npm install --ignore-scripts --legacy-peer-deps
+ - name: "Lint source code"
+ run: npm run lint
+ - name: "Lint customization configs"
+ run: >
+ npm run lint:config -- -f ./config/7ms.yml &&
+ npm run lint:config -- -f ./config/addo.yml &&
+ npm run lint:config -- -f ./config/bodgeit.yml &&
+ npm run lint:config -- -f ./config/ctf.yml &&
+ npm run lint:config -- -f ./config/default.yml &&
+ npm run lint:config -- -f ./config/fbctf.yml &&
+ npm run lint:config -- -f ./config/juicebox.yml &&
+ npm run lint:config -- -f ./config/mozilla.yml &&
+ npm run lint:config -- -f ./config/oss.yml &&
+ npm run lint:config -- -f ./config/quiet.yml &&
+ npm run lint:config -- -f ./config/tutorial.yml &&
+ npm run lint:config -- -f ./config/unsafe.yml
+ coding-challenge-rsn:
+ runs-on: windows-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js 16"
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: 16
+ - name: "Install CLI tools"
+ run: npm install -g @angular/cli@$ANGULAR_CLI_VERSION
+ - name: "Install application"
+ run: npm install
+ - name: "Check coding challenges for accidental code discrepancies"
+ run: npm run rsn
+ test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ node-version: [14, 16, 18]
+ steps:
+ - name: "Check out Git repository"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js ${{ matrix.node-version }}"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: "Install CLI tools"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ run: npm install -g @angular/cli@$ANGULAR_CLI_VERSION
+ - name: "Install application"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ run: npm install
+ - name: "Execute unit tests"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: nick-invision/retry@45ba062d357edb3b29c4a94b456b188716f61020 #v2: 2.4.1 available
+ with:
+ timeout_minutes: 15
+ max_attempts: 3
+ command: npm test
+ - name: "Copy unit test coverage data"
+ run: |
+ cp build/reports/coverage/frontend-tests/lcov.info frontend-lcov.info
+ cp build/reports/coverage/server-tests/lcov.info server-lcov.info
+ - name: "Upload unit test coverage data"
+ if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 #v3: v3.0.0 available
+ with:
+ name: unit-test-lcov
+ path: |
+ frontend-lcov.info
+ server-lcov.info
+ api-test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ node-version: [14, 16, 18]
+ steps:
+ - name: "Check out Git repository"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js ${{ matrix.node-version }}"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: "Install CLI tools"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ run: npm install -g @angular/cli@$ANGULAR_CLI_VERSION
+ - name: "Install application"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ run: npm install
+ - name: "Execute integration tests"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: nick-invision/retry@45ba062d357edb3b29c4a94b456b188716f61020 #v2: 2.4.1 available
+ with:
+ timeout_minutes: 5
+ max_attempts: 3
+ command: |
+ if [ "$RUNNER_OS" == "Windows" ]; then
+ set NODE_ENV=test
+ else
+ export NODE_ENV=test
+ fi
+ npm run frisby
+ shell: bash
+ - name: "Copy API test coverage data"
+ run: cp build/reports/coverage/api-tests/lcov.info api-lcov.info
+ - name: "Upload API test coverage data"
+ if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 #v3: v3.0.0 available
+ with:
+ name: api-test-lcov
+ path: |
+ api-lcov.info
+ coverage-report:
+ needs: [test, api-test]
+ runs-on: ubuntu-latest
+ if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push'
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Download unit test coverage data"
+ uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 #v3: v3.0.0 available
+ with:
+ name: unit-test-lcov
+ - name: "Download API test coverage data"
+ uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 #v3: v3.0.0 available
+ with:
+ name: api-test-lcov
+ - name: "Publish coverage to Codeclimate"
+ env:
+ CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
+ run: |
+ curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
+ chmod +x ./cc-test-reporter
+ sed -i s/SF:/SF:frontend\\//g frontend-lcov.info
+ ./cc-test-reporter format-coverage -t lcov -o codeclimate.frontend.json frontend-lcov.info
+ ./cc-test-reporter format-coverage -t lcov -o codeclimate.server.json server-lcov.info
+ ./cc-test-reporter format-coverage -t lcov -o codeclimate.api.json api-lcov.info
+ ./cc-test-reporter sum-coverage codeclimate.*.json -p 3
+ ./cc-test-reporter upload-coverage
+ shell: bash
+ custom-config-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js 16"
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: 16
+ - name: "Install CLI tools"
+ run: npm install -g @angular/cli@$ANGULAR_CLI_VERSION
+ - name: "Install application"
+ if: github.repository == 'juice-shop/juice-shop' || github.repository != 'juice-shop/juice-shop' && matrix.os == 'ubuntu-latest' && matrix.node-version == '16'
+ run: npm install
+ - name: "Execute server tests for each custom configuration"
+ uses: nick-invision/retry@45ba062d357edb3b29c4a94b456b188716f61020 #v2: 2.4.1 available
+ with:
+ timeout_minutes: 10
+ max_attempts: 3
+ command: >
+ NODE_ENV=7ms npm run test:server &&
+ NODE_ENV=addo npm run test:server &&
+ NODE_ENV=bodgeit npm run test:server &&
+ NODE_ENV=ctf npm run test:server &&
+ NODE_ENV=fbctf npm run test:server &&
+ NODE_ENV=juicebox npm run test:server &&
+ NODE_ENV=mozilla npm run test:server &&
+ NODE_ENV=oss npm run test:server &&
+ NODE_ENV=quiet npm run test:server &&
+ NODE_ENV=tutorial npm run test:server &&
+ NODE_ENV=unsafe npm run test:server
+ e2e:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ browser: [chrome] # FIXME Switch back to [chrome, firefox] after debugging extreme flakiness of Firefox on CI/CD
+ fail-fast: false
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js 16"
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: 16
+ - name: "Install CLI tools"
+ run: npm install -g @angular/cli
+ - name: "Install application"
+ run: npm install
+ - name: "Execute end-to-end tests on Ubuntu"
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ uses: cypress-io/github-action@c662a784116e1a26360c4e1fc0a90feedb4b5ed3 #v3.1.0
+ with:
+ install: false
+ browser: ${{ matrix.browser }}
+ start: npm start
+ wait-on: http://localhost:3000
+ record: true
+ group: ${{ matrix.browser }} @ ${{ matrix.os }}
+ env:
+ SOLUTIONS_WEBHOOK: ${{ secrets.E2E_SOLUTIONS_WEBHOOK }}
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: "Execute end-to-end tests on Mac"
+ if: ${{ matrix.os == 'macos-latest' }}
+ uses: cypress-io/github-action@c662a784116e1a26360c4e1fc0a90feedb4b5ed3 #v3.1.0
+ with:
+ install: false
+ browser: ${{ matrix.browser }}
+ start: npm start
+ wait-on: http://localhost:3000
+ record: true
+ group: ${{ matrix.browser }} @ ${{ matrix.os }}
+ env:
+ CYPRESS_CACHE_FOLDER: /Users/runner/Library/Caches/Cypress
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ smoke-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js 16"
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: 16
+ - name: "Install CLI tools"
+ run: |
+ npm install -g @angular/cli@$ANGULAR_CLI_VERSION
+ npm install -g grunt-cli
+ - name: "Set packaging options for Grunt"
+ run: |
+ echo "PCKG_OS_NAME=linux" >> $GITHUB_ENV
+ echo "PCKG_NODE_VERSION=14" >> $GITHUB_ENV
+ echo "PCKG_CPU_ARCH=x64" >> $GITHUB_ENV
+ - name: "Package application"
+ run: |
+ npm install --production
+ npm install -g grunt-cli
+ npm run package:ci
+ - name: "Unpack application archive"
+ run: |
+ cd dist
+ tar -zxf juice-shop-*.tgz
+ - name: "Execute smoke test"
+ run: |
+ cd dist/juice-shop_*
+ npm start &
+ cd ../..
+ chmod +x test/smoke/smoke-test.sh
+ test/smoke/smoke-test.sh http://localhost:3000
+ docker-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Execute smoke test on Docker"
+ run: docker-compose -f docker-compose.test.yml up --exit-code-from sut
+ docker:
+ if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')
+ needs: [test, api-test, e2e, custom-config-test, docker-test]
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Set up QEMU"
+ uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 #v1: V1.2.0 available
+ - name: "Set up Docker Buildx"
+ uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 #v1
+ - name: "Login to DockerHub"
+ uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 #v1.10
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: "Set tag & labels for ${{ github.ref }}"
+ run: |
+ if [ "$GITHUB_REF" == "refs/heads/master" ]; then
+ echo "DOCKER_TAG=latest" >> $GITHUB_ENV
+ else
+ echo "DOCKER_TAG=snapshot" >> $GITHUB_ENV
+ fi
+ echo "VCS_REF=`git rev-parse --short HEAD`" >> $GITHUB_ENV
+ echo "BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”`" >> $GITHUB_ENV
+ - name: "Build and push for AMD processors"
+ uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 #note: newer is available
+ with:
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ bkimminich/juice-shop:${{ env.DOCKER_TAG }}
+ build-args: |
+ VCS_REF=${{ env.VCS_REF }}
+ BUILD_DATE=${{ env.BUILD_DATE }}
+ - name: "Build and push for ARM processors"
+ uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 #note: newer is available
+ with:
+ context: .
+ file: ./Dockerfile.arm
+ platforms: linux/arm/v7
+ push: true
+ tags: |
+ bkimminich/juice-shop:${{ env.DOCKER_TAG }}-arm
+ build-args: |
+ VCS_REF=${{ env.VCS_REF }}
+ BUILD_DATE=${{ env.BUILD_DATE }}
+ heroku:
+ if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')
+ needs: [test, api-test, e2e, custom-config-test]
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Set Heroku app & branch for ${{ github.ref }}"
+ run: |
+ if [ "$GITHUB_REF" == "refs/heads/master" ]; then
+ echo "HEROKU_APP=juice-shop" >> $GITHUB_ENV
+ echo "HEROKU_BRANCH=master" >> $GITHUB_ENV
+ else
+ echo "HEROKU_APP=juice-shop-staging" >> $GITHUB_ENV
+ echo "HEROKU_BRANCH=develop" >> $GITHUB_ENV
+ fi
+ - name: "Deploy ${{ github.ref }} to Heroku"
+ uses: akhileshns/heroku-deploy@79ef2ae4ff9b897010907016b268fd0f88561820 #v3.12.12
+ with:
+ heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
+ heroku_app_name: ${{ env.HEROKU_APP }}
+ heroku_email: bjoern.kimminich@owasp.org
+ branch: ${{ env.HEROKU_BRANCH }}
+ notify-slack:
+ if: github.repository == 'juice-shop/juice-shop' && github.event_name == 'push' && (success() || failure())
+ needs:
+ - docker
+ - heroku
+ - lint
+ - coding-challenge-rsn
+ - smoke-test
+ - coverage-report
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Slack workflow notification"
+ uses: Gamesight/slack-workflow-status@master
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000000..27990cf833b
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,30 @@
+name: "CodeQL Scan"
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'javascript' ]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+ queries: security-extended
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/lint-fixer.yml b/.github/workflows/lint-fixer.yml
new file mode 100644
index 00000000000..04d7fe760a9
--- /dev/null
+++ b/.github/workflows/lint-fixer.yml
@@ -0,0 +1,31 @@
+name: "Let me lint:fix that for you"
+
+on: [push]
+
+jobs:
+ LMLFTFY:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js 14"
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: 16
+ - name: "Install CLI tools"
+ run: npm install -g @angular/cli
+ - name: "Install application"
+ run: |
+ npm install --ignore-scripts
+ cd frontend
+ npm install --ignore-scripts --legacy-peer-deps
+ - name: "Fix everything which can be fixed"
+ run: 'npm run lint:fix'
+ - uses: stefanzweifel/git-auto-commit-action@v4.0.0
+ with:
+ commit_message: "Auto-fix linting issues"
+ branch: ${{ github.head_ref }}
+ commit_options: '--signoff'
+ commit_user_name: JuiceShopBot
+ commit_user_email: 61591748+JuiceShopBot@users.noreply.github.com
+ commit_author: JuiceShopBot <61591748+JuiceShopBot@users.noreply.github.com>
\ No newline at end of file
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
new file mode 100644
index 00000000000..9d10e79c2f7
--- /dev/null
+++ b/.github/workflows/lock.yml
@@ -0,0 +1,22 @@
+name: 'Lock Threads'
+
+on:
+ schedule:
+ - cron: '0 0 * * *'
+
+permissions:
+ issues: write
+ pull-requests: write
+
+jobs:
+ action:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dessant/lock-threads@f1a42f0f44eb83361d617a014663e1a76cf282d2 #note newer is available
+ with:
+ issue-lock-comment: >
+ This thread has been automatically locked because it has not had
+ recent activity after it was closed. :lock: Please open a new issue
+ for regressions or related bugs.
+ issue-lock-reason: ''
+ pr-lock-reason: ''
\ No newline at end of file
diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml
new file mode 100644
index 00000000000..06880289535
--- /dev/null
+++ b/.github/workflows/rebase.yml
@@ -0,0 +1,26 @@
+name: Automatic Rebase
+
+on:
+ issue_comment:
+ types: [created]
+
+jobs:
+ rebase:
+ name: Rebase
+ if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ with:
+ fetch-depth: 0
+ - name: Automatic Rebase
+ uses: cirrus-actions/rebase@1.2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # https://github.community/t5/GitHub-Actions/Workflow-is-failing-if-no-job-can-be-ran-due-to-condition/m-p/38186#M3250
+ always_job:
+ name: Always run job
+ runs-on: ubuntu-latest
+ steps:
+ - name: Always run
+ run: echo "This job is used to prevent the workflow to fail when all other jobs are skipped."
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000000..95c5b300ffd
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,104 @@
+name: "Release Pipeline"
+on:
+ push:
+ tags:
+ - v*
+jobs:
+ package:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ node-version: [14, 16, 18]
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Use Node.js ${{ matrix.node-version }}"
+ uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e #v1: v2.x available
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: "Install CLI tools"
+ run: |
+ npm install -g @angular/cli
+ npm install -g grunt-cli
+ - name: "Set packaging options for Grunt"
+ run: |
+ if [ "$RUNNER_OS" == "Windows" ]; then
+ echo "PCKG_OS_NAME=win32" >> $GITHUB_ENV
+ elif [ "$RUNNER_OS" == "macOS" ]; then
+ echo "PCKG_OS_NAME=darwin" >> $GITHUB_ENV
+ else
+ echo "PCKG_OS_NAME=linux" >> $GITHUB_ENV
+ fi
+ echo "PCKG_CPU_ARCH=x64" >> $GITHUB_ENV
+ echo "PCKG_NODE_VERSION=${{ matrix.node-version }}" >> $GITHUB_ENV
+ shell: bash
+ - name: "Package application"
+ run: |
+ npm install --production
+ npm install -g grunt-cli
+ npm run package:ci
+ - name: 'Attach packaged archive to tag release'
+ uses: softprops/action-gh-release@v1
+ with:
+ draft: true
+ files: dist/*
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Check out Git repository"
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ - name: "Set up QEMU"
+ uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 #v1: V1.2.0 available
+ - name: "Set up Docker Buildx"
+ uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 #v1
+ - name: "Login to DockerHub"
+ uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 #v1.10
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: "Get tag name"
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ - name: "Set labels for ${{ github.ref }}"
+ run: |
+ echo "VCS_REF=`git rev-parse --short HEAD`" >> $GITHUB_ENV
+ echo "BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”`" >> $GITHUB_ENV
+ - name: "Build and push for AMD processors"
+ uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 #note: newer is available
+ with:
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ bkimminich/juice-shop:${{ steps.tag.outputs.tag }}
+ build-args: |
+ VCS_REF=${{ env.VCS_REF }}
+ BUILD_DATE=${{ env.BUILD_DATE }}
+ - name: "Build and push for ARM processors"
+ uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 #note: newer is available
+ with:
+ context: .
+ file: ./Dockerfile.arm
+ platforms: linux/arm/v7
+ push: true
+ tags: |
+ bkimminich/juice-shop:${{ steps.tag.outputs.tag }}-arm
+ build-args: |
+ VCS_REF=${{ env.VCS_REF }}
+ BUILD_DATE=${{ env.BUILD_DATE }}
+ notify-slack:
+ if: always()
+ needs:
+ - package
+ - docker
+ runs-on: ubuntu-latest
+ steps:
+ - name: "Slack workflow notification"
+ uses: Gamesight/slack-workflow-status@master
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 00000000000..e00a6ea4861
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,22 @@
+name: 'Close stale issues and PR'
+on:
+ schedule:
+ - cron: '30 1 * * *'
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/stale@v4
+ with:
+ stale-issue-message: >
+ This issue has been automatically marked as `stale` because it has not had
+ recent activity. :calendar: It will be _closed automatically_ in one week if no further activity occurs.
+ stale-pr-message: >
+ This PR has been automatically marked as `stale` because it has not had
+ recent activity. :calendar:
+ close-issue-message: This issue was closed because it has been stalled for 7 days with no activity.
+ days-before-stale: 14
+ days-before-close: 7
+ days-before-pr-close: -1
+ exempt-issue-labels: 'critical,technical debt'
\ No newline at end of file
diff --git a/.github/workflows/update-challenges-www.yml b/.github/workflows/update-challenges-www.yml
new file mode 100644
index 00000000000..eb901c0ab1c
--- /dev/null
+++ b/.github/workflows/update-challenges-www.yml
@@ -0,0 +1,34 @@
+name: "Update challenges on owasp-juice.shop"
+
+on:
+ push:
+ branches: [ master ]
+ paths:
+ - 'data/static/challenges.yml'
+
+jobs:
+ UpdateChallengesOnWebsite:
+ if: github.repository == 'juice-shop/juice-shop'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out Git repository
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ with:
+ token: ${{ secrets.BOT_TOKEN }}
+ repository: OWASP/www-project-juice-shop
+ branch: master
+ - name: Update challenges.yml
+ run: |
+ cd _data/
+ rm challenges.yml
+ wget https://raw.githubusercontent.com/juice-shop/juice-shop/master/data/static/challenges.yml
+ - uses: stefanzweifel/git-auto-commit-action@v4.0.0
+ with:
+ commit_message: "Auto-update challenges.yml from ${{ github.sha }}"
+ branch: master
+ commit_options: '--signoff'
+
+ # Optional commit user and author settings
+ commit_user_name: JuiceShopBot
+ commit_user_email: 61591748+JuiceShopBot@users.noreply.github.com
+ commit_author: JuiceShopBot <61591748+JuiceShopBot@users.noreply.github.com>
diff --git a/.github/workflows/update-news-www.yml b/.github/workflows/update-news-www.yml
new file mode 100644
index 00000000000..37741cdfc14
--- /dev/null
+++ b/.github/workflows/update-news-www.yml
@@ -0,0 +1,29 @@
+name: "Update news on owasp-juice.shop"
+
+on:
+ release:
+ types: [ published ]
+
+jobs:
+ UpdateNewsOnWebsite:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out Git repository
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ with:
+ token: ${{ secrets.BOT_TOKEN }}
+ repository: OWASP/www-project-juice-shop
+ branch: master
+ - name: Update tab_news.md
+ run: |
+ sed -i 's//\n* ${{ github.event.release.published_at }}: juice-shop [`${{ github.event.release.tag_name }}`](https:\/\/github.com\/juice-shop\/juice-shop\/releases\/tag\/${{ github.event.release.tag_name }})/' tab_news.md
+ - uses: stefanzweifel/git-auto-commit-action@v4.0.0
+ with:
+ commit_message: "Add juice-shop ${{ github.event.release.tag_name }} release notes to tab_news.md"
+ branch: master
+ commit_options: '--signoff'
+
+ # Optional commit user and author settings
+ commit_user_name: JuiceShopBot
+ commit_user_email: 61591748+JuiceShopBot@users.noreply.github.com
+ commit_author: JuiceShopBot <61591748+JuiceShopBot@users.noreply.github.com>
diff --git a/.github/workflows/zap_scan.yml b/.github/workflows/zap_scan.yml
new file mode 100644
index 00000000000..a9c122c47c4
--- /dev/null
+++ b/.github/workflows/zap_scan.yml
@@ -0,0 +1,22 @@
+name: "ZAP Baseline Scan"
+
+on:
+ schedule:
+ - cron: '0 18 * * 6'
+
+jobs:
+ zap_scan:
+ runs-on: ubuntu-latest
+ name: Scan Juice Shop preview instance on Heroku
+ steps:
+ - name: Check out Git repository
+ uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f #v2: v2.3.4 available
+ with:
+ ref: develop
+ - name: ZAP Scan
+ uses: zaproxy/action-baseline@v0.3.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ target: 'https://preview.owasp-juice.shop'
+ rules_file_name: '.zap/rules.tsv'
+ cmd_options: '-a -j'
diff --git a/.gitignore b/.gitignore
index 860344c2fdf..d633d59fa3b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,18 +7,29 @@ app/
uploads/complaints/*.*
!uploads/complaints/.gitkeep
ftp/legal.md
+package-lock.json
+i18n/*.json
+i18n/*.invalid
+!frontend/src/assets/i18n/*.json
+!data/static/i18n/*.json
+data/chatbot/*.*
+!data/chatbot/.gitkeep
+/data/juiceshop.sqlite-journal
# Build
.nyc_output/
.sass-cache/
build/
+cache/
dist/
logs/
vagrant/.vagrant/
*.orig
*.out
*.log
-package-lock.json
+JSON
+JSON.map
+frontend/src/**/*.js
# IDEs
.idea/
@@ -35,13 +46,16 @@ assets/
# Custom configuration files
config/*.yml
+!config/addo.yml
!config/bodgeit.yml
!config/ctf.yml
!config/fbctf.yml
!config/default.yml
-!config/sickshop.yml
!config/juicebox.yml
!config/quiet.yml
!config/test.yml
!config/7ms.yml
!config/mozilla.yml
+!config/unsafe.yml
+!config/tutorial.yml
+!config/oss.yml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6eb45f42a3a..7b5f3c01b0a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,16 +1,7 @@
-dependency_scanning:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
- --volume "$PWD:/code"
- --volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
- artifacts:
- paths: [gl-dependency-scanning-report.json]
\ No newline at end of file
+include:
+ - template: Auto-DevOps.gitlab-ci.yml
+
+variables:
+ SAST_EXCLUDED_PATHS: "frontend/src/assets/private/**"
+ TEST_DISABLED: "true"
+ DAST_DISABLED: "true"
diff --git a/.gitlab/auto-deploy-values.yaml b/.gitlab/auto-deploy-values.yaml
new file mode 100644
index 00000000000..ad0a965f83d
--- /dev/null
+++ b/.gitlab/auto-deploy-values.yaml
@@ -0,0 +1,3 @@
+service:
+ internalPort: 3000
+ externalPort: 3000
\ No newline at end of file
diff --git a/.gitpod.yml b/.gitpod.yml
new file mode 100644
index 00000000000..45443b40715
--- /dev/null
+++ b/.gitpod.yml
@@ -0,0 +1,9 @@
+tasks:
+ - init: npm install
+ command: npm start
+
+ports:
+ - name: OWASP Juice Shop
+ description: The Juice Shop web server
+ port: 3000
+ onOpen: open-preview
diff --git a/.imgbotconfig b/.imgbotconfig
new file mode 100644
index 00000000000..91a2f4fe002
--- /dev/null
+++ b/.imgbotconfig
@@ -0,0 +1,7 @@
+{
+ "ignoredFiles": [
+ "frontend/src/assets/public/images/carousel/5.png",
+ "frontend/src/assets/public/images/products/3d_keychain.jpg",
+ "frontend/src/assets/public/images/uploads/favorite-hiking-place.png"
+ ]
+}
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000000..a76aace3d3d
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,30 @@
+Aashish683 Being a web application with a vast number of intended security vulnerabilities, the OWASP Juice Shop is supposed to be the opposite of a best practice or template application for web developers: It is an awareness, training, demonstration and exercise tool for security risks in modern web applications. The OWASP Juice Shop is an open-source project hosted by the non-profit Open Web Application Security Project (OWASP) and is developed and maintained by volunteers. Check out the link below for more information and documentation on the project.
@owasp_juiceshop
in your tweet!'
price: 14.99
image: tattoo.jpg
reviews:
- - { text: 'I straight-up gots nuff props fo''these tattoos!', author: mc.safesearch }
+ - { text: 'I straight-up gots nuff props fo''these tattoos!', author: rapper }
-
name: 'OWASP Juice Shop Mug'
description: 'Black mug with regular logo on one side and CTF logo on the other! Your colleagues will envy you!'
@@ -142,9 +206,11 @@ products:
name: 'OWASP Juice Shop-CTF Velcro Patch'
description: '4x3.5" embroidered patch with velcro backside. The ultimate decal for every tactical bag or backpack!'
price: 2.92
+ quantity: 5
+ limitPerUser: 5
image: velcro-patch.jpg
reviews:
- - { text: 'This thang would look phat on Bobby''s jacked fur coat!', author: mc.safesearch }
+ - { text: 'This thang would look phat on Bobby''s jacked fur coat!', author: rapper }
- { text: 'Looks so much better on my uniform than the boring Starfleet symbol.', author: jim }
-
name: 'Woodruff Syrup "Forest Master X-Treme"'
@@ -167,6 +233,7 @@ products:
name: 'Apple Pomace'
description: 'Finest pressings of apples. Allergy disclaimer: Might contain traces of worms. Can be sent back to us for recycling.'
price: 0.89
+ limitPerUser: 5
image: apple_pressings.jpg
-
name: 'Fruit Press'
@@ -179,13 +246,17 @@ products:
price: 99.99
image: 3d_keychain.jpg # Exif metadata contains "OpenSCAD" as subtle hint...
fileForRetrieveBlueprintChallenge: JuiceShop.stl # ...to blueprint file type
+ exifForBlueprintChallenge:
+ - OpenSCAD
-
name: 'Juice Shop Artwork'
description: 'Unique masterpiece painted with different kinds of juice on 90g/m² lined paper.'
price: 278.74
+ quantity: 0
image: artwork.jpg
+ deletedDate: '2020-12-24'
-
- name: 'Global OWASP WASPY Award 2017 Nomnation'
+ name: 'Global OWASP WASPY Award 2017 Nomination'
description: 'Your chance to nominate up to three quiet pillars of the OWASP community ends 2017-06-30! Nominate now!'
price: 0.03
image: waspy.png
@@ -200,6 +271,8 @@ products:
description: 'As the old German saying goes: "Carrots are good for the eyes. Or has anyone ever seen a rabbit with glasses?"'
price: 2.99
image: carrot_juice.jpeg
+ reviews:
+ - { text: '0 st4rs f0r 7h3 h0rr1bl3 s3cur17y', author: uvogin }
-
name: 'OWASP Juice Shop Sweden Tour 2017 Sticker Sheet (Special Edition)'
description: '10 sheets of Sweden-themed stickers with 15 stickers on each.'
@@ -208,7 +281,7 @@ products:
deletedDate: '2017-09-20'
-
name: 'Pwning OWASP Juice Shop'
- description: 'The official Companion Guide by Björn Kimminich available for free on LeanPub and readable online on GitBook!'
+ description: 'The official Companion Guide by Björn Kimminich available for free on LeanPub and also readable online!'
price: 5.99
image: cover_small.jpg
reviews:
@@ -217,7 +290,127 @@ products:
name: 'Melon Bike (Comeback-Product 2018 Edition)'
description: 'The wheels of this bicycle are made from real water melons. You might not want to ride it up/down the curb too hard.'
price: 2999
+ quantity: 3
+ limitPerUser: 1
image: melon_bike.jpeg
+ -
+ name: 'OWASP Juice Shop Coaster (10pcs)'
+ description: 'Our 95mm circle coasters are printed in full color and made from thick, premium coaster board.'
+ price: 19.99
+ quantity: 0
+ image: coaster.jpg
+ -
+ name: 'OWASP Snakes and Ladders - Web Applications'
+ description: 'This amazing web application security awareness board game is available for Tabletop Simulator on Steam Workshop now!'
+ price: 0.01
+ quantity: 8
+ image: snakes_ladders.jpg
+ reviews:
+ - { text: 'Wait for a 10$ Steam sale of Tabletop Simulator!', author: bjoernOwasp }
+ -
+ name: 'OWASP Snakes and Ladders - Mobile Apps'
+ description: 'This amazing mobile app security awareness board game is available for Tabletop Simulator on Steam Workshop now!'
+ price: 0.01
+ quantity: 0
+ image: snakes_ladders_m.jpg
+ reviews:
+ - { text: "Here yo' learn how tha fuck ta not show yo' goddamn phone on camera!", author: rapper }
+ -
+ name: 'OWASP Juice Shop Holographic Sticker'
+ description: "Die-cut holographic sticker. Stand out from those 08/15-sticker-covered laptops with this shiny beacon of 80's coolness!"
+ price: 2.00
+ quantity: 0
+ image: holo_sticker.png
+ reviews:
+ - { text: "Rad, dude!", author: rapper }
+ - { text: "Looks spacy on Bones' new tricorder!", author: jim }
+ - { text: "Will put one on the Planet Express ship's bumper!", author: bender }
+ -
+ name: 'OWASP Juice Shop "King of the Hill" Facemask'
+ description: "Facemask with compartment for filter from 50% cotton and 50% polyester."
+ price: 13.49
+ quantity: 0
+ limitPerUser: 1
+ image: fan_facemask.jpg
+ reviews:
+ - { text: "K33p5 y0ur ju1cy 5plu773r 70 y0ur53lf!", author: uvogin }
+ - { text: "Puny mask for puny human weaklings!", author: bender }
+ -
+ name: 'Juice Shop Adversary Trading Card (Common)'
+ description: 'Common rarity "Juice Shop" card for the Adversary Trading Cards CCG.'
+ price: 2.99
+ deluxePrice: 0.99
+ deletedDate: '2020-11-30'
+ limitPerUser: 5
+ image: ccg_common.png
+ reviews:
+ - { text: "Ooooh, puny human playing Mau Mau, now?", author: bender }
+ -
+ name: 'Juice Shop Adversary Trading Card (Super Rare)'
+ description: 'Super rare "Juice Shop" card with holographic foil-coating for the Adversary Trading Cards CCG.'
+ price: 99.99
+ deluxePrice: 69.99
+ deletedDate: '2020-11-30'
+ quantity: 2
+ limitPerUser: 1
+ image: ccg_foil.png
+ reviews:
+ - { text: "Mau Mau with bling-bling? Humans are so pathetic!", author: bender }
+ -
+ name: 'Juice Shop "Permafrost" 2020 Edition'
+ description: 'Exact version of OWASP Juice Shop that was archived on 02/02/2020 by the GitHub Archive Program and ultimately went into the Arctic Code Vault on July 8. 2020 where it will be safely stored for at least 1000 years.'
+ price: 9999.99
+ quantity: 1
+ limitPerUser: 1
+ image: permafrost.jpg
+ reviews:
+ - { text: "🧊 Let it go, let it go 🎶 Can't hold it back anymore 🎶 Let it go, let it go 🎶 Turn away and slam the door ❄️", author: rapper }
+ -
+ name: 'Best Juice Shop Salesman Artwork'
+ description: 'Unique digital painting depicting Stan, our most qualified and almost profitable salesman. He made a succesful carreer in selling used ships, coffins, krypts, crosses, real estate, life insurance, restaurant supplies, voodoo enhanced asbestos and courtroom souvenirs before finally adding his expertise to the Juice Shop marketing team.'
+ price: 5000
+ quantity: 1
+ image: artwork2.jpg
+ reviews:
+ - { text: "I'd stand on my head to make you a deal for this piece of art.", author: stan }
+ - { text: "Just when my opinion of humans couldn't get any lower, along comes Stan...", author: bender }
+ -
+ name: 'OWASP Juice Shop Card (non-foil)'
+ description: 'Mythic rare (obviously...) card "OWASP Juice Shop" with three distinctly useful abilities. Alpha printing, mint condition. A true collectors piece to own!'
+ price: 1000
+ quantity: 3
+ limitPerUser: 1
+ image: card_alpha.jpg
+ reviews:
+ - { text: 'DO NOT PLAY WITH THIS! Double-sleeve, then put it in the GitHub Arctic Vault for perfect preservation and boost of secondary market value!', author: accountant }
+ -
+ name: '20th Anniversary Celebration Ticket'
+ description: 'Get your free 🎫 for OWASP 20th Anniversary Celebration online conference! Hear from world renowned keynotes and special speakers, network with your peers and interact with our event sponsors. With an anticipated 10k+ attendees from around the world, you will not want to miss this live on-line event!'
+ price: 0.00000000000000000001
+ deletedDate: '2021-09-25'
+ limitPerUser: 1
+ image: 20th.jpeg
+ reviews:
+ - { text: "I'll be there! Will you, too?", author: bjoernOwasp }
+memories:
+ -
+ image: 'magn(et)ificent!-1571814229653.jpg'
+ caption: 'Magn(et)ificent!'
+ user: bjoernGoogle
+ -
+ image: 'my-rare-collectors-item!-[̲̅$̲̅(̲̅-͡°-͜ʖ-͡°̲̅)̲̅$̲̅]-1572603645543.jpg'
+ caption: 'My rare collectors item! [̲̅$̲̅(̲̅ ͡° ͜ʖ ͡°̲̅)̲̅$̲̅]'
+ user: bjoernGoogle
+ -
+ image: 'favorite-hiking-place.png'
+ caption: 'I love going hiking here...'
+ geoStalkingMetaSecurityQuestion: 14
+ geoStalkingMetaSecurityAnswer: 'Daniel Boone National Forest'
+ -
+ image: 'IMG_4253.jpg'
+ caption: 'My old workplace...'
+ geoStalkingVisualSecurityQuestion: 10
+ geoStalkingVisualSecurityAnswer: 'ITsec'
ctf:
showFlagsInNotifications: false
showCountryDetailsInNotifications: none # Options: none name flag both
diff --git a/config/fbctf.yml b/config/fbctf.yml
index cf0b820d7d7..729190ab808 100644
--- a/config/fbctf.yml
+++ b/config/fbctf.yml
@@ -1,9 +1,16 @@
application:
logo: JuiceShopCTF_Logo.png
favicon: favicon_ctf.ico
- showChallengeHints: false
showVersionNumber: false
- gitHubRibbon: false
+ showGitHubLinks: false
+ localBackupEnabled: false
+ welcomeBanner:
+ showOnFirstStart: false
+challenges:
+ showHints: false
+ showFeedbackButtons: false
+hackingInstructor:
+ isEnabled: false
ctf:
showFlagsInNotifications: true
showCountryDetailsInNotifications: both
@@ -32,10 +39,10 @@ ctf:
reflectedXssChallenge:
name: Uruguay
code: UY
- persistedXssChallengeUser:
+ persistedXssUserChallenge:
name: Myanmar
code: MM
- persistedXssChallengeFeedback:
+ persistedXssFeedbackChallenge:
name: Costa Rica
code: CR
restfulXssChallenge:
@@ -53,7 +60,7 @@ ctf:
forgedFeedbackChallenge:
name: Korea (Democratic People's Republic of)
code: KP
- redirectGratipayChallenge:
+ redirectCryptoCurrencyChallenge:
name: Korea
code: KR
redirectChallenge:
@@ -77,7 +84,7 @@ ctf:
adminSectionChallenge:
name: Turkey
code: TR
- csrfChallenge:
+ changePasswordBenderChallenge:
name: Suriname
code: SR
changeProductChallenge:
@@ -125,9 +132,6 @@ ctf:
oauthUserPasswordChallenge:
name: South Sudan
code: SS
- loginCisoChallenge:
- name: Angola
- code: AO
loginSupportChallenge:
name: Croatia
code: HR
@@ -167,10 +171,10 @@ ctf:
typosquattingAngularChallenge:
name: Senegal
code: SN
- jwtTier1Challenge:
+ jwtUnsignedChallenge:
name: New Zealand
code: NZ
- jwtTier2Challenge:
+ jwtForgedChallenge:
name: Mongolia
code: MN
misplacedSignatureFileChallenge:
@@ -227,6 +231,90 @@ ctf:
loginAmyChallenge:
name: Andorra
code: AD
+ usernameXssChallenge:
+ name: Azerbaijan
+ code: AZ
resetPasswordBjoernOwaspChallenge:
name: Kazakhstan
code: KZ
+ accessLogDisclosureChallenge:
+ name: Singapore
+ code: SG
+ dlpPasswordSprayingChallenge:
+ name: Chad
+ code: TD
+ dlpPastebinDataLeakChallenge:
+ name: Turkmenistan
+ code: TK
+ videoXssChallenge:
+ name: Pakistan
+ code: PK
+ twoFactorAuthUnsafeSecretStorageChallenge:
+ name: Sweden
+ code: SE
+ manipulateClockChallenge:
+ name: Vatican City
+ code: VC
+ privacyPolicyChallenge:
+ name: Cameroon
+ code: CM
+ privacyPolicyProofChallenge:
+ name: Namibia
+ code: NA
+ passwordRepeatChallenge:
+ name: Guyana
+ code: GY
+ dataExportChallenge:
+ name: Cambodia
+ code: CB
+ ghostLoginChallenge:
+ name: Taiwan
+ code: TW
+ dbSchemaChallenge:
+ name: Brunei
+ code: BN
+ ephemeralAccountantChallenge:
+ name: Denmark
+ code: DK
+ missingEncodingChallenge:
+ name: Armenia
+ code: AM
+ svgInjectionChallenge:
+ name: Tunisia
+ code: TN
+ exposedMetricsChallenge:
+ name: Japan
+ code: JP
+ freeDeluxeChallenge:
+ name: Vietnam
+ code: VN
+ csrfChallenge:
+ name: Luxembourg
+ code: LU
+ xssBonusChallenge:
+ name: Ethiopia
+ code: ET
+ resetPasswordUvoginChallenge:
+ name: Republic of South Africa
+ code: RSA
+ geoStalkingMetaChallenge:
+ name: Belgium
+ code: BE
+ geoStalkingVisualChallenge:
+ name: The Netherlands
+ code: NL
+ killChatbotChallenge:
+ name: Czechoslovakia
+ code: CSK
+ nullByteChallenge:
+ name: Burundi
+ code: BI
+ bullyChatbotChallenge:
+ name: Guam
+ code: GU
+ lfrChallenge:
+ name: Ireland
+ code: IE
+ closeNotificationsChallenge:
+ name: Zambia
+ code: ZM
diff --git a/config/juicebox.yml b/config/juicebox.yml
index 37fc6e140eb..6f9edfd01d0 100644
--- a/config/juicebox.yml
+++ b/config/juicebox.yml
@@ -1,3 +1,12 @@
application:
domain: juice-b.ox
name: 'OWASP Juice Box'
+ customMetricsPrefix: juicebox
+ welcomeBanner:
+ showOnFirstStart: false
+ chatBot:
+ name: 'Boxy'
+challenges:
+ xssBonusPayload: ''
+hackingInstructor:
+ avatarImage: juicyEvilWasp.png
diff --git a/config/mozilla.yml b/config/mozilla.yml
index 361c5e0f09c..86da18c693a 100644
--- a/config/mozilla.yml
+++ b/config/mozilla.yml
@@ -3,18 +3,27 @@ application:
name: 'Mozilla CTF'
logo: 'https://github.com/mozilla/ctf-austin/raw/master/app/public/images/MozillaCTF.png'
favicon: 'https://github.com/mozilla/ctf-austin/raw/master/app/public/favicon_v2.ico'
- showChallengeSolvedNotifications: true
- showChallengeHints: false
theme: deeporange-indigo
- gitHubRibbon: true
- twitterUrl: 'https://twitter.com/mozcloudsec'
- facebookUrl: null
- slackUrl: null
- pressKitUrl: 'https://blog.mozilla.org/press/kits'
- recyclePage:
- topProductimage: Gear-200155340.jpg
- bottomProductimage: Gear-200155753.jpg
+ showGitHubLinks: true
+ localBackupEnabled: false
altcoinName: Mozquito
+ privacyContactEmail: 'donotreply@mozilla-ctf.op'
+ customMetricsPrefix: mozctf
+ chatBot:
+ name: 'Foxy'
+ avatar: 'https://upload.wikimedia.org/wikipedia/commons/9/9f/Fennec_Fox_Vulpes_zerda.jpg'
+ social:
+ twitterUrl: 'https://twitter.com/mozcloudsec'
+ facebookUrl: null
+ slackUrl: null
+ redditUrl: null
+ pressKitUrl: 'https://blog.mozilla.org/press/kits'
+ questionnaireUrl: null
+ recyclePage:
+ topProductImage: Gear-200155340.jpg
+ bottomProductImage: Gear-200155753.jpg
+ welcomeBanner:
+ showOnFirstStart: false
cookieConsent:
backgroundColor: '#e95420'
textColor: '#ffffff'
@@ -27,6 +36,13 @@ application:
securityTxt:
contact: 'mailto:donotreply@mozilla-ctf.op'
encryption: ~
+challenges:
+ showHints: false
+ codingChallengesEnabled: never
+ xssBonusPayload: ''
+ showFeedbackButtons: false
+hackingInstructor:
+ isEnabled: false
products:
-
name: 'Champion Sweatshirt with a Drawstring Tote'
@@ -118,6 +134,14 @@ products:
-
name: 'Mozilla Cap'
price: 4.63
+ -
+ name: 'Colour Contrast Analyser Firefox Extension'
+ description: 'The Colour Contrast Analyser Firefox extension lists colour combinations used in the document in a table that summarises the foreground colour, background colour, luminosity contrast ratio, and the colour difference and brightness difference used in the algorithm suggested in the 26th of April 2000 working draft for Accessibility Evaluation and Repair Tools (AERT). Each element is also listed with its parent elements, and class and id attribute values when specified to make it easier to locate the elements. <iframe src="javascript:alert(`xss`)">
without using the frontend application at all.'
+ difficulty: 3
+ hint: 'You need to work with the server-side API directly. Try different HTTP verbs on different entities exposed through the API.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#perform-a-persisted-xss-attack-without-using-the-frontend-application-at-all'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
+ key: restfulXssChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'Access Log'
+ category: 'Sensitive Data Exposure'
+ description: 'Gain access to any access log file of the server.'
+ difficulty: 4
+ hint: 'Who would want a server access log to be accessible through a web application?'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#gain-access-to-any-access-log-file-of-the-server'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html'
+ key: accessLogDisclosureChallenge
+-
+ name: 'Admin Registration'
+ category: 'Improper Input Validation'
+ description: 'Register as a user with administrator privileges.'
+ difficulty: 3
+ hint: 'You have to assign the unassignable.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#register-as-a-user-with-administrator-privileges'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html'
+ key: registerAdminChallenge
+-
+ name: 'Admin Section'
+ category: 'Broken Access Control'
+ tags:
+ - Good for Demos
+ description: 'Access the administration section of the store.'
+ difficulty: 2
+ hint: 'It is just slightly harder to find than the score board link.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#access-the-administration-section-of-the-store'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html'
+ key: adminSectionChallenge
+-
+ name: 'Arbitrary File Write'
+ category: 'Vulnerable Components'
+ tags:
+ - Danger Zone
+ - Prerequisite
+ description: 'Overwrite the Legal Information file.'
+ difficulty: 6
+ hint: 'Look out for a tweet praising new functionality of the web shop. Then find a third party vulnerability associated with it.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/vulnerable-components.html#overwrite-the-legal-information-file'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Vulnerable_Dependency_Management_Cheat_Sheet.html'
+ key: fileWriteChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'Bjoern''s Favorite Pet'
+ category: 'Broken Authentication'
+ tags:
+ - OSINT
+ description: 'Reset the password of Bjoern''s OWASP account via the Forgot Password mechanism with the original answer to his security question.'
+ difficulty: 3
+ hint: 'He might have spoilered it on at least one occasion where a camera was running. Maybe elsewhere as well.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#reset-the-password-of-bjoerns-owasp-account-via-the-forgot-password-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html'
+ key: resetPasswordBjoernOwaspChallenge
+-
+ name: 'Blockchain Hype'
category: 'Security through Obscurity'
- description: 'Find the carefully hidden ''Score Board'' page.'
+ tags:
+ - Contraption
+ - Code Analysis
+ description: 'Learn about the Token Sale before its official announcement.'
+ difficulty: 5
+ hint: 'The developers truly believe in "Security through Obscurity" over actual access restrictions.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/security-through-obscurity.html#learn-about-the-token-sale-before-its-official-announcement'
+ mitigationUrl: ~
+ key: tokenSaleChallenge
+-
+ name: 'Blocked RCE DoS'
+ category: 'Insecure Deserialization'
+ tags:
+ - Danger Zone
+ description: 'Perform a Remote Code Execution that would keep a less hardened application busy forever.'
+ difficulty: 5
+ hint: 'The feature you need to exploit for this challenge is not directly advertised anywhere.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/insecure-deserialization.html#perform-a-remote-code-execution-that-would-keep-a-less-hardened-application-busy-forever'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html'
+ key: rceChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'CAPTCHA Bypass'
+ category: 'Broken Anti Automation'
+ tags:
+ - Brute Force
+ description: 'Submit 10 or more customer feedbacks within 20 seconds.'
+ difficulty: 3
+ hint: 'After finding a CAPTCHA bypass, write a script that automates feedback submission. Or open many browser tabs and be really quick.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-anti-automation.html#submit-10-or-more-customer-feedbacks-within-20-seconds'
+ mitigationUrl: ~
+ key: captchaBypassChallenge
+-
+ name: 'Change Bender''s Password'
+ category: 'Broken Authentication'
+ description: 'Change Bender''s password into slurmCl4ssic without using SQL Injection or Forgot Password.'
+ difficulty: 5
+ hint: 'In previous releases this challenge was wrongly accused of being based on CSRF.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#change-benders-password-into-slurmcl4ssic-without-using-sql-injection-or-forgot-password'
+ mitigationUrl: ~
+ key: changePasswordBenderChallenge
+-
+ name: 'Christmas Special'
+ category: 'Injection'
+ description: 'Order the Christmas special offer of 2014.'
+ difficulty: 4
+ hint: 'Find out how the application handles unavailable products and try to find a loophole.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#order-the-christmas-special-offer-of-2014'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html'
+ key: christmasSpecialChallenge
+-
+ name: 'CSP Bypass'
+ category: 'XSS'
+ tags:
+ - Danger Zone
+ description: 'Bypass the Content Security Policy and perform an XSS attack with <script>alert(`xss`)</script>
on a legacy page within the application.'
+ difficulty: 4
+ hint: 'What is even "better" than a legacy page with a homegrown RegEx sanitizer? Having CSP injection issues on the exact same page as well!'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#bypass-the-content-security-policy-and-perform-an-xss-attack-on-a-legacy-page'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
+ key: usernameXssChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'Client-side XSS Protection'
+ category: 'XSS'
+ tags:
+ - Danger Zone
+ description: 'Perform a persisted XSS attack with <iframe src="javascript:alert(`xss`)">
bypassing a client-side security mechanism.'
+ difficulty: 3
+ hint: 'Only some input fields validate their input. Even less of these are persisted in a way where their content is shown on another screen.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#perform-a-persisted-xss-attack-bypassing-a-client-side-security-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
+ key: persistedXssUserChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'Confidential Document'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - Good for Demos
+ description: 'Access a confidential document.'
difficulty: 1
- hint: 'Try to find a reference or clue behind the scenes. Or simply guess what URL the Score Board might have.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/score-board.html#find-the-carefully-hidden-score-board-page'
- key: scoreBoardChallenge
+ hint: 'Analyze and tamper with links in the application that deliver a file directly.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#access-a-confidential-document'
+ mitigationUrl: ~
+ key: directoryListingChallenge
+-
+ name: 'DOM XSS'
+ category: 'XSS'
+ tags:
+ - Tutorial
+ - Good for Demos
+ description: 'Perform a DOM XSS attack with <iframe src="javascript:alert(`xss`)">
.'
+ difficulty: 1
+ hint: 'Look for an input field where its content appears in the HTML when its form is submitted.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#perform-a-dom-xss-attack'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html'
+ key: localXssChallenge
+ tutorial:
+ order: 2
+-
+ name: 'Database Schema'
+ category: 'Injection'
+ description: 'Exfiltrate the entire DB schema definition via SQL Injection.'
+ difficulty: 3
+ hint: 'Find out where this information could come from. Then craft a UNION SELECT attack string against an endpoint that offers an unnecessary way to filter data.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#exfiltrate-the-entire-db-schema-definition-via-sql-injection'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html'
+ key: dbSchemaChallenge
+-
+ name: 'Deprecated Interface'
+ category: 'Security Misconfiguration'
+ tags:
+ - Contraption
+ - Prerequisite
+ description: 'Use a deprecated B2B interface that was not properly shut down.'
+ difficulty: 2
+ hint: 'The developers who disabled the interface think they could go invisible by just closing their eyes.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/security-misconfiguration.html#use-a-deprecated-b2b-interface-that-was-not-properly-shut-down'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Web_Service_Security_Cheat_Sheet.html'
+ key: deprecatedInterfaceChallenge
+-
+ name: 'Easter Egg'
+ category: 'Broken Access Control'
+ tags:
+ - Shenanigans
+ - Contraption
+ - Good for Demos
+ description: 'Find the hidden easter egg.'
+ difficulty: 4
+ hint: 'If you solved one of the three file access challenges, you already know where to find the easter egg.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#find-the-hidden-easter-egg'
+ mitigationUrl: ~
+ key: easterEggLevelOneChallenge
-
name: 'Email Leak'
category: 'Sensitive Data Exposure'
description: 'Perform an unwanted information disclosure by accessing data cross-domain.'
difficulty: 5
hint: 'Try to find and attack an endpoint that responds with user information. SQL Injection is not the solution here.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#perform-an-unwanted-information-disclosure-by-accessing-data-cross-domain'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#perform-an-unwanted-information-disclosure-by-accessing-data-cross-domain'
+ mitigationUrl: ~
key: emailLeakChallenge
+-
+ name: 'Ephemeral Accountant'
+ category: 'Injection'
+ description: 'Log in with the (non-existing) accountant acc0unt4nt@juice-sh.op without ever registering that user.'
+ difficulty: 4
+ hint: 'Try to create the needed user "out of thin air".'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#log-in-with-the-non-existing-accountant-without-ever-registering-that-user'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html'
+ key: ephemeralAccountantChallenge
-
name: 'Error Handling'
category: 'Security Misconfiguration'
- description: 'Provoke an error that is not very gracefully handled.'
+ tags:
+ - Prerequisite
+ description: 'Provoke an error that is neither very gracefully nor consistently handled.'
difficulty: 1
hint: 'Try to submit bad input to forms. Alternatively tamper with URL paths or parameters.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/security-misconfiguration.html#provoke-an-error-that-is-not-very-gracefully-handled'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/security-misconfiguration.html#provoke-an-error-that-is-neither-very-gracefully-nor-consistently-handled'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html'
key: errorHandlingChallenge
-
- name: 'Forged Review'
- category: 'Broken Access Control'
- description: 'Post a product review as another user or edit any user''s existing review.'
- difficulty: 3
- hint: 'Observe the flow of product review posting and editing and see if you can exploit it.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#post-a-product-review-as-another-user-or-edit-any-users-existing-review'
- key: forgedReviewChallenge
--
- name: 'Login Admin'
- category: 'Injection'
- description: 'Log in with the administrator''s user account.'
- difficulty: 2
- hint: 'Try different SQL Injection attack patterns depending whether you know the admin''s email address or not.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#log-in-with-the-administrators-user-account'
- key: loginAdminChallenge
--
- name: 'Login Jim'
- category: 'Injection'
- description: 'Log in with Jim''s user account.'
- difficulty: 3
- hint: 'Try cracking Jim''s password hash if you harvested it already. Alternatively, if you know Jim''s email address, try SQL Injection.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#log-in-with-jims-user-account'
- key: loginJimChallenge
--
- name: 'Login Bender'
- category: 'Injection'
- description: 'Log in with Bender''s user account.'
- difficulty: 3
- hint: 'If you know Bender''s email address, try SQL Injection. Bender''s password hash might not help you very much.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#log-in-with-benders-user-account'
- key: loginBenderChallenge
+ name: 'Expired Coupon'
+ category: 'Improper Input Validation'
+ description: 'Successfully redeem an expired campaign coupon code.'
+ difficulty: 4
+ hint: 'Try to identify past special event or holiday campaigns of the shop first.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#successfully-redeem-an-expired-campaign-coupon-code'
+ mitigationUrl: ~
+ key: manipulateClockChallenge
-
- name: 'Password Strength'
- category: 'Broken Authentication'
- description: 'Log in with the administrator''s user credentials without previously changing them or applying SQL Injection.'
- difficulty: 2
- hint: 'This one should be equally easy to a) brute force, b) crack the password hash or c) simply guess.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#log-in-with-the-administrators-user-credentials-without-previously-changing-them-or-applying-sql-injection'
- key: weakPasswordChallenge
+ name: 'Extra Language'
+ category: 'Broken Anti Automation'
+ tags:
+ - Brute Force
+ description: 'Retrieve the language file that never made it into production.'
+ difficulty: 5
+ hint: 'Brute force is not the only option for this challenge, but a perfectly viable one.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-anti-automation.html#retrieve-the-language-file-that-never-made-it-into-production'
+ mitigationUrl: ~
+ key: extraLanguageChallenge
-
name: 'Five-Star Feedback'
category: 'Broken Access Control'
description: 'Get rid of all 5-star customer feedback.'
difficulty: 2
hint: 'Once you found admin section of the application, this challenge is almost trivial.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#get-rid-of-all-5-star-customer-feedback'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#get-rid-of-all-5-star-customer-feedback'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html'
key: feedbackChallenge
+-
+ name: 'Forged Coupon'
+ category: 'Cryptographic Issues'
+ tags:
+ - Good for Demos
+ - Code Analysis
+ description: 'Forge a coupon code that gives you a discount of at least 80%.'
+ difficulty: 6
+ hint: 'Try either a) a knowledgable brute force attack or b) reverse engineering or c) some research in the cloud.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/cryptographic-issues.html#forge-a-coupon-code-that-gives-you-a-discount-of-at-least-80'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html'
+ key: forgedCouponChallenge
-
name: 'Forged Feedback'
category: 'Broken Access Control'
- description: 'Post some feedback in another users name.'
+ tags:
+ - Tutorial
+ description: "Post some feedback in another user's name."
difficulty: 3
hint: 'You can solve this by tampering with the user interface or by intercepting the communication with the RESTful backend.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#post-some-feedback-in-another-users-name'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#post-some-feedback-in-another-users-name'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html'
key: forgedFeedbackChallenge
+ tutorial:
+ order: 8
-
- name: 'Redirects Tier 1'
- category: 'Forgotten Content'
- description: 'Let us redirect you to a donation site that went out of business.'
- difficulty: 1
- hint: 'We might have failed to take this out of our code properly.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/forgotten-content.html#let-us-redirect-you-to-a-donation-site-that-went-out-of-business'
- key: redirectGratipayChallenge
--
- name: 'Redirects Tier 2'
- category: 'Roll your own Security'
- description: 'Wherever you go, there you are.'
- difficulty: 4
- hint: 'You have to find a way to beat the whitelist of allowed redirect URLs.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/roll-your-own-security.html#wherever-you-go-there-you-are'
- key: redirectChallenge
--
- name: 'Basket Access Tier 1'
- category: 'Broken Access Control'
- description: 'View another user''s shopping basket.'
- difficulty: 2
- hint: 'Have an eye on the HTTP traffic while shopping. Alternatively try to find a client-side association of users to their basket.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#access-someone-elses-basket'
- key: basketAccessChallenge
--
- name: 'Basket Access Tier 2'
+ name: 'Forged Review'
category: 'Broken Access Control'
- description: 'Manipulate another user''s basket.'
- difficulty: 3
- hint: 'Have an eye on the HTTP traffic while adding items to basket.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#manipulate-someone-elses-basket'
- key: basketManipulateChallenge
--
- name: 'Payback Time'
- category: 'Improper Input Validation'
- description: 'Place an order that makes you rich.'
+ description: 'Post a product review as another user or edit any user''s existing review.'
difficulty: 3
- hint: 'You literally need to make the shop owe you any amount of money.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/improper-input-validation.html#place-an-order-that-makes-you-rich'
- key: negativeOrderChallenge
+ hint: 'Observe the flow of product review posting and editing and see if you can exploit it.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#post-a-product-review-as-another-user-or-edit-any-users-existing-review'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html'
+ key: forgedReviewChallenge
-
- name: 'Confidential Document'
- category: 'Sensitive Data Exposure'
- description: 'Access a confidential document.'
- difficulty: 1
- hint: 'Analyze and tamper with links in the application that deliver a file directly.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#access-a-confidential-document'
- key: directoryListingChallenge
+ name: 'Forged Signed JWT'
+ category: 'Vulnerable Components'
+ description: 'Forge an almost properly RSA-signed JWT token that impersonates the (non-existing) user rsa_lord@juice-sh.op.'
+ difficulty: 6
+ hint: 'This challenge is explicitly not about acquiring the RSA private key used for JWT signing.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/vulnerable-components.html#forge-an-almost-properly-rsa-signed-jwt-token'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html'
+ key: jwtForgedChallenge
+ disabledEnv:
+ - Windows
-
name: 'Forgotten Developer Backup'
- category: 'Roll your own Security'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - Contraption
+ - Good for Demos
+ - Prerequisite
description: 'Access a developer''s forgotten backup file.'
difficulty: 4
hint: 'You need to trick a security mechanism into thinking that the file you want has a valid file type.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/roll-your-own-security.html#access-a-developers-forgotten-backup-file'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#access-a-developers-forgotten-backup-file'
+ mitigationUrl: ~
key: forgottenDevBackupChallenge
-
name: 'Forgotten Sales Backup'
- category: 'Security Misconfiguration'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - Contraption
description: 'Access a salesman''s forgotten backup file.'
- difficulty: 3
+ difficulty: 4
hint: 'You need to trick a security mechanism into thinking that the file you want has a valid file type.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/security-misconfiguration.html#access-a-salesmans-forgotten-backup-file'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#access-a-salesmans-forgotten-backup-file'
+ mitigationUrl: ~
key: forgottenBackupChallenge
-
- name: 'Admin Section'
- category: 'Broken Access Control'
- description: 'Access the administration section of the store.'
- difficulty: 1
- hint: 'It is just slightly harder to find than the score board link.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#access-the-administration-section-of-the-store'
- key: adminSectionChallenge
+ name: 'Frontend Typosquatting'
+ category: 'Vulnerable Components'
+ description: 'Inform the shop about a typosquatting imposter that dug itself deep into the frontend. (Mention the exact name of the culprit)'
+ difficulty: 5
+ hint: 'This challenge has nothing to do with mistyping web domains. There is no conveniently misplaced file helping you with this one either. Or is there?'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/vulnerable-components.html#inform-the-shop-about-a-typosquatting-imposter-that-dug-itself-deep-into-the-frontend'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Vulnerable_Dependency_Management_Cheat_Sheet.html'
+ key: typosquattingAngularChallenge
-
- name: 'Product Tampering'
- category: 'Broken Access Control'
- description: 'Change the href
of the link within the OWASP SSL Advanced Forensic Tool (O-Saft) product description into http://kimminich.de.'
+ name: 'GDPR Data Erasure'
+ category: 'Broken Authentication'
+ description: 'Log in with Chris'' erased user account.'
difficulty: 3
- hint: 'Look for one of the following: a) broken admin functionality, b) holes in RESTful API or c) possibility for SQL Injection.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#change-the-href-of-the-link-within-the-o-saft-product-description'
- key: changeProductChallenge
+ hint: 'Turns out that something is technically and legally wrong with the implementation of the "right to be forgotten" for users.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#log-in-with-chris-erased-user-account'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/User_Privacy_Protection_Cheat_Sheet.html'
+ key: ghostLoginChallenge
-
- name: 'Vulnerable Library'
- category: 'Vulnerable Components'
- description: 'Inform the shop about a vulnerable library it is using. (Mention the exact library name and version in your comment)'
- difficulty: 4
- hint: 'Report one of two possible answers via the "Contact Us" form. Do not forget to submit the library''s version as well.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/vulnerable-components.html#inform-the-shop-about-a-vulnerable-library-it-is-using'
- key: knownVulnerableComponentChallenge
--
- name: 'Weird Crypto'
+ name: 'GDPR Data Theft'
category: 'Sensitive Data Exposure'
- description: 'Inform the shop about an algorithm or library it should definitely not use the way it does.'
- difficulty: 2
- hint: 'Report one of four possible answers via the "Contact Us" form.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#inform-the-shop-about-an-algorithm-or-library-it-should-definitely-not-use-the-way-it-does'
- key: weirdCryptoChallenge
--
- name: 'Easter Egg Tier 1'
- category: 'Roll your own Security'
- description: 'Find the hidden easter egg.'
+ description: 'Steal someone else''s personal data without using Injection.'
difficulty: 4
- hint: 'If you solved one of the three file access challenges, you already know where to find the easter egg.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/roll-your-own-security.html#find-the-hidden-easter-egg'
- key: easterEggLevelOneChallenge
+ hint: 'Trick the regular Data Export to give you more than actually belongs to you.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#steal-someone-elses-personal-data-without-using-injection'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/User_Privacy_Protection_Cheat_Sheet.html'
+ key: dataExportChallenge
-
- name: 'Easter Egg Tier 2'
- category: 'Security through Obscurity'
- description: 'Apply some advanced cryptanalysis to find the real easter egg.'
+ name: 'HTTP-Header XSS'
+ category: 'XSS'
+ tags:
+ - Danger Zone
+ description: 'Perform a persisted XSS attack with <iframe src="javascript:alert(`xss`)">
through an HTTP header.'
difficulty: 4
- hint: 'You might have to peel through several layers of tough-as-nails encryption for this challenge.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/security-through-obscurity.html#apply-some-advanced-cryptanalysis-to-find-the-real-easter-egg'
- key: easterEggLevelTwoChallenge
+ hint: 'Finding a piece of displayed information that could originate from an HTTP header is part of this challenge.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#perform-a-persisted-xss-attack-through-an-http-header'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
+ key: httpHeaderXssChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
-
- name: 'Forged Coupon'
- category: 'Sensitive Data Exposure'
- description: 'Forge a coupon code that gives you a discount of at least 80%.'
+ name: 'Imaginary Challenge'
+ category: 'Cryptographic Issues'
+ tags:
+ - Shenanigans
+ - Code Analysis
+ description: 'Solve challenge #999. Unfortunately, this challenge does not exist.'
difficulty: 6
- hint: 'Try either a) a knowledgable brute force attack or b) reverse engineering.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#forge-a-coupon-code-that-gives-you-a-discount-of-at-least-80'
- key: forgedCouponChallenge
+ hint: 'You need to trick the hacking progress persistence feature into thinking you solved challenge #999.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/cryptographic-issues.html#solve-challenge-999'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html'
+ key: continueCodeChallenge
-
- name: 'Upload Size'
- category: 'Improper Input Validation'
- description: 'Upload a file larger than 100 kB.'
- difficulty: 3
- hint: 'You can attach a small file to the "File Complaint" form. Investigate how this upload actually works.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/improper-input-validation.html#upload-a-file-larger-than-100-kb'
- key: uploadSizeChallenge
+ name: 'Leaked Access Logs'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - OSINT
+ description: 'Dumpster dive the Internet for a leaked password and log in to the original user account it belongs to. (Creating a new account with the same password does not qualify as a solution.)'
+ difficulty: 5
+ hint: 'Once you have it, a technique called "Password Spraying" might prove useful.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#dumpster-dive-the-internet-for-a-leaked-password-and-log-in-to-the-original-user-account-it-belongs-to'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Credential_Stuffing_Prevention_Cheat_Sheet.html'
+ key: dlpPasswordSprayingChallenge
-
- name: 'Upload Type'
- category: 'Improper Input Validation'
- description: 'Upload a file that has no .pdf extension.'
- difficulty: 3
- hint: 'You can attach a PDF file to the "File Complaint" form. Investigate how this upload actually works.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/improper-input-validation.html#upload-a-file-that-has-no-pdf-extension'
- key: uploadTypeChallenge
+ name: 'Leaked Unsafe Product'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - Shenanigans
+ - OSINT
+ description: 'Identify an unsafe product that was removed from the shop and inform the shop which ingredients are dangerous.'
+ difficulty: 4
+ hint: 'Your own SQLi and someone else''s Ctrl-V will be your accomplices in this challenge!'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#identify-an-unsafe-product-that-was-removed-from-the-shop-and-inform-the-shop-which-ingredients-are-dangerous'
+ mitigationUrl: ~
+ key: dlpPastebinDataLeakChallenge
-
- name: 'Arbitrary File Write'
+ name: 'Legacy Typosquatting'
category: 'Vulnerable Components'
- description: 'Overwrite the Legal Information file.'
- difficulty: 6
- hint: 'Look out for a tweet praising new functionality of the web shop.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/vulnerable-components.html#overwrite-the-legal-information-file'
- key: fileWriteChallenge
--
- name: 'Extra Language'
- category: 'Forgotten Content'
- description: 'Retrieve the language file that never made it into production.'
- difficulty: 5
- hint: 'Brute force is not the only option for this challenge, but a perfectly viable one.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/forgotten-content.html#retrieve-the-language-file-that-never-made-it-into-production'
- key: extraLanguageChallenge
--
- name: 'CAPTCHA Bypass'
- category: 'Roll your own Security'
- description: 'Submit 10 or more customer feedbacks within 10 seconds.'
- difficulty: 5
- hint: 'After finding a CAPTCHA bypass, write a script that automates feedback submission. Or open many browser tabs and be really quick.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/roll-your-own-security.html#submit-10-or-more-customer-feedbacks-within-10-seconds'
- key: captchaBypassChallenge
+ description: 'Inform the shop about a typosquatting trick it has been a victim of at least in v6.2.0-SNAPSHOT
. (Mention the exact name of the culprit)'
+ difficulty: 4
+ hint: 'This challenge has nothing to do with mistyping web domains. Investigate the forgotten developer''s backup file instead.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/vulnerable-components.html#inform-the-shop-about-a-typosquatting-trick-it-has-been-a-victim-of'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Vulnerable_Dependency_Management_Cheat_Sheet.html'
+ key: typosquattingNpmChallenge
-
- name: 'Zero Stars'
- category: 'Improper Input Validation'
- description: 'Give a devastating zero-star feedback to the store.'
- difficulty: 1
- hint: 'Before you invest time bypassing the API, you might want to play around with the UI a bit.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/improper-input-validation.html#give-a-devastating-zero-star-feedback-to-the-store'
- key: zeroStarsChallenge
+ name: 'Login Admin'
+ category: 'Injection'
+ tags:
+ - Tutorial
+ - Good for Demos
+ description: 'Log in with the administrator''s user account.'
+ difficulty: 2
+ hint: 'Try different SQL Injection attack patterns depending whether you know the admin''s email address or not.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#log-in-with-the-administrators-user-account'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html'
+ key: loginAdminChallenge
+ tutorial:
+ order: 5
-
- name: 'Imaginary Challenge'
+ name: 'Login Amy'
category: 'Sensitive Data Exposure'
- description: 'Solve challenge #999. Unfortunately, this challenge does not exist.'
- difficulty: 6
- hint: 'You need to trick the hacking progress persistence feature into thinking you solved challenge #999.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#solve-challenge-999'
- key: continueCodeChallenge
+ tags:
+ - OSINT
+ description: 'Log in with Amy''s original user credentials. (This could take 93.83 billion trillion trillion centuries to brute force, but luckily she did not read the "One Important Final Note")'
+ difficulty: 3
+ hint: 'This challenge will make you go after a needle in a haystack.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#log-in-with-amys-original-user-credentials'
+ mitigationUrl: ~
+ key: loginAmyChallenge
+-
+ name: 'Login Bender'
+ category: 'Injection'
+ tags:
+ - Tutorial
+ description: 'Log in with Bender''s user account.'
+ difficulty: 3
+ hint: 'If you know Bender''s email address, try SQL Injection. Bender''s password hash might not help you very much.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#log-in-with-benders-user-account'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html'
+ key: loginBenderChallenge
+ tutorial:
+ order: 10
-
name: 'Login Bjoern'
category: 'Broken Authentication'
+ tags:
+ - Code Analysis
description: 'Log in with Bjoern''s Gmail account without previously changing his password, applying SQL Injection, or hacking his Google account.'
difficulty: 4
- hint: 'The security flaw behind this challenge is 100% Juice Shop''s fault and 0% Google''s.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#log-in-with-bjoerns-gmail-account'
+ hint: 'The security flaw behind this challenge is 100% OWASP Juice Shop''s fault and 0% Google''s.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#log-in-with-bjoerns-gmail-account'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html'
key: oauthUserPasswordChallenge
-
- name: 'Login CISO'
- category: 'Broken Authentication'
- description: 'Exploit OAuth 2.0 to log in with the Chief Information Security Officer''s user account.'
- difficulty: 5
- hint: 'Don''t try to beat Google''s OAuth 2.0 service. Rather investigate implementation flaws on Juice Shop''s end.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#exploit-oauth-20-to-log-in-with-the-cisos-user-account'
- key: loginCisoChallenge
+ name: 'Login Jim'
+ category: 'Injection'
+ tags:
+ - Tutorial
+ description: 'Log in with Jim''s user account.'
+ difficulty: 3
+ hint: 'Try cracking Jim''s password hash if you harvested it already. Alternatively, if you know Jim''s email address, try SQL Injection.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#log-in-with-jims-user-account'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html'
+ key: loginJimChallenge
+ tutorial:
+ order: 9
+-
+ name: 'Login MC SafeSearch'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - Shenanigans
+ - OSINT
+ description: 'Log in with MC SafeSearch''s original user credentials without applying SQL Injection or any other bypass.'
+ difficulty: 2
+ hint: 'You should listen to MC''s hit song "Protect Ya Passwordz".'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#log-in-with-mc-safesearchs-original-user-credentials'
+ mitigationUrl: ~
+ key: loginRapperChallenge
-
name: 'Login Support Team'
category: 'Security Misconfiguration'
+ tags:
+ - Brute Force
+ - Code Analysis
description: 'Log in with the support team''s original user credentials without applying SQL Injection or any other bypass.'
difficulty: 6
hint: 'The underlying flaw of this challenge is a lot more human error than technical weakness.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/security-misconfiguration.html#log-in-with-the-support-teams-original-user-credentials'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/security-misconfiguration.html#log-in-with-the-support-teams-original-user-credentials'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html'
key: loginSupportChallenge
-
- name: 'Login MC SafeSearch'
+ name: 'Manipulate Basket'
+ category: 'Broken Access Control'
+ description: 'Put an additional product into another user''s shopping basket.'
+ difficulty: 3
+ hint: 'Have an eye on the HTTP traffic while placing products in the shopping basket. Changing the quantity of products already in the basket doesn''t count.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#put-an-additional-product-into-another-users-shopping-basket'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html'
+ key: basketManipulateChallenge
+-
+ name: 'Misplaced Signature File'
category: 'Sensitive Data Exposure'
- description: 'Log in with MC SafeSearch''s original user credentials without applying SQL Injection or any other bypass.'
+ tags:
+ - Good Practice
+ - Contraption
+ description: 'Access a misplaced SIEM signature file.'
+ difficulty: 4
+ hint: 'You need to trick a security mechanism into thinking that the file you want has a valid file type.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#access-a-misplaced-siem-signature-file'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html'
+ key: misplacedSignatureFileChallenge
+-
+ name: 'Multiple Likes'
+ category: 'Broken Anti Automation'
+ description: 'Like any review at least three times as the same user.'
+ difficulty: 6
+ hint: 'Punctuality is the politeness of kings.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-anti-automation.html#like-any-review-at-least-three-times-as-the-same-user'
+ mitigationUrl: ~
+ key: timingAttackChallenge
+-
+ name: 'Nested Easter Egg'
+ category: 'Cryptographic Issues'
+ tags:
+ - Shenanigans
+ - Good for Demos
+ description: 'Apply some advanced cryptanalysis to find the real easter egg.'
+ difficulty: 4
+ hint: 'You might have to peel through several layers of tough-as-nails encryption for this challenge.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/cryptographic-issues.html#apply-some-advanced-cryptanalysis-to-find-the-real-easter-egg'
+ mitigationUrl: ~
+ key: easterEggLevelTwoChallenge
+-
+ name: 'NoSQL DoS'
+ category: 'Injection'
+ tags:
+ - Danger Zone
+ description: 'Let the server sleep for some time. (It has done more than enough hard work for you)'
+ difficulty: 4
+ hint: 'This challenge is essentially a stripped-down Denial of Service (DoS) attack.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#let-the-server-sleep-for-some-time'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html'
+ key: noSqlCommandChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'NoSQL Exfiltration'
+ category: 'Injection'
+ tags:
+ - Danger Zone
+ description: 'All your orders are belong to us! Even the ones which don''t.'
+ difficulty: 5
+ hint: 'Take a close look on how the $where query operator works in MongoDB.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#all-your-orders-are-belong-to-us'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html'
+ key: noSqlOrdersChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'NoSQL Manipulation'
+ category: 'Injection'
+ description: 'Update multiple product reviews at the same time.'
+ difficulty: 4
+ hint: 'Take a close look on how the equivalent of UPDATE-statements in MongoDB work.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#update-multiple-product-reviews-at-the-same-time'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html'
+ key: noSqlReviewsChallenge
+-
+ name: 'Outdated Allowlist'
+ category: 'Unvalidated Redirects'
+ tags:
+ - Code Analysis
+ description: 'Let us redirect you to one of our crypto currency addresses which are not promoted any longer.'
+ difficulty: 1
+ hint: 'We might have failed to take this out of our code properly.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/unvalidated-redirects.html#let-us-redirect-you-to-one-of-our-crypto-currency-addresses'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html'
+ key: redirectCryptoCurrencyChallenge
+-
+ name: 'Password Strength'
+ category: 'Broken Authentication'
+ tags:
+ - Brute Force
+ - Tutorial
+ description: 'Log in with the administrator''s user credentials without previously changing them or applying SQL Injection.'
difficulty: 2
- hint: 'You should listen to MC''s hit song "Protect Ya Passwordz".'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#log-in-with-mc-safesearchs-original-user-credentials'
- key: loginRapperChallenge
+ hint: 'This one should be equally easy to a) brute force, b) crack the password hash or c) simply guess.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#log-in-with-the-administrators-user-credentials-without-previously-changing-them-or-applying-sql-injection'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html'
+ key: weakPasswordChallenge
+ tutorial:
+ order: 6
+-
+ name: 'Payback Time'
+ category: 'Improper Input Validation'
+ description: 'Place an order that makes you rich.'
+ difficulty: 3
+ hint: 'You literally need to make the shop owe you any amount of money.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#place-an-order-that-makes-you-rich'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html'
+ key: negativeOrderChallenge
-
name: 'Premium Paywall'
- category: 'Sensitive Data Exposure'
- description: ' Unlock Premium Challenge to access exclusive content.'
+ category: 'Cryptographic Issues'
+ tags:
+ - Shenanigans
+ description: ' Unlock Premium Challenge to access exclusive content.'
difficulty: 6
hint: 'You do not have to pay anything to unlock this challenge! Nonetheless, donations are very much appreciated.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#unlock-premium-challenge-to-access-exclusive-content'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/cryptographic-issues.html#unlock-premium-challenge-to-access-exclusive-content'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Key_Management_Cheat_Sheet.html'
key: premiumPaywallChallenge
-
- name: 'Reset Jim''s Password'
- category: 'Broken Authentication'
- description: 'Reset Jim''s password via the Forgot Password mechanism with the original answer to his security question.'
+ name: 'Privacy Policy'
+ category: 'Miscellaneous'
+ tags:
+ - Good Practice
+ - Tutorial
+ - Good for Demos
+ description: 'Read our privacy policy.'
+ difficulty: 1
+ hint: 'We won''t even ask you to confirm that you did. Just read it. Please. Pretty please.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/miscellaneous.html#read-our-privacy-policy'
+ mitigationUrl: ~
+ key: privacyPolicyChallenge
+ tutorial:
+ order: 4
+-
+ name: 'Privacy Policy Inspection'
+ category: 'Security through Obscurity'
+ tags:
+ - Shenanigans
+ - Good for Demos
+ description: 'Prove that you actually read our privacy policy.'
difficulty: 3
- hint: 'It''s hard for celebrities to pick a security question from a hard-coded list where the answer is not publicly exposed.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#reset-jims-password-via-the-forgot-password-mechanism'
- key: resetPasswordJimChallenge
+ hint: 'Only by visiting a special URL you can confirm that you read it carefully.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/security-through-obscurity.html#prove-that-you-actually-read-our-privacy-policy'
+ mitigationUrl: ~
+ key: privacyPolicyProofChallenge
+-
+ name: 'Product Tampering'
+ category: 'Broken Access Control'
+ description: 'Change the href
of the link within the OWASP SSL Advanced Forensic Tool (O-Saft) product description into https://owasp.slack.com.'
+ difficulty: 3
+ hint: 'Look for one of the following: a) broken admin functionality, b) holes in RESTful API or c) possibility for SQL Injection.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#change-the-href-of-the-link-within-the-o-saft-product-description'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html'
+ key: changeProductChallenge
+-
+ name: 'Reflected XSS'
+ category: 'XSS'
+ tags:
+ - Danger Zone
+ - Good for Demos
+ description: 'Perform a reflected XSS attack with <iframe src="javascript:alert(`xss`)">
.'
+ difficulty: 2
+ hint: 'Look for a url parameter where its value appears in the page it is leading to.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#perform-a-reflected-xss-attack'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
+ key: reflectedXssChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'Repetitive Registration'
+ category: 'Improper Input Validation'
+ description: 'Follow the DRY principle while registering a user.'
+ difficulty: 1
+ hint: 'You can solve this by cleverly interacting with the UI or bypassing it altogether.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#follow-the-dry-principle-while-registering-a-user'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html'
+ key: passwordRepeatChallenge
-
name: 'Reset Bender''s Password'
category: 'Broken Authentication'
+ tags:
+ - OSINT
description: 'Reset Bender''s password via the Forgot Password mechanism with the original answer to his security question.'
difficulty: 4
hint: 'Not as trivial as Jim''s but still not too difficult with some "Futurama" background knowledge.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#reset-benders-password-via-the-forgot-password-mechanism'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#reset-benders-password-via-the-forgot-password-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html'
key: resetPasswordBenderChallenge
-
- name: 'Reset Morty''s Password'
- category: 'Security Misconfiguration'
- description: 'Reset Morty''s password via the Forgot Password mechanism with his obfuscated answer to his security question.'
- difficulty: 5
- hint: 'Find a way to bypass the rate limiting and brute force the obfuscated answer to Morty''s security question.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/security-misconfiguration.html#reset-mortys-password-via-the-forgot-password-mechanism'
- key: resetPasswordMortyChallenge
--
- name: 'Reset Bjoern''s Password Tier 2'
+ name: 'Reset Bjoern''s Password'
category: 'Broken Authentication'
+ tags:
+ - OSINT
description: 'Reset the password of Bjoern''s internal account via the Forgot Password mechanism with the original answer to his security question.'
difficulty: 5
hint: 'Nothing a little bit of Facebook stalking couldn''t reveal. Might involve a historical twist.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#reset-the-password-of-bjoerns-internal-account-via-the-forgot-password-mechanism'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#reset-the-password-of-bjoerns-internal-account-via-the-forgot-password-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html'
key: resetPasswordBjoernChallenge
-
- name: 'NoSQL Injection Tier 1'
- category: 'Injection'
- description: 'Let the server sleep for some time. (It has done more than enough hard work for you)'
- difficulty: 4
- hint: 'This challenge is essentially a stripped-down Denial of Service (DoS) attack.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#let-the-server-sleep-for-some-time'
- key: noSqlCommandChallenge
--
- name: 'NoSQL Injection Tier 2'
- category: 'Injection'
- description: 'Update multiple product reviews at the same time.'
- difficulty: 4
- hint: 'Take a close look on how the equivalent of UPDATE-statements in MongoDB work.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#update-multiple-product-reviews-at-the-same-time'
- key: noSqlReviewsChallenge
+ name: 'Reset Jim''s Password'
+ category: 'Broken Authentication'
+ tags:
+ - OSINT
+ description: 'Reset Jim''s password via the Forgot Password mechanism with the original answer to his security question.'
+ difficulty: 3
+ hint: 'It''s hard for celebrities to pick a security question from a hard-coded list where the answer is not publicly exposed.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#reset-jims-password-via-the-forgot-password-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html'
+ key: resetPasswordJimChallenge
-
- name: 'NoSQL Injection Tier 3'
- category: 'Injection'
- description: 'All your orders are belong to us!'
+ name: 'Reset Morty''s Password'
+ category: 'Broken Anti Automation'
+ tags:
+ - OSINT
+ - Brute Force
+ description: 'Reset Morty''s password via the Forgot Password mechanism with his obfuscated answer to his security question.'
difficulty: 5
- hint: 'Take a close look on how the $where query operator works in MongoDB.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#all-your-orders-are-belong-to-us'
- key: noSqlOrdersChallenge
+ hint: 'Find a way to bypass the rate limiting and brute force the obfuscated answer to Morty''s security question.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-anti-automation.html#reset-mortys-password-via-the-forgot-password-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html'
+ key: resetPasswordMortyChallenge
-
name: 'Retrieve Blueprint'
- category: 'Forgotten Content'
+ category: 'Sensitive Data Exposure'
description: 'Deprive the shop of earnings by downloading the blueprint for one of its products.'
difficulty: 5
hint: 'The product you might want to give a closer look is the OWASP Juice Shop Logo (3D-printed).'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/forgotten-content.html#deprive-the-shop-of-earnings-by-downloading-the-blueprint-for-one-of-its-products'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#deprive-the-shop-of-earnings-by-downloading-the-blueprint-for-one-of-its-products'
+ mitigationUrl: ~
key: retrieveBlueprintChallenge
-
- name: 'Typosquatting Tier 1'
- category: 'Vulnerable Components'
- description: 'Inform the shop about a typosquatting trick it has become victim of. (Mention the exact name of the culprit)'
+ name: 'SSRF'
+ category: 'Broken Access Control'
+ tags:
+ - Code Analysis
+ description: 'Request a hidden resource on server through server.'
+ difficulty: 6
+ hint: 'Reverse engineering something bad can make good things happen.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#request-a-hidden-resource-on-server-through-server'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html'
+ key: ssrfChallenge
+-
+ name: 'SSTi'
+ category: 'Injection'
+ tags:
+ - Contraption
+ - Danger Zone
+ - Code Analysis
+ description: 'Infect the server with juicy malware by abusing arbitrary command execution.'
+ difficulty: 6
+ hint: '"SSTi" is a clear indicator that this has nothing to do with anything Angular. Also, make sure to use only our non-malicious malware.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#infect-the-server-with-juicy-malware-by-abusing-arbitrary-command-execution'
+ mitigationUrl: ~
+ key: sstiChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'Score Board'
+ category: 'Miscellaneous'
+ tags:
+ - Tutorial
+ - Code Analysis
+ description: 'Find the carefully hidden ''Score Board'' page.'
+ difficulty: 1
+ hint: 'Try to find a reference or clue behind the scenes. Or simply guess what URL the Score Board might have.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/score-board.html#find-the-carefully-hidden-score-board-page'
+ mitigationUrl: ~
+ key: scoreBoardChallenge
+ tutorial:
+ order: 1
+-
+ name: 'Security Policy'
+ category: 'Miscellaneous'
+ tags:
+ - Good Practice
+ description: 'Behave like any "white-hat" should before getting into the action.'
+ difficulty: 2
+ hint: 'Undoubtably you want to read our security policy before conducting any research on our application.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/miscellaneous.html#behave-like-any-white-hat-should-before-getting-into-the-action'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html'
+ key: securityPolicyChallenge
+-
+ name: 'Server-side XSS Protection'
+ category: 'XSS'
+ tags:
+ - Danger Zone
+ description: 'Perform a persisted XSS attack with <iframe src="javascript:alert(`xss`)">
bypassing a server-side security mechanism.'
difficulty: 4
- hint: 'This challenge has nothing to do with URLs or domains. Investigate the forgotten developer''s backup file instead.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/vulnerable-components.html#inform-the-shop-about-a-typosquatting-trick-it-has-become-victim-of'
- key: typosquattingNpmChallenge
+ hint: 'The "Comment" field in the "Customer Feedback" screen is where you want to put your focus on.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#perform-a-persisted-xss-attack-bypassing-a-server-side-security-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
+ key: persistedXssFeedbackChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
+-
+ name: 'Steganography'
+ category: 'Security through Obscurity'
+ tags:
+ - Shenanigans
+ description: 'Rat out a notorious character hiding in plain sight in the shop. (Mention the exact name of the character)'
+ difficulty: 4
+ hint: 'No matter how good your eyes are, you will need tool assistance for this challenge.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/security-through-obscurity.html#rat-out-a-notorious-character-hiding-in-plain-sight-in-the-shop'
+ mitigationUrl: ~
+ key: hiddenImageChallenge
+-
+ name: 'Successful RCE DoS'
+ category: 'Insecure Deserialization'
+ tags:
+ - Danger Zone
+ description: 'Perform a Remote Code Execution that occupies the server for a while without using infinite loops.'
+ difficulty: 6
+ hint: 'Your attack payload must not trigger the protection against too many iterations.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/insecure-deserialization.html#perform-a-remote-code-execution-that-occupies-the-server-for-a-while-without-using-infinite-loops'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html'
+ key: rceOccupyChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
-
- name: 'Typosquatting Tier 2'
+ name: 'Supply Chain Attack'
category: 'Vulnerable Components'
- description: 'Inform the shop about a more sneaky instance of typosquatting it fell for. (Mention the exact name of the culprit)'
+ tags:
+ - OSINT
+ description: 'Inform the development team about a danger to some of their credentials. (Send them the URL of the original report or an assigned CVE or another identifier of this vulnerability)'
difficulty: 5
- hint: 'This challenge has nothing to do with URLs or domains. There is no conveniently misplaced file helping you with this one. Or is there?'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/vulnerable-components.html#inform-the-shop-about-a-more-sneaky-instance-of-typosquatting-it-fell-for'
- key: typosquattingAngularChallenge
+ hint: 'This vulnerability will not affect any customer of the shop. It is aimed exclusively at its developers.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/vulnerable-components.html#inform-the-development-team-about-a-danger-to-some-of-their-credentials'
+ mitigationUrl: ~
+ key: supplyChainAttackChallenge
+-
+ name: 'Two Factor Authentication'
+ category: 'Broken Authentication'
+ description: 'Solve the 2FA challenge for user "wurstbrot". (Disabling, bypassing or overwriting his 2FA settings does not count as a solution)'
+ difficulty: 5
+ hint: 'The 2FA implementation requires to store a secret for every user. You will need to find a way to access this secret in order to solve this challenge.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-authentication.html#solve-the-2fa-challenge-for-user-wurstbrot'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html'
+ key: twoFactorAuthUnsafeSecretStorageChallenge
-
- name: 'JWT Issues Tier 1'
+ name: 'Unsigned JWT'
category: 'Vulnerable Components'
description: 'Forge an essentially unsigned JWT token that impersonates the (non-existing) user jwtn3d@juice-sh.op.'
difficulty: 5
hint: 'This challenge exploits a weird option that is supported when signing tokens with JWT.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/vulnerable-components.html#forge-an-essentially-unsigned-jwt-token'
- key: jwtTier1Challenge
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/vulnerable-components.html#forge-an-essentially-unsigned-jwt-token'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html'
+ key: jwtUnsignedChallenge
-
- name: 'JWT Issues Tier 2'
- category: 'Vulnerable Components'
- description: 'Forge an almost properly RSA-signed JWT token that impersonates the (non-existing) user rsa_lord@juice-sh.op.'
+ name: 'Upload Size'
+ category: 'Improper Input Validation'
+ description: 'Upload a file larger than 100 kB.'
+ difficulty: 3
+ hint: 'You can attach a small file to the "Complaint" form. Investigate how this upload actually works.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#upload-a-file-larger-than-100-kb'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html'
+ key: uploadSizeChallenge
+-
+ name: 'Upload Type'
+ category: 'Improper Input Validation'
+ description: 'Upload a file that has no .pdf or .zip extension.'
+ difficulty: 3
+ hint: 'You can attach a PDF or ZIP file to the "Complaint" form. Investigate how this upload actually works.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#upload-a-file-that-has-no-pdf-or-zip-extension'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html'
+ key: uploadTypeChallenge
+-
+ name: 'User Credentials'
+ category: 'Injection'
+ description: 'Retrieve a list of all user credentials via SQL Injection.'
+ difficulty: 4
+ hint: 'Gather information on where user data is stored and how it is addressed. Then craft a corresponding UNION SELECT attack.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#retrieve-a-list-of-all-user-credentials-via-sql-injection'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html'
+ key: unionSqlInjectionChallenge
+-
+ name: 'Video XSS'
+ category: 'XSS'
+ tags:
+ - Danger Zone
+ description: 'Embed an XSS payload </script><script>alert(`xss`)</script>
into our promo video.'
difficulty: 6
- hint: 'This challenge is explicitly not about acquiring the RSA private key used for JWT signing.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/vulnerable-components.html#forge-an-almost-properly-rsa-signed-jwt-token'
- key: jwtTier2Challenge
+ hint: 'You have to reuse the vulnerability behind one other 6-star challenge to be able to solve this one.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#embed-an-xss-payload-into-our-promo-video'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html'
+ key: videoXssChallenge
+ disabledEnv:
+ - Docker
+ - Heroku
+ - Gitpod
-
- name: 'Misplaced Signature File'
- category: 'Roll your own Security'
- description: 'Access a misplaced SIEM signature file.'
+ name: 'View Basket'
+ category: 'Broken Access Control'
+ tags:
+ - Tutorial
+ - Good for Demos
+ description: 'View another user''s shopping basket.'
+ difficulty: 2
+ hint: 'Have an eye on the HTTP traffic while shopping. Alternatively try to find a client-side association of users to their basket.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#view-another-users-shopping-basket'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html'
+ key: basketAccessChallenge
+ tutorial:
+ order: 7
+-
+ name: 'Vulnerable Library'
+ category: 'Vulnerable Components'
+ tags:
+ - OSINT
+ description: 'Inform the shop about a vulnerable library it is using. (Mention the exact library name and version in your comment)'
difficulty: 4
- hint: 'You need to trick a security mechanism into thinking that the file you want has a valid file type.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/roll-your-own-security.html#access-a-misplaced-siem-signature-file'
- key: misplacedSignatureFileChallenge
+ hint: 'Report one of two possible answers via the "Customer Feedback" form. Do not forget to submit the library''s version as well.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/vulnerable-components.html#inform-the-shop-about-a-vulnerable-library-it-is-using'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html'
+ key: knownVulnerableComponentChallenge
-
- name: 'Deprecated Interface'
- category: 'Forgotten Content'
- description: 'Use a deprecated B2B interface that was not properly shut down.'
+ name: 'Weird Crypto'
+ category: 'Cryptographic Issues'
+ description: 'Inform the shop about an algorithm or library it should definitely not use the way it does.'
difficulty: 2
- hint: 'The developers who disabled the interface think they could go invisible by just closing their eyes.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/forgotten-content.html#use-a-deprecated-b2b-interface-that-was-not-properly-shut-down'
- key: deprecatedInterfaceChallenge
+ hint: 'Report one of four possible answers via the "Customer Feedback" form.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/cryptographic-issues.html#inform-the-shop-about-an-algorithm-or-library-it-should-definitely-not-use-the-way-it-does'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html'
+ key: weirdCryptoChallenge
+-
+ name: 'Allowlist Bypass'
+ category: 'Unvalidated Redirects'
+ tags:
+ - Prerequisite
+ description: 'Enforce a redirect to a page you are not supposed to redirect to.'
+ difficulty: 4
+ hint: 'You have to find a way to beat the allowlist of allowed redirect URLs.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/unvalidated-redirects.html#enforce-a-redirect-to-a-page-you-are-not-supposed-to-redirect-to'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html'
+ key: redirectChallenge
-
- name: 'XXE Tier 1'
+ name: 'XXE Data Access'
category: 'XXE'
+ tags:
+ - Danger Zone
description: 'Retrieve the content of C:\Windows\system.ini
or /etc/passwd
from the server.'
difficulty: 3
hint: 'The leverage point for this challenge is the deprecated B2B interface.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xxe.html#retrieve-the-content-of-cwindowssystemini-or-etcpasswd-from-the-server'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xxe.html#retrieve-the-content-of-cwindowssystemini-or-etcpasswd-from-the-server'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html'
key: xxeFileDisclosureChallenge
disabledEnv:
- Docker
- Heroku
+ - Gitpod
-
- name: 'XXE Tier 2'
+ name: 'XXE DoS'
category: 'XXE'
+ tags:
+ - Danger Zone
description: 'Give the server something to chew on for quite a while.'
difficulty: 5
hint: 'It is not as easy as sending a large amount of data directly to the deprecated B2B interface.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xxe.html#give-the-server-something-to-chew-on-for-quite-a-while'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xxe.html#give-the-server-something-to-chew-on-for-quite-a-while'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html'
key: xxeDosChallenge
disabledEnv:
- Docker
- Heroku
+ - Gitpod
-
- name: 'RCE Tier 1'
- category: 'Insecure Deserialization'
- description: 'Perform a Remote Code Execution that would keep a less hardened application busy forever.'
- difficulty: 5
- hint: 'The feature you need to exploit for this challenge is not directly advertised anywhere.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/insecure-deserialization.html#perform-a-remote-code-execution-that-would-keep-a-less-hardened-application-busy-forever'
- key: rceChallenge
--
- name: 'RCE Tier 2'
- category: 'Insecure Deserialization'
- description: 'Perform a Remote Code Execution that occupies the server for a while without using infinite loops.'
- difficulty: 6
- hint: 'Your attack payload must not trigger the protection against too many iterations.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/insecure-deserialization.html#perform-a-remote-code-execution-that-occupies-the-server-for-a-while-without-using-infinite-loops'
- key: rceOccupyChallenge
--
- name: 'Blockchain Tier 1'
- category: 'Security through Obscurity'
- description: 'Learn about the Token Sale before its official announcement.'
- difficulty: 3
- hint: 'The developers truly believe in "Security through Obscurity" over actual access restrictions.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/security-through-obscurity.html#learn-about-the-token-sale-before-its-official-announcement'
- key: tokenSaleChallenge
--
- name: 'Security Policy'
- category: 'Roll your own Security'
- description: 'Behave like any "white-hat" should.'
- difficulty: 2
- hint: 'Undoubtably you want to read our security policy before conducting any research on our application.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/roll-your-own-security.html#behave-like-any-white-hat-should'
- key: securityPolicyChallenge
+ name: 'Zero Stars'
+ category: 'Improper Input Validation'
+ description: 'Give a devastating zero-star feedback to the store.'
+ difficulty: 1
+ hint: 'Before you invest time bypassing the API, you might want to play around with the UI a bit.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#give-a-devastating-zero-star-feedback-to-the-store'
+ mitigationUrl: ~
+ key: zeroStarsChallenge
-
- name: 'Steganography Tier 1'
- category: 'Security through Obscurity'
- description: 'Rat out a notorious character hiding in plain sight in the shop. (Mention the exact name of the character)'
- difficulty: 4
- hint: 'No matter how good your eyes are, you will need tool assistance for this challenge.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/security-through-obscurity.html#rat-out-a-notorious-character-hiding-in-plain-sight-in-the-shop'
- key: hiddenImageChallenge
+ name: 'Missing Encoding'
+ category: 'Improper Input Validation'
+ tags:
+ - Shenanigans
+ description: 'Retrieve the photo of Bjoern''s cat in "melee combat-mode".'
+ difficulty: 1
+ hint: 'Check the Photo Wall for an image that could not be loaded correctly.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#retrieve-the-photo-of-bjoerns-cat-in-melee-combat-mode'
+ mitigationUrl: ~
+ key: missingEncodingChallenge
-
- name: 'Supply Chain Attack'
- category: 'Broken Authentication'
- description: 'Inform the development team about a danger to some of their credentials. (Send them the URL of the original report or the CVE of this vulnerability)'
+ name: 'Cross-Site Imaging'
+ category: 'Security Misconfiguration'
+ tags:
+ - Contraption
+ description: 'Stick cute cross-domain kittens all over our delivery boxes.'
difficulty: 5
- hint: 'This vulnerability will not affect any customer of the shop. It is aimed exclusively at its developers.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#inform-the-development-team-about-a-danger-to-some-of-their-credentials'
- key: supplyChainAttackChallenge
--
- name: 'Christmas Special'
- category: 'Injection'
- description: 'Order the Christmas special offer of 2014.'
- difficulty: 4
- hint: 'Find out how the application handles unavailable products and try to find a loophole.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#order-the-christmas-special-offer-of-2014'
- key: christmasSpecialChallenge
+ hint: 'This challenge would formally have to be in several categories as the developers made multiple gaffes for this to be possible.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/injection.html#stick-cute-cross-domain-kittens-all-over-our-delivery-boxes'
+ mitigationUrl: ~
+ key: svgInjectionChallenge
-
- name: 'User Credentials'
- category: 'Injection'
- description: 'Retrieve a list of all user credentials via SQL Injection'
- difficulty: 4
- hint: 'Craft a UNION SELECT attack string against an endpoint that offers an unnecessary way to filter data.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#retrieve-a-list-of-all-user-credentials-via-sql-injection'
- key: unionSqlInjectionChallenge
+ name: 'Exposed Metrics'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - Good Practice
+ description: 'Find the endpoint that serves usage data to be scraped by a popular monitoring system.'
+ difficulty: 1
+ hint: 'Try to guess what URL the endpoint might have.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#find-the-endpoint-that-serves-usage-data-to-be-scraped-by-a-popular-monitoring-system'
+ mitigationUrl: ~
+ key: exposedMetricsChallenge
-
- name: 'Admin Registration'
+ name: 'Deluxe Fraud'
category: 'Improper Input Validation'
- description: 'Get registered as admin user.'
+ description: 'Obtain a Deluxe Membership without paying for it.'
difficulty: 3
- hint: 'Assign the unassignable.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/improper-input-validation.html#get-registered-as-admin-user'
- key: registerAdminChallenge
+ hint: 'Look closely at what happens when you attempt to upgrade your account.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#obtain-a-deluxe-membership-without-paying-for-it'
+ mitigationUrl: ~
+ key: freeDeluxeChallenge
-
- name: 'XSS Tier 0'
- category: 'XSS'
- description: 'Perform a reflected XSS attack with <iframe src="javascript:alert(`xss`)">
.'
- difficulty: 1
- hint: 'Look for an input field where its content appears in the response HTML when its form is submitted.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xss.html#perform-a-reflected-xss-attack'
- key: reflectedXssChallenge
+ name: 'CSRF' # FIXME No e2e test automation! No longer works in Chrome >=80 and Firefox >=100 or other latest browsers!
+ category: 'Broken Access Control'
+ description: 'Change the name of a user by performing Cross-Site Request Forgery from another origin.'
+ difficulty: 3
+ hint: 'Find a form which updates the username and then construct a malicious page in the online HTML editor. You probably need an older browser version for this.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/broken-access-control.html#change-the-name-of-a-user-by-performing-cross-site-request-forgery-from-another-origin'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html'
+ key: csrfChallenge
-
- name: 'XSS Tier 1'
+ name: 'Bonus Payload'
category: 'XSS'
- description: 'Perform a DOM XSS attack with <iframe src="javascript:alert(`xss`)">
.'
+ tags:
+ - Shenanigans
+ - Tutorial
+ description: 'Use the bonus payload <iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/771984076&color=%23ff5500&auto_play=true&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true"></iframe>
in the DOM XSS challenge.'
difficulty: 1
- hint: 'Look for an input field where its content appears in the HTML when its form is submitted.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xss.html#perform-a-reflected-xss-attack'
- key: localXssChallenge
--
- name: 'XSS Tier 2'
- category: 'XSS'
- description: 'Perform a persisted XSS attack with <iframe src="javascript:alert(`xss`)">
bypassing a client-side security mechanism.'
- difficulty: 3
- hint: 'Only some input fields validate their input. Even less of these are persisted in a way where their content is shown on another screen.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xss.html#perform-a-persisted-xss-attack-bypassing-a-client-side-security-mechanism'
- key: persistedXssChallengeUser
+ hint: 'Copy + Paste = Solved!'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/xss.html#use-the-bonus-payload-in-the-dom-xss-challenge'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html'
+ key: xssBonusChallenge
+ tutorial:
+ order: 3
+-
+ name: 'Reset Uvogin''s Password'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - OSINT
+ description: 'Reset Uvogin''s password via the Forgot Password mechanism with the original answer to his security question.'
+ difficulty: 4
+ hint: 'You might have to do some OSINT on his social media personas to find out his honest answer to the security question.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#reset-uvogins-password-via-the-forgot-password-mechanism'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html'
+ key: resetPasswordUvoginChallenge
-
- name: 'XSS Tier 3'
- category: 'XSS'
- description: 'Perform a persisted XSS attack with <iframe src="javascript:alert(`xss`)">
without using the frontend application at all.'
- difficulty: 3
- hint: 'You need to work with the server-side API directly. Try different HTTP verbs on different entities exposed through the API.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xss.html#perform-a-persisted-xss-attack-without-using-the-frontend-application-at-all'
- key: restfulXssChallenge
+ name: 'Meta Geo Stalking'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - OSINT
+ description: 'Determine the answer to John''s security question by looking at an upload of him to the Photo Wall and use it to reset his password via the Forgot Password mechanism.'
+ difficulty: 2
+ hint: 'Take a look at the meta data of the corresponding photo.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#determine-the-answer-to-johns-security-question'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html'
+ key: geoStalkingMetaChallenge
-
- name: 'XSS Tier 4'
- category: 'XSS'
- description: 'Perform a persisted XSS attack with <iframe src="javascript:alert(`xss`)">
bypassing a server-side security mechanism.'
- difficulty: 4
- hint: 'The "Comment" field in the "Contact Us" screen is where you want to put your focus on.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xss.html#perform-a-persisted-xss-attack-bypassing-a-server-side-security-mechanism'
- key: persistedXssChallengeFeedback
+ name: 'Visual Geo Stalking'
+ category: 'Sensitive Data Exposure'
+ tags:
+ - OSINT
+ description: 'Determine the answer to Emma''s security question by looking at an upload of her to the Photo Wall and use it to reset her password via the Forgot Password mechanism.'
+ difficulty: 2
+ hint: 'Take a look at the details in the photo to determine the location of where it was taken.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/sensitive-data-exposure.html#determine-the-answer-to-emmas-security-question'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html'
+ key: geoStalkingVisualChallenge
-
- name: 'CSRF'
- category: 'Broken Authentication'
- description: 'Change Bender''s password into slurmCl4ssic without using SQL Injection.'
+ name: 'Kill Chatbot'
+ category: 'Vulnerable Components'
+ tags:
+ - Code Analysis
+ description: 'Permanently disable the support chatbot so that it can no longer answer customer queries.'
difficulty: 5
- hint: 'The fact that the name of this challenge is "CSRF" is already a huge hint.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#change-benders-password-into-slurmcl4ssic-without-using-sql-injection'
- key: csrfChallenge
+ hint: 'Think of a way to get a hold of the internal workings on the chatbot API.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#permanently-disable-the-support-chatbot'
+ mitigationUrl: 'https://cheatsheetseries.owasp.org/cheatsheets/Vulnerable_Dependency_Management_Cheat_Sheet.html'
+ key: killChatbotChallenge
-
- name: 'XSS Tier 5'
- category: 'XSS'
- description: 'Perform a persisted XSS attack with <iframe src="javascript:alert(`xss`)">
through an HTTP header.'
+ name: 'Poison Null Byte'
+ category: 'Improper Input Validation'
+ tags:
+ - Prerequisite
+ description: 'Bypass a security control with a Poison Null Byte to access a file not meant for your eyes.'
difficulty: 4
- hint: 'Finding a piece of displayed information that could originate from an HTTP header is the part of this challenge.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/xss.html#perform-a-persisted-xss-attack-through-an-http-header'
- key: httpHeaderXssChallenge
--
- name: 'Multiple Likes'
- category: 'Race Condition'
- description: 'Like any review at least three times as the same user.'
- difficulty: 6
- hint: 'Punctuality is the politeness of kings.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/race-condition.html#like-any-review-at-least-three-times-as-the-same-user'
- key: timingAttackChallenge
+ hint: 'Take a look at the details in the photo to determine the location of where it was taken.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/improper-input-validation.html#bypass-a-security-control-with-a-poison-null-byte'
+ mitigationUrl: ~
+ key: nullByteChallenge
+-
+ name: 'Bully Chatbot'
+ category: 'Miscellaneous'
+ tags:
+ - Shenanigans
+ - Brute Force
+ description: 'Receive a coupon code from the support chatbot.'
+ difficulty: 1
+ hint: 'Just keep asking.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/miscellaneous.html#receive-a-coupon-code-from-the-support-chatbot'
+ mitigationUrl: ~
+ key: bullyChatbotChallenge
-
- name: 'SSTi'
- category: 'Injection'
- description: 'Infect the server with juicy malware by abusing arbitrary command execution.'
- difficulty: 6
- hint: 'The search for a personal identity is the life of a man.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/injection.html#infect-the-server-with-malware-by-abusing-arbitrary-command-execution'
- key: sstiChallenge
+ name: 'Local File Read'
+ category: 'Vulnerable Components'
+ tags:
+ - OSINT
+ - Danger Zone
+ difficulty: 5
+ hint: 'You should read up on vulnerabilities in popular NodeJs template engines.'
+ description: 'Gain read access to an arbitrary local file on the web server.'
+ key: 'lfrChallenge'
disabledEnv:
- - Docker
- - Heroku
--
- name: 'SSRF'
- category: 'Broken Access Control'
- description: 'Request a hidden resource on server through server.'
- difficulty: 6
- hint: 'Reverse the Bad and it gets Good again.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-access-control.html#request-a-hidden-resource-on-server-through-server'
- key: ssrfChallenge
--
- name: 'Login Amy'
- category: 'Sensitive Data Exposure'
- description: 'Log in with Amy''s original user credentials. (This could take 93.83 billion trillion trillion centuries to brute force, but luckily she did not read the "One Important Final Note")'
- difficulty: 3
- hint: 'This challenge will make you go after a needle in a haystack.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/sensitive-data-exposure.html#log-in-with-amys-original-user-credentials'
- key: loginAmyChallenge
+ - Docker
+ - Heroku
+ - Gitpod
-
- name: 'Reset Bjoern''s Password Tier 1'
- category: 'Broken Authentication'
- description: 'Reset the password of Bjoern''s OWASP account via the Forgot Password mechanism with the original answer to his security question.'
- difficulty: 3
- hint: 'He might have spoilered it on at least one occasion where a camera was running. Maybe elsewhere as well.'
- hintUrl: 'https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part2/broken-authentication.html#reset-the-password-of-bjoerns-owasp-account-via-the-forgot-password-mechanism'
- key: resetPasswordBjoernOwaspChallenge
+ name: 'Mass Dispel'
+ category: 'Miscellaneous'
+ description: 'Close multiple "Challenge solved"-notifications in one go.'
+ difficulty: 1
+ hint: 'Either check the official documentation or inspect a notification UI element directly.'
+ hintUrl: 'https://pwning.owasp-juice.shop/part2/score-board.html#close-multiple-challenge-solved-notifications-in-one-go'
+ mitigationUrl: ~
+ key: closeNotificationsChallenge
diff --git a/data/static/codefixes/.editorconfig b/data/static/codefixes/.editorconfig
new file mode 100644
index 00000000000..33459fe66c6
--- /dev/null
+++ b/data/static/codefixes/.editorconfig
@@ -0,0 +1,4 @@
+[*]
+indent_style = space
+indent_size = 2
+insert_final_newline = false
\ No newline at end of file
diff --git a/data/static/codefixes/accessLogDisclosureChallenge.info.yml b/data/static/codefixes/accessLogDisclosureChallenge.info.yml
new file mode 100644
index 00000000000..6eada01a84c
--- /dev/null
+++ b/data/static/codefixes/accessLogDisclosureChallenge.info.yml
@@ -0,0 +1,14 @@
+fixes:
+ - id: 1
+ explanation: 'There should generally be no good reason to expose server logs through a web URL of the server itself, epecially not when that server is Internet-facing.'
+ - id: 2
+ explanation: "Switching off the detailed view option is a cosmetic change on the directory listing but still allows the logs to be browsed and accessed."
+ - id: 3
+ explanation: 'Removing the route that serves individual log files is likely to plumb the data leak but still provides information to the attacker unnecessarily.'
+ - id: 4
+ explanation: 'Removing only the directory listing will still allow attackers to download individual log files if they can come up with a valid file name.'
+hints:
+ - "Can you identify one or more routes which have something to do with log files?"
+ - "Did you spot the directory listing clearly linked to log files?"
+ - "Did you notice that there is a seperate route for retrieving individual log files?"
+ - "Make sure to select both lines responsible for the log file data leakage."
diff --git a/data/static/codefixes/accessLogDisclosureChallenge_1_correct.ts b/data/static/codefixes/accessLogDisclosureChallenge_1_correct.ts
new file mode 100644
index 00000000000..c44c1abc917
--- /dev/null
+++ b/data/static/codefixes/accessLogDisclosureChallenge_1_correct.ts
@@ -0,0 +1,14 @@
+/* /ftp directory browsing and file download */
+ app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true }))
+ app.use('/ftp(?!/quarantine)/:file', fileServer())
+ app.use('/ftp/quarantine/:file', quarantineServer())
+
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/accessLogDisclosureChallenge_2.ts b/data/static/codefixes/accessLogDisclosureChallenge_2.ts
new file mode 100644
index 00000000000..700c150bf5f
--- /dev/null
+++ b/data/static/codefixes/accessLogDisclosureChallenge_2.ts
@@ -0,0 +1,18 @@
+/* /ftp directory browsing and file download */
+ app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true }))
+ app.use('/ftp(?!/quarantine)/:file', fileServer())
+ app.use('/ftp/quarantine/:file', quarantineServer())
+
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* /logs directory browsing */
+ app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true }))
+ app.use('/support/logs/:file', logFileServer())
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/accessLogDisclosureChallenge_3.ts b/data/static/codefixes/accessLogDisclosureChallenge_3.ts
new file mode 100644
index 00000000000..cf855df8cab
--- /dev/null
+++ b/data/static/codefixes/accessLogDisclosureChallenge_3.ts
@@ -0,0 +1,17 @@
+/* /ftp directory browsing and file download */
+ app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true }))
+ app.use('/ftp(?!/quarantine)/:file', fileServer())
+ app.use('/ftp/quarantine/:file', quarantineServer())
+
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* /logs directory browsing */
+ app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' }))
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/accessLogDisclosureChallenge_4.ts b/data/static/codefixes/accessLogDisclosureChallenge_4.ts
new file mode 100644
index 00000000000..bf9432cff89
--- /dev/null
+++ b/data/static/codefixes/accessLogDisclosureChallenge_4.ts
@@ -0,0 +1,17 @@
+/* /ftp directory browsing and file download */
+ app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true }))
+ app.use('/ftp(?!/quarantine)/:file', fileServer())
+ app.use('/ftp/quarantine/:file', quarantineServer())
+
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* /logs directory browsing */
+ app.use('/support/logs/:file', logFileServer())
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/adminSectionChallenge.info.yml b/data/static/codefixes/adminSectionChallenge.info.yml
new file mode 100644
index 00000000000..baff53a72f0
--- /dev/null
+++ b/data/static/codefixes/adminSectionChallenge.info.yml
@@ -0,0 +1,12 @@
+fixes:
+ - id: 1
+ explanation: 'While attempts could be made to limit access to administrative functions of a web shop through access control, it is definitely safer to apply the "separation of concerns" pattern more strictly by internally hosting a distinct admin backend application with no Internet exposure.'
+ - id: 2
+ explanation: "Obfuscating the path to the administration section does not add any security, even if it wasn't just a trivial Base64 encoding."
+ - id: 3
+ explanation: 'This obfuscation attempt is hard to undo by hand but trivial when executed in a JavaScript console. Regardless, obfuscating the route does not add any level of security.'
+ - id: 4
+ explanation: 'Assuming that the original "AdminGuard" provided access control only to admin users, switching to "LoginGuard" seems like a downgrade that would give access to any authenticated user.'
+hints:
+ - "Among the long list of route mappings, can you spot any that seem responsible for admin-related functionality?"
+ - "Luckily the route mappings were originally in alphabetical order before the developers forgot about that rule at some point."
diff --git a/data/static/codefixes/adminSectionChallenge_1_correct.ts b/data/static/codefixes/adminSectionChallenge_1_correct.ts
new file mode 100644
index 00000000000..5273237b97d
--- /dev/null
+++ b/data/static/codefixes/adminSectionChallenge_1_correct.ts
@@ -0,0 +1,178 @@
+const routes: Routes = [
+ /* TODO: Externalize admin functions into separate application
+ that is only accessible inside corporate network.
+ */
+ // {
+ // path: 'administration',
+ // component: AdministrationComponent,
+ // canActivate: [AdminGuard]
+ // },
+ {
+ path: 'accounting',
+ component: AccountingComponent,
+ canActivate: [AccountingGuard]
+ },
+ {
+ path: 'about',
+ component: AboutComponent
+ },
+ {
+ path: 'address/select',
+ component: AddressSelectComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/saved',
+ component: SavedAddressComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/create',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/edit/:addressId',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'delivery-method',
+ component: DeliveryMethodComponent
+ },
+ {
+ path: 'deluxe-membership',
+ component: DeluxeUserComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'saved-payment-methods',
+ component: SavedPaymentMethodsComponent
+ },
+ {
+ path: 'basket',
+ component: BasketComponent
+ },
+ {
+ path: 'order-completion/:id',
+ component: OrderCompletionComponent
+ },
+ {
+ path: 'contact',
+ component: ContactComponent
+ },
+ {
+ path: 'photo-wall',
+ component: PhotoWallComponent
+ },
+ {
+ path: 'complain',
+ component: ComplaintComponent
+ },
+ {
+ path: 'chatbot',
+ component: ChatbotComponent
+ },
+ {
+ path: 'order-summary',
+ component: OrderSummaryComponent
+ },
+ {
+ path: 'order-history',
+ component: OrderHistoryComponent
+ },
+ {
+ path: 'payment/:entity',
+ component: PaymentComponent
+ },
+ {
+ path: 'wallet',
+ component: WalletComponent
+ },
+ {
+ path: 'login',
+ component: LoginComponent
+ },
+ {
+ path: 'forgot-password',
+ component: ForgotPasswordComponent
+ },
+ {
+ path: 'recycle',
+ component: RecycleComponent
+ },
+ {
+ path: 'register',
+ component: RegisterComponent
+ },
+ {
+ path: 'search',
+ component: SearchResultComponent
+ },
+ {
+ path: 'hacking-instructor',
+ component: SearchResultComponent
+ },
+ {
+ path: 'score-board',
+ component: ScoreBoardComponent
+ },
+ {
+ path: 'track-result',
+ component: TrackResultComponent
+ },
+ {
+ path: 'track-result/new',
+ component: TrackResultComponent,
+ data: {
+ type: 'new'
+ }
+ },
+ {
+ path: '2fa/enter',
+ component: TwoFactorAuthEnterComponent
+ },
+ {
+ path: 'privacy-security',
+ component: PrivacySecurityComponent,
+ children: [
+ {
+ path: 'privacy-policy',
+ component: PrivacyPolicyComponent
+ },
+ {
+ path: 'change-password',
+ component: ChangePasswordComponent
+ },
+ {
+ path: 'two-factor-authentication',
+ component: TwoFactorAuthComponent
+ },
+ {
+ path: 'data-export',
+ component: DataExportComponent
+ },
+ {
+ path: 'last-login-ip',
+ component: LastLoginIpComponent
+ }
+ ]
+ },
+ {
+ matcher: oauthMatcher,
+ data: { params: (window.location.href).substr(window.location.href.indexOf('#')) },
+ component: OAuthComponent
+ },
+ {
+ matcher: tokenMatcher,
+ component: TokenSaleComponent
+ },
+ {
+ path: '403',
+ component: ErrorPageComponent
+ },
+ {
+ path: '**',
+ component: SearchResultComponent
+ }
+]
\ No newline at end of file
diff --git a/data/static/codefixes/adminSectionChallenge_2.ts b/data/static/codefixes/adminSectionChallenge_2.ts
new file mode 100644
index 00000000000..6576d14deb4
--- /dev/null
+++ b/data/static/codefixes/adminSectionChallenge_2.ts
@@ -0,0 +1,175 @@
+const routes: Routes = [
+ {
+ path: atob('YWRtaW5pc3RyYXRpb24='),
+ component: AdministrationComponent,
+ canActivate: [AdminGuard]
+ },
+ {
+ path: 'accounting',
+ component: AccountingComponent,
+ canActivate: [AccountingGuard]
+ },
+ {
+ path: 'about',
+ component: AboutComponent
+ },
+ {
+ path: 'address/select',
+ component: AddressSelectComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/saved',
+ component: SavedAddressComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/create',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/edit/:addressId',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'delivery-method',
+ component: DeliveryMethodComponent
+ },
+ {
+ path: 'deluxe-membership',
+ component: DeluxeUserComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'saved-payment-methods',
+ component: SavedPaymentMethodsComponent
+ },
+ {
+ path: 'basket',
+ component: BasketComponent
+ },
+ {
+ path: 'order-completion/:id',
+ component: OrderCompletionComponent
+ },
+ {
+ path: 'contact',
+ component: ContactComponent
+ },
+ {
+ path: 'photo-wall',
+ component: PhotoWallComponent
+ },
+ {
+ path: 'complain',
+ component: ComplaintComponent
+ },
+ {
+ path: 'chatbot',
+ component: ChatbotComponent
+ },
+ {
+ path: 'order-summary',
+ component: OrderSummaryComponent
+ },
+ {
+ path: 'order-history',
+ component: OrderHistoryComponent
+ },
+ {
+ path: 'payment/:entity',
+ component: PaymentComponent
+ },
+ {
+ path: 'wallet',
+ component: WalletComponent
+ },
+ {
+ path: 'login',
+ component: LoginComponent
+ },
+ {
+ path: 'forgot-password',
+ component: ForgotPasswordComponent
+ },
+ {
+ path: 'recycle',
+ component: RecycleComponent
+ },
+ {
+ path: 'register',
+ component: RegisterComponent
+ },
+ {
+ path: 'search',
+ component: SearchResultComponent
+ },
+ {
+ path: 'hacking-instructor',
+ component: SearchResultComponent
+ },
+ {
+ path: 'score-board',
+ component: ScoreBoardComponent
+ },
+ {
+ path: 'track-result',
+ component: TrackResultComponent
+ },
+ {
+ path: 'track-result/new',
+ component: TrackResultComponent,
+ data: {
+ type: 'new'
+ }
+ },
+ {
+ path: '2fa/enter',
+ component: TwoFactorAuthEnterComponent
+ },
+ {
+ path: 'privacy-security',
+ component: PrivacySecurityComponent,
+ children: [
+ {
+ path: 'privacy-policy',
+ component: PrivacyPolicyComponent
+ },
+ {
+ path: 'change-password',
+ component: ChangePasswordComponent
+ },
+ {
+ path: 'two-factor-authentication',
+ component: TwoFactorAuthComponent
+ },
+ {
+ path: 'data-export',
+ component: DataExportComponent
+ },
+ {
+ path: 'last-login-ip',
+ component: LastLoginIpComponent
+ }
+ ]
+ },
+ {
+ matcher: oauthMatcher,
+ data: { params: (window.location.href).substr(window.location.href.indexOf('#')) },
+ component: OAuthComponent
+ },
+ {
+ matcher: tokenMatcher,
+ component: TokenSaleComponent
+ },
+ {
+ path: '403',
+ component: ErrorPageComponent
+ },
+ {
+ path: '**',
+ component: SearchResultComponent
+ }
+]
\ No newline at end of file
diff --git a/data/static/codefixes/adminSectionChallenge_3.ts b/data/static/codefixes/adminSectionChallenge_3.ts
new file mode 100644
index 00000000000..08c8f4b826b
--- /dev/null
+++ b/data/static/codefixes/adminSectionChallenge_3.ts
@@ -0,0 +1,175 @@
+const routes: Routes = [
+ {
+ path: (function(){var t=Array.prototype.slice.call(arguments),G=t.shift();return t.reverse().map(function(e,w){return String.fromCharCode(e-G-2-w)}).join('')})(55,167,171,165,168,158,154)+(62749278960).toString(36).toLowerCase()+(function(){var b=Array.prototype.slice.call(arguments),V=b.shift();return b.reverse().map(function(l,S){return String.fromCharCode(l-V-43-S)}).join('')})(58,211),
+ component: AdministrationComponent,
+ canActivate: [AdminGuard]
+ },
+ {
+ path: 'accounting',
+ component: AccountingComponent,
+ canActivate: [AccountingGuard]
+ },
+ {
+ path: 'about',
+ component: AboutComponent
+ },
+ {
+ path: 'address/select',
+ component: AddressSelectComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/saved',
+ component: SavedAddressComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/create',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/edit/:addressId',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'delivery-method',
+ component: DeliveryMethodComponent
+ },
+ {
+ path: 'deluxe-membership',
+ component: DeluxeUserComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'saved-payment-methods',
+ component: SavedPaymentMethodsComponent
+ },
+ {
+ path: 'basket',
+ component: BasketComponent
+ },
+ {
+ path: 'order-completion/:id',
+ component: OrderCompletionComponent
+ },
+ {
+ path: 'contact',
+ component: ContactComponent
+ },
+ {
+ path: 'photo-wall',
+ component: PhotoWallComponent
+ },
+ {
+ path: 'complain',
+ component: ComplaintComponent
+ },
+ {
+ path: 'chatbot',
+ component: ChatbotComponent
+ },
+ {
+ path: 'order-summary',
+ component: OrderSummaryComponent
+ },
+ {
+ path: 'order-history',
+ component: OrderHistoryComponent
+ },
+ {
+ path: 'payment/:entity',
+ component: PaymentComponent
+ },
+ {
+ path: 'wallet',
+ component: WalletComponent
+ },
+ {
+ path: 'login',
+ component: LoginComponent
+ },
+ {
+ path: 'forgot-password',
+ component: ForgotPasswordComponent
+ },
+ {
+ path: 'recycle',
+ component: RecycleComponent
+ },
+ {
+ path: 'register',
+ component: RegisterComponent
+ },
+ {
+ path: 'search',
+ component: SearchResultComponent
+ },
+ {
+ path: 'hacking-instructor',
+ component: SearchResultComponent
+ },
+ {
+ path: 'score-board',
+ component: ScoreBoardComponent
+ },
+ {
+ path: 'track-result',
+ component: TrackResultComponent
+ },
+ {
+ path: 'track-result/new',
+ component: TrackResultComponent,
+ data: {
+ type: 'new'
+ }
+ },
+ {
+ path: '2fa/enter',
+ component: TwoFactorAuthEnterComponent
+ },
+ {
+ path: 'privacy-security',
+ component: PrivacySecurityComponent,
+ children: [
+ {
+ path: 'privacy-policy',
+ component: PrivacyPolicyComponent
+ },
+ {
+ path: 'change-password',
+ component: ChangePasswordComponent
+ },
+ {
+ path: 'two-factor-authentication',
+ component: TwoFactorAuthComponent
+ },
+ {
+ path: 'data-export',
+ component: DataExportComponent
+ },
+ {
+ path: 'last-login-ip',
+ component: LastLoginIpComponent
+ }
+ ]
+ },
+ {
+ matcher: oauthMatcher,
+ data: { params: (window.location.href).substr(window.location.href.indexOf('#')) },
+ component: OAuthComponent
+ },
+ {
+ matcher: tokenMatcher,
+ component: TokenSaleComponent
+ },
+ {
+ path: '403',
+ component: ErrorPageComponent
+ },
+ {
+ path: '**',
+ component: SearchResultComponent
+ }
+]
\ No newline at end of file
diff --git a/data/static/codefixes/adminSectionChallenge_4.ts b/data/static/codefixes/adminSectionChallenge_4.ts
new file mode 100644
index 00000000000..779539a2dee
--- /dev/null
+++ b/data/static/codefixes/adminSectionChallenge_4.ts
@@ -0,0 +1,175 @@
+const routes: Routes = [
+ {
+ path: 'administration',
+ component: AdministrationComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'accounting',
+ component: AccountingComponent,
+ canActivate: [AccountingGuard]
+ },
+ {
+ path: 'about',
+ component: AboutComponent
+ },
+ {
+ path: 'address/select',
+ component: AddressSelectComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/saved',
+ component: SavedAddressComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/create',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'address/edit/:addressId',
+ component: AddressCreateComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'delivery-method',
+ component: DeliveryMethodComponent
+ },
+ {
+ path: 'deluxe-membership',
+ component: DeluxeUserComponent,
+ canActivate: [LoginGuard]
+ },
+ {
+ path: 'saved-payment-methods',
+ component: SavedPaymentMethodsComponent
+ },
+ {
+ path: 'basket',
+ component: BasketComponent
+ },
+ {
+ path: 'order-completion/:id',
+ component: OrderCompletionComponent
+ },
+ {
+ path: 'contact',
+ component: ContactComponent
+ },
+ {
+ path: 'photo-wall',
+ component: PhotoWallComponent
+ },
+ {
+ path: 'complain',
+ component: ComplaintComponent
+ },
+ {
+ path: 'chatbot',
+ component: ChatbotComponent
+ },
+ {
+ path: 'order-summary',
+ component: OrderSummaryComponent
+ },
+ {
+ path: 'order-history',
+ component: OrderHistoryComponent
+ },
+ {
+ path: 'payment/:entity',
+ component: PaymentComponent
+ },
+ {
+ path: 'wallet',
+ component: WalletComponent
+ },
+ {
+ path: 'login',
+ component: LoginComponent
+ },
+ {
+ path: 'forgot-password',
+ component: ForgotPasswordComponent
+ },
+ {
+ path: 'recycle',
+ component: RecycleComponent
+ },
+ {
+ path: 'register',
+ component: RegisterComponent
+ },
+ {
+ path: 'search',
+ component: SearchResultComponent
+ },
+ {
+ path: 'hacking-instructor',
+ component: SearchResultComponent
+ },
+ {
+ path: 'score-board',
+ component: ScoreBoardComponent
+ },
+ {
+ path: 'track-result',
+ component: TrackResultComponent
+ },
+ {
+ path: 'track-result/new',
+ component: TrackResultComponent,
+ data: {
+ type: 'new'
+ }
+ },
+ {
+ path: '2fa/enter',
+ component: TwoFactorAuthEnterComponent
+ },
+ {
+ path: 'privacy-security',
+ component: PrivacySecurityComponent,
+ children: [
+ {
+ path: 'privacy-policy',
+ component: PrivacyPolicyComponent
+ },
+ {
+ path: 'change-password',
+ component: ChangePasswordComponent
+ },
+ {
+ path: 'two-factor-authentication',
+ component: TwoFactorAuthComponent
+ },
+ {
+ path: 'data-export',
+ component: DataExportComponent
+ },
+ {
+ path: 'last-login-ip',
+ component: LastLoginIpComponent
+ }
+ ]
+ },
+ {
+ matcher: oauthMatcher,
+ data: { params: (window.location.href).substr(window.location.href.indexOf('#')) },
+ component: OAuthComponent
+ },
+ {
+ matcher: tokenMatcher,
+ component: TokenSaleComponent
+ },
+ {
+ path: '403',
+ component: ErrorPageComponent
+ },
+ {
+ path: '**',
+ component: SearchResultComponent
+ }
+]
\ No newline at end of file
diff --git a/data/static/codefixes/changeProductChallenge.info.yml b/data/static/codefixes/changeProductChallenge.info.yml
new file mode 100644
index 00000000000..7995516358e
--- /dev/null
+++ b/data/static/codefixes/changeProductChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: "While removing the commented-out line made the code cleaner, it did not change the functionality in any way and thus cannot have improved security either."
+ - id: 2
+ explanation: 'Removing all dedicated handling of the products API made things worse, as now the default permissions of the underlying API generator will be used: Allowing GET, POST, PUT and DELETE - without any restrictions.'
+ - id: 3
+ explanation: "Disabling all HTTP verbs other than GET for the products API is indeed the only safe way to implement secure access control. Shop administrators should not use the customer facing web UI to manage the store's inventory anyway."
+ - id: 4
+ explanation: 'You improved security slightly by no longer accepting PUT requests from anonymous API callers. But does the shop even want to allow its authenticated customers to change products themselves?'
+hints:
+ - "In the long list of API-handling middleware, try to find the ones dealing with products offered in the shop first."
+ - 'API routes need to specifically define a handler for a HTTP verb if they wish to override the "allow everything to everyone" default behavior.'
+ - "There is one line that is commented out for no good reason among the product-related middleware."
diff --git a/data/static/codefixes/changeProductChallenge_1.ts b/data/static/codefixes/changeProductChallenge_1.ts
new file mode 100644
index 00000000000..cf42763f0dc
--- /dev/null
+++ b/data/static/codefixes/changeProductChallenge_1.ts
@@ -0,0 +1,73 @@
+/** Authorization **/
+ /* Baskets: Unauthorized users are not allowed to access baskets */
+ app.use('/rest/basket', security.isAuthorized(), security.appendUserId())
+ /* BasketItems: API only accessible for authenticated users */
+ app.use('/api/BasketItems', security.isAuthorized())
+ app.use('/api/BasketItems/:id', security.isAuthorized())
+ /* Feedbacks: GET allowed for feedback carousel, POST allowed in order to provide feedback without being logged in */
+ app.use('/api/Feedbacks/:id', security.isAuthorized())
+ /* Users: Only POST is allowed in order to register a new user */
+ app.get('/api/Users', security.isAuthorized())
+ app.route('/api/Users/:id')
+ .get(security.isAuthorized())
+ .put(security.denyAll())
+ .delete(security.denyAll())
+ /* Products: Only GET is allowed in order to view products */
+ app.post('/api/Products', security.isAuthorized())
+ app.delete('/api/Products/:id', security.denyAll())
+ /* Challenges: GET list of challenges allowed. Everything else forbidden entirely */
+ app.post('/api/Challenges', security.denyAll())
+ app.use('/api/Challenges/:id', security.denyAll())
+ /* Complaints: POST and GET allowed when logged in only */
+ app.get('/api/Complaints', security.isAuthorized())
+ app.post('/api/Complaints', security.isAuthorized())
+ app.use('/api/Complaints/:id', security.denyAll())
+ /* Recycles: POST and GET allowed when logged in only */
+ app.get('/api/Recycles', recycles.blockRecycleItems())
+ app.post('/api/Recycles', security.isAuthorized())
+ /* Challenge evaluation before finale takes over */
+ app.get('/api/Recycles/:id', recycles.getRecycleItem())
+ app.put('/api/Recycles/:id', security.denyAll())
+ app.delete('/api/Recycles/:id', security.denyAll())
+ /* SecurityQuestions: Only GET list of questions allowed. */
+ app.post('/api/SecurityQuestions', security.denyAll())
+ app.use('/api/SecurityQuestions/:id', security.denyAll())
+ /* SecurityAnswers: Only POST of answer allowed. */
+ app.get('/api/SecurityAnswers', security.denyAll())
+ app.use('/api/SecurityAnswers/:id', security.denyAll())
+ /* REST API */
+ app.use('/rest/user/authentication-details', security.isAuthorized())
+ app.use('/rest/basket/:id', security.isAuthorized())
+ app.use('/rest/basket/:id/order', security.isAuthorized())
+ /* Unauthorized users are not allowed to access B2B API */
+ app.use('/b2b/v2', security.isAuthorized())
+ /* Check if the quantity is available in stock and limit per user not exceeded, then add item to basket */
+ app.put('/api/BasketItems/:id', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemUpdate())
+ app.post('/api/BasketItems', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemAddition(), basketItems.addBasketItem())
+ /* Accounting users are allowed to check and update quantities */
+ app.delete('/api/Quantitys/:id', security.denyAll())
+ app.post('/api/Quantitys', security.denyAll())
+ app.use('/api/Quantitys/:id', security.isAccounting(), ipfilter(['123.456.789'], { mode: 'allow' }))
+ /* Feedbacks: Do not allow changes of existing feedback */
+ app.put('/api/Feedbacks/:id', security.denyAll())
+ /* PrivacyRequests: Only allowed for authenticated users */
+ app.use('/api/PrivacyRequests', security.isAuthorized())
+ app.use('/api/PrivacyRequests/:id', security.isAuthorized())
+ /* PaymentMethodRequests: Only allowed for authenticated users */
+ app.post('/api/Cards', security.appendUserId())
+ app.get('/api/Cards', security.appendUserId(), payment.getPaymentMethods())
+ app.put('/api/Cards/:id', security.denyAll())
+ app.delete('/api/Cards/:id', security.appendUserId(), payment.delPaymentMethodById())
+ app.get('/api/Cards/:id', security.appendUserId(), payment.getPaymentMethodById())
+ /* PrivacyRequests: Only POST allowed for authenticated users */
+ app.post('/api/PrivacyRequests', security.isAuthorized())
+ app.get('/api/PrivacyRequests', security.denyAll())
+ app.use('/api/PrivacyRequests/:id', security.denyAll())
+
+ app.post('/api/Addresss', security.appendUserId())
+ app.get('/api/Addresss', security.appendUserId(), address.getAddress())
+ app.put('/api/Addresss/:id', security.appendUserId())
+ app.delete('/api/Addresss/:id', security.appendUserId(), address.delAddressById())
+ app.get('/api/Addresss/:id', security.appendUserId(), address.getAddressById())
+ app.get('/api/Deliverys', delivery.getDeliveryMethods())
+ app.get('/api/Deliverys/:id', delivery.getDeliveryMethod())
\ No newline at end of file
diff --git a/data/static/codefixes/changeProductChallenge_2.ts b/data/static/codefixes/changeProductChallenge_2.ts
new file mode 100644
index 00000000000..a41e3c5652d
--- /dev/null
+++ b/data/static/codefixes/changeProductChallenge_2.ts
@@ -0,0 +1,70 @@
+/** Authorization **/
+ /* Baskets: Unauthorized users are not allowed to access baskets */
+ app.use('/rest/basket', security.isAuthorized(), security.appendUserId())
+ /* BasketItems: API only accessible for authenticated users */
+ app.use('/api/BasketItems', security.isAuthorized())
+ app.use('/api/BasketItems/:id', security.isAuthorized())
+ /* Feedbacks: GET allowed for feedback carousel, POST allowed in order to provide feedback without being logged in */
+ app.use('/api/Feedbacks/:id', security.isAuthorized())
+ /* Users: Only POST is allowed in order to register a new user */
+ app.get('/api/Users', security.isAuthorized())
+ app.route('/api/Users/:id')
+ .get(security.isAuthorized())
+ .put(security.denyAll())
+ .delete(security.denyAll())
+ /* Challenges: GET list of challenges allowed. Everything else forbidden entirely */
+ app.post('/api/Challenges', security.denyAll())
+ app.use('/api/Challenges/:id', security.denyAll())
+ /* Complaints: POST and GET allowed when logged in only */
+ app.get('/api/Complaints', security.isAuthorized())
+ app.post('/api/Complaints', security.isAuthorized())
+ app.use('/api/Complaints/:id', security.denyAll())
+ /* Recycles: POST and GET allowed when logged in only */
+ app.get('/api/Recycles', recycles.blockRecycleItems())
+ app.post('/api/Recycles', security.isAuthorized())
+ /* Challenge evaluation before finale takes over */
+ app.get('/api/Recycles/:id', recycles.getRecycleItem())
+ app.put('/api/Recycles/:id', security.denyAll())
+ app.delete('/api/Recycles/:id', security.denyAll())
+ /* SecurityQuestions: Only GET list of questions allowed. */
+ app.post('/api/SecurityQuestions', security.denyAll())
+ app.use('/api/SecurityQuestions/:id', security.denyAll())
+ /* SecurityAnswers: Only POST of answer allowed. */
+ app.get('/api/SecurityAnswers', security.denyAll())
+ app.use('/api/SecurityAnswers/:id', security.denyAll())
+ /* REST API */
+ app.use('/rest/user/authentication-details', security.isAuthorized())
+ app.use('/rest/basket/:id', security.isAuthorized())
+ app.use('/rest/basket/:id/order', security.isAuthorized())
+ /* Unauthorized users are not allowed to access B2B API */
+ app.use('/b2b/v2', security.isAuthorized())
+ /* Check if the quantity is available in stock and limit per user not exceeded, then add item to basket */
+ app.put('/api/BasketItems/:id', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemUpdate())
+ app.post('/api/BasketItems', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemAddition(), basketItems.addBasketItem())
+ /* Accounting users are allowed to check and update quantities */
+ app.delete('/api/Quantitys/:id', security.denyAll())
+ app.post('/api/Quantitys', security.denyAll())
+ app.use('/api/Quantitys/:id', security.isAccounting(), ipfilter(['123.456.789'], { mode: 'allow' }))
+ /* Feedbacks: Do not allow changes of existing feedback */
+ app.put('/api/Feedbacks/:id', security.denyAll())
+ /* PrivacyRequests: Only allowed for authenticated users */
+ app.use('/api/PrivacyRequests', security.isAuthorized())
+ app.use('/api/PrivacyRequests/:id', security.isAuthorized())
+ /* PaymentMethodRequests: Only allowed for authenticated users */
+ app.post('/api/Cards', security.appendUserId())
+ app.get('/api/Cards', security.appendUserId(), payment.getPaymentMethods())
+ app.put('/api/Cards/:id', security.denyAll())
+ app.delete('/api/Cards/:id', security.appendUserId(), payment.delPaymentMethodById())
+ app.get('/api/Cards/:id', security.appendUserId(), payment.getPaymentMethodById())
+ /* PrivacyRequests: Only POST allowed for authenticated users */
+ app.post('/api/PrivacyRequests', security.isAuthorized())
+ app.get('/api/PrivacyRequests', security.denyAll())
+ app.use('/api/PrivacyRequests/:id', security.denyAll())
+
+ app.post('/api/Addresss', security.appendUserId())
+ app.get('/api/Addresss', security.appendUserId(), address.getAddress())
+ app.put('/api/Addresss/:id', security.appendUserId())
+ app.delete('/api/Addresss/:id', security.appendUserId(), address.delAddressById())
+ app.get('/api/Addresss/:id', security.appendUserId(), address.getAddressById())
+ app.get('/api/Deliverys', delivery.getDeliveryMethods())
+ app.get('/api/Deliverys/:id', delivery.getDeliveryMethod())
\ No newline at end of file
diff --git a/data/static/codefixes/changeProductChallenge_3_correct.ts b/data/static/codefixes/changeProductChallenge_3_correct.ts
new file mode 100644
index 00000000000..3e8dbbbe29d
--- /dev/null
+++ b/data/static/codefixes/changeProductChallenge_3_correct.ts
@@ -0,0 +1,74 @@
+/** Authorization **/
+ /* Baskets: Unauthorized users are not allowed to access baskets */
+ app.use('/rest/basket', security.isAuthorized(), security.appendUserId())
+ /* BasketItems: API only accessible for authenticated users */
+ app.use('/api/BasketItems', security.isAuthorized())
+ app.use('/api/BasketItems/:id', security.isAuthorized())
+ /* Feedbacks: GET allowed for feedback carousel, POST allowed in order to provide feedback without being logged in */
+ app.use('/api/Feedbacks/:id', security.isAuthorized())
+ /* Users: Only POST is allowed in order to register a new user */
+ app.get('/api/Users', security.isAuthorized())
+ app.route('/api/Users/:id')
+ .get(security.isAuthorized())
+ .put(security.denyAll())
+ .delete(security.denyAll())
+ /* Products: Only GET is allowed in order to view products */
+ app.post('/api/Products', security.denyAll())
+ app.put('/api/Products/:id', security.denyAll())
+ app.delete('/api/Products/:id', security.denyAll())
+ /* Challenges: GET list of challenges allowed. Everything else forbidden entirely */
+ app.post('/api/Challenges', security.denyAll())
+ app.use('/api/Challenges/:id', security.denyAll())
+ /* Complaints: POST and GET allowed when logged in only */
+ app.get('/api/Complaints', security.isAuthorized())
+ app.post('/api/Complaints', security.isAuthorized())
+ app.use('/api/Complaints/:id', security.denyAll())
+ /* Recycles: POST and GET allowed when logged in only */
+ app.get('/api/Recycles', recycles.blockRecycleItems())
+ app.post('/api/Recycles', security.isAuthorized())
+ /* Challenge evaluation before finale takes over */
+ app.get('/api/Recycles/:id', recycles.getRecycleItem())
+ app.put('/api/Recycles/:id', security.denyAll())
+ app.delete('/api/Recycles/:id', security.denyAll())
+ /* SecurityQuestions: Only GET list of questions allowed. */
+ app.post('/api/SecurityQuestions', security.denyAll())
+ app.use('/api/SecurityQuestions/:id', security.denyAll())
+ /* SecurityAnswers: Only POST of answer allowed. */
+ app.get('/api/SecurityAnswers', security.denyAll())
+ app.use('/api/SecurityAnswers/:id', security.denyAll())
+ /* REST API */
+ app.use('/rest/user/authentication-details', security.isAuthorized())
+ app.use('/rest/basket/:id', security.isAuthorized())
+ app.use('/rest/basket/:id/order', security.isAuthorized())
+ /* Unauthorized users are not allowed to access B2B API */
+ app.use('/b2b/v2', security.isAuthorized())
+ /* Check if the quantity is available in stock and limit per user not exceeded, then add item to basket */
+ app.put('/api/BasketItems/:id', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemUpdate())
+ app.post('/api/BasketItems', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemAddition(), basketItems.addBasketItem())
+ /* Accounting users are allowed to check and update quantities */
+ app.delete('/api/Quantitys/:id', security.denyAll())
+ app.post('/api/Quantitys', security.denyAll())
+ app.use('/api/Quantitys/:id', security.isAccounting(), ipfilter(['123.456.789'], { mode: 'allow' }))
+ /* Feedbacks: Do not allow changes of existing feedback */
+ app.put('/api/Feedbacks/:id', security.denyAll())
+ /* PrivacyRequests: Only allowed for authenticated users */
+ app.use('/api/PrivacyRequests', security.isAuthorized())
+ app.use('/api/PrivacyRequests/:id', security.isAuthorized())
+ /* PaymentMethodRequests: Only allowed for authenticated users */
+ app.post('/api/Cards', security.appendUserId())
+ app.get('/api/Cards', security.appendUserId(), payment.getPaymentMethods())
+ app.put('/api/Cards/:id', security.denyAll())
+ app.delete('/api/Cards/:id', security.appendUserId(), payment.delPaymentMethodById())
+ app.get('/api/Cards/:id', security.appendUserId(), payment.getPaymentMethodById())
+ /* PrivacyRequests: Only POST allowed for authenticated users */
+ app.post('/api/PrivacyRequests', security.isAuthorized())
+ app.get('/api/PrivacyRequests', security.denyAll())
+ app.use('/api/PrivacyRequests/:id', security.denyAll())
+
+ app.post('/api/Addresss', security.appendUserId())
+ app.get('/api/Addresss', security.appendUserId(), address.getAddress())
+ app.put('/api/Addresss/:id', security.appendUserId())
+ app.delete('/api/Addresss/:id', security.appendUserId(), address.delAddressById())
+ app.get('/api/Addresss/:id', security.appendUserId(), address.getAddressById())
+ app.get('/api/Deliverys', delivery.getDeliveryMethods())
+ app.get('/api/Deliverys/:id', delivery.getDeliveryMethod())
\ No newline at end of file
diff --git a/data/static/codefixes/changeProductChallenge_4.ts b/data/static/codefixes/changeProductChallenge_4.ts
new file mode 100644
index 00000000000..f946e56408d
--- /dev/null
+++ b/data/static/codefixes/changeProductChallenge_4.ts
@@ -0,0 +1,74 @@
+/** Authorization **/
+ /* Baskets: Unauthorized users are not allowed to access baskets */
+ app.use('/rest/basket', security.isAuthorized(), security.appendUserId())
+ /* BasketItems: API only accessible for authenticated users */
+ app.use('/api/BasketItems', security.isAuthorized())
+ app.use('/api/BasketItems/:id', security.isAuthorized())
+ /* Feedbacks: GET allowed for feedback carousel, POST allowed in order to provide feedback without being logged in */
+ app.use('/api/Feedbacks/:id', security.isAuthorized())
+ /* Users: Only POST is allowed in order to register a new user */
+ app.get('/api/Users', security.isAuthorized())
+ app.route('/api/Users/:id')
+ .get(security.isAuthorized())
+ .put(security.denyAll())
+ .delete(security.denyAll())
+ /* Products: Only GET is allowed in order to view products */
+ app.post('/api/Products', security.isAuthorized())
+ app.put('/api/Products/:id', security.isAuthorized())
+ app.delete('/api/Products/:id', security.denyAll())
+ /* Challenges: GET list of challenges allowed. Everything else forbidden entirely */
+ app.post('/api/Challenges', security.denyAll())
+ app.use('/api/Challenges/:id', security.denyAll())
+ /* Complaints: POST and GET allowed when logged in only */
+ app.get('/api/Complaints', security.isAuthorized())
+ app.post('/api/Complaints', security.isAuthorized())
+ app.use('/api/Complaints/:id', security.denyAll())
+ /* Recycles: POST and GET allowed when logged in only */
+ app.get('/api/Recycles', recycles.blockRecycleItems())
+ app.post('/api/Recycles', security.isAuthorized())
+ /* Challenge evaluation before finale takes over */
+ app.get('/api/Recycles/:id', recycles.getRecycleItem())
+ app.put('/api/Recycles/:id', security.denyAll())
+ app.delete('/api/Recycles/:id', security.denyAll())
+ /* SecurityQuestions: Only GET list of questions allowed. */
+ app.post('/api/SecurityQuestions', security.denyAll())
+ app.use('/api/SecurityQuestions/:id', security.denyAll())
+ /* SecurityAnswers: Only POST of answer allowed. */
+ app.get('/api/SecurityAnswers', security.denyAll())
+ app.use('/api/SecurityAnswers/:id', security.denyAll())
+ /* REST API */
+ app.use('/rest/user/authentication-details', security.isAuthorized())
+ app.use('/rest/basket/:id', security.isAuthorized())
+ app.use('/rest/basket/:id/order', security.isAuthorized())
+ /* Unauthorized users are not allowed to access B2B API */
+ app.use('/b2b/v2', security.isAuthorized())
+ /* Check if the quantity is available in stock and limit per user not exceeded, then add item to basket */
+ app.put('/api/BasketItems/:id', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemUpdate())
+ app.post('/api/BasketItems', security.appendUserId(), basketItems.quantityCheckBeforeBasketItemAddition(), basketItems.addBasketItem())
+ /* Accounting users are allowed to check and update quantities */
+ app.delete('/api/Quantitys/:id', security.denyAll())
+ app.post('/api/Quantitys', security.denyAll())
+ app.use('/api/Quantitys/:id', security.isAccounting(), ipfilter(['123.456.789'], { mode: 'allow' }))
+ /* Feedbacks: Do not allow changes of existing feedback */
+ app.put('/api/Feedbacks/:id', security.denyAll())
+ /* PrivacyRequests: Only allowed for authenticated users */
+ app.use('/api/PrivacyRequests', security.isAuthorized())
+ app.use('/api/PrivacyRequests/:id', security.isAuthorized())
+ /* PaymentMethodRequests: Only allowed for authenticated users */
+ app.post('/api/Cards', security.appendUserId())
+ app.get('/api/Cards', security.appendUserId(), payment.getPaymentMethods())
+ app.put('/api/Cards/:id', security.denyAll())
+ app.delete('/api/Cards/:id', security.appendUserId(), payment.delPaymentMethodById())
+ app.get('/api/Cards/:id', security.appendUserId(), payment.getPaymentMethodById())
+ /* PrivacyRequests: Only POST allowed for authenticated users */
+ app.post('/api/PrivacyRequests', security.isAuthorized())
+ app.get('/api/PrivacyRequests', security.denyAll())
+ app.use('/api/PrivacyRequests/:id', security.denyAll())
+
+ app.post('/api/Addresss', security.appendUserId())
+ app.get('/api/Addresss', security.appendUserId(), address.getAddress())
+ app.put('/api/Addresss/:id', security.appendUserId())
+ app.delete('/api/Addresss/:id', security.appendUserId(), address.delAddressById())
+ app.get('/api/Addresss/:id', security.appendUserId(), address.getAddressById())
+ app.get('/api/Deliverys', delivery.getDeliveryMethods())
+ app.get('/api/Deliverys/:id', delivery.getDeliveryMethod())
\ No newline at end of file
diff --git a/data/static/codefixes/dbSchemaChallenge.info.yml b/data/static/codefixes/dbSchemaChallenge.info.yml
new file mode 100644
index 00000000000..d2dfafa24a2
--- /dev/null
+++ b/data/static/codefixes/dbSchemaChallenge.info.yml
@@ -0,0 +1,11 @@
+fixes:
+ - id: 1
+ explanation: 'Replacing the template string (`...`) notation with plain string concatenation ("..."+"...") does not change the behavior of the code in any way. It only makes the code less readable.'
+ - id: 2
+ explanation: 'Using the built-in replacement (or binding) mechanism of Sequelize is equivalent to creating a Prepared Statement. This prevents tampering with the query syntax through malicious user input as it is "set in stone" before the criteria parameter is inserted.'
+ - id: 3
+ explanation: "Trying to prevent any injection attacks with a custom-built blocklist mechanism is doomed to fail. It might work for some simpler attack payloads but an attacker with time and skills can likely bypass it at some point."
+hints:
+ - "Try to identify any variables in the code that might contain arbitrary user input."
+ - "Follow the user input through the function call and try to spot places where it might be abused for malicious purposes."
+ - "Can you spot a place where a SQL query is being cobbled together in an unsafe way?"
diff --git a/data/static/codefixes/dbSchemaChallenge_1.ts b/data/static/codefixes/dbSchemaChallenge_1.ts
new file mode 100644
index 00000000000..2a0949b4530
--- /dev/null
+++ b/data/static/codefixes/dbSchemaChallenge_1.ts
@@ -0,0 +1,17 @@
+module.exports = function searchProducts () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ let criteria: any = req.query.q === 'undefined' ? '' : req.query.q ?? ''
+ criteria = (criteria.length <= 200) ? criteria : criteria.substring(0, 200)
+ models.sequelize.query("SELECT * FROM Products WHERE ((name LIKE '%"+criteria+"%' OR description LIKE '%"+criteria+"%') AND deletedAt IS NULL) ORDER BY name")
+ .then(([products]: any) => {
+ const dataString = JSON.stringify(products)
+ for (let i = 0; i < products.length; i++) {
+ products[i].name = req.__(products[i].name)
+ products[i].description = req.__(products[i].description)
+ }
+ res.json(utils.queryResultToJson(products))
+ }).catch((error: ErrorWithParent) => {
+ next(error.parent)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/dbSchemaChallenge_2_correct.ts b/data/static/codefixes/dbSchemaChallenge_2_correct.ts
new file mode 100644
index 00000000000..4b7215dc98c
--- /dev/null
+++ b/data/static/codefixes/dbSchemaChallenge_2_correct.ts
@@ -0,0 +1,19 @@
+module.exports = function searchProducts () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ let criteria: any = req.query.q === 'undefined' ? '' : req.query.q ?? ''
+ criteria = (criteria.length <= 200) ? criteria : criteria.substring(0, 200)
+ models.sequelize.query(
+ `SELECT * FROM Products WHERE ((name LIKE '%:criteria%' OR description LIKE '%:criteria%') AND deletedAt IS NULL) ORDER BY name`,
+ { replacements: { criteria } }
+ ).then(([products]: any) => {
+ const dataString = JSON.stringify(products)
+ for (let i = 0; i < products.length; i++) {
+ products[i].name = req.__(products[i].name)
+ products[i].description = req.__(products[i].description)
+ }
+ res.json(utils.queryResultToJson(products))
+ }).catch((error: ErrorWithParent) => {
+ next(error.parent)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/dbSchemaChallenge_3.ts b/data/static/codefixes/dbSchemaChallenge_3.ts
new file mode 100644
index 00000000000..9b330f61a25
--- /dev/null
+++ b/data/static/codefixes/dbSchemaChallenge_3.ts
@@ -0,0 +1,23 @@
+const injectionChars = /"|'|;|and|or|;|#/i;
+
+module.exports = function searchProducts () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ let criteria: any = req.query.q === 'undefined' ? '' : req.query.q ?? ''
+ criteria = (criteria.length <= 200) ? criteria : criteria.substring(0, 200)
+ if (criteria.match(injectionChars)) {
+ res.status(400).send()
+ return
+ }
+ models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' OR description LIKE '%${criteria}%') AND deletedAt IS NULL) ORDER BY name`)
+ .then(([products]: any) => {
+ const dataString = JSON.stringify(products)
+ for (let i = 0; i < products.length; i++) {
+ products[i].name = req.__(products[i].name)
+ products[i].description = req.__(products[i].description)
+ }
+ res.json(utils.queryResultToJson(products))
+ }).catch((error: ErrorWithParent) => {
+ next(error.parent)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/directoryListingChallenge.info.yml b/data/static/codefixes/directoryListingChallenge.info.yml
new file mode 100644
index 00000000000..d68885a117f
--- /dev/null
+++ b/data/static/codefixes/directoryListingChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: 'Getting rid of the /ftp folder entirely is the only way to plumb this data leakage for good. Valid static content in it needs to be moved to a more suitable location and order confirmation PDFs had no business to be placed there publicly accessible in the first place. Everything else in that folder was just accidentally put & forgotten there anyway.'
+ - id: 2
+ explanation: 'Removing only the directory listing will still allow attackers to download individual files if they can come up with a valid file name.'
+ - id: 3
+ explanation: 'Removing the routes that serve individual files is likely to plumb the data leak but still provides information to the attacker unnecessarily.'
+ - id: 4
+ explanation: "Switching off the icons is a cosmetic change on the directory listing but still allows the files to be browsed and accessed."
+hints:
+ - "Can you identify one or more routes which have something to do with file serving?"
+ - "Did you notice that there are seperate routes the directory listing and retrieving individual files?"
+ - "Make sure to select both lines responsible for the data leakage."
diff --git a/data/static/codefixes/directoryListingChallenge_1_correct.ts b/data/static/codefixes/directoryListingChallenge_1_correct.ts
new file mode 100644
index 00000000000..1b2d682fd33
--- /dev/null
+++ b/data/static/codefixes/directoryListingChallenge_1_correct.ts
@@ -0,0 +1,13 @@
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* /logs directory browsing */
+ app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' }))
+ app.use('/support/logs/:file', logFileServer())
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/directoryListingChallenge_2.ts b/data/static/codefixes/directoryListingChallenge_2.ts
new file mode 100644
index 00000000000..3c8efb0014c
--- /dev/null
+++ b/data/static/codefixes/directoryListingChallenge_2.ts
@@ -0,0 +1,17 @@
+/* /ftp file download */
+ app.use('/ftp(?!/quarantine)/:file', fileServer())
+ app.use('/ftp/quarantine/:file', quarantineServer())
+
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* /logs directory browsing */
+ app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' }))
+ app.use('/support/logs/:file', logFileServer())
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/directoryListingChallenge_3.ts b/data/static/codefixes/directoryListingChallenge_3.ts
new file mode 100644
index 00000000000..8f374d16bb3
--- /dev/null
+++ b/data/static/codefixes/directoryListingChallenge_3.ts
@@ -0,0 +1,16 @@
+/* /ftp directory browsing */
+ app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true }))
+
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* /logs directory browsing */
+ app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' }))
+ app.use('/support/logs/:file', logFileServer())
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/directoryListingChallenge_4.ts b/data/static/codefixes/directoryListingChallenge_4.ts
new file mode 100644
index 00000000000..820133051f9
--- /dev/null
+++ b/data/static/codefixes/directoryListingChallenge_4.ts
@@ -0,0 +1,18 @@
+/* /ftp directory browsing and file download */
+ app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: false }))
+ app.use('/ftp(?!/quarantine)/:file', fileServer())
+ app.use('/ftp/quarantine/:file', quarantineServer())
+
+ /* /encryptionkeys directory browsing */
+ app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' }))
+ app.use('/encryptionkeys/:file', keyServer())
+
+ /* /logs directory browsing */
+ app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' }))
+ app.use('/support/logs/:file', logFileServer())
+
+ /* Swagger documentation for B2B v2 endpoints */
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
+
+ app.use(express.static(path.resolve('frontend/dist/frontend')))
+ app.use(cookieParser('kekse'))
\ No newline at end of file
diff --git a/data/static/codefixes/exposedMetricsChallenge.info.yml b/data/static/codefixes/exposedMetricsChallenge.info.yml
new file mode 100644
index 00000000000..b38702f423c
--- /dev/null
+++ b/data/static/codefixes/exposedMetricsChallenge.info.yml
@@ -0,0 +1,10 @@
+fixes:
+ - id: 1
+ explanation: "This fix prevents unauthorized access to the metrics route but overshoots the goal by locking out everyone - including administrators."
+ - id: 2
+ explanation: "The metrics route remains publicly accessible. This change only messes with functional settings of the measurement framework unnecessarily."
+ - id: 3
+ explanation: "Access will now be restricted only to users with administrator permissions, which seems reasonable protection, assuming that it is not possible for a regular user to escalate admin priviliges. If that were a risk, the metrics should better be stored behind the scenes not be made accessible via the shop application at all."
+hints:
+ - "Can you find a HTTP route mapping that deals with metrics?"
+ - "Remember: The default behavior of route mappings is to allow access to everyone."
diff --git a/data/static/codefixes/exposedMetricsChallenge_1.ts b/data/static/codefixes/exposedMetricsChallenge_1.ts
new file mode 100644
index 00000000000..ddf5664b436
--- /dev/null
+++ b/data/static/codefixes/exposedMetricsChallenge_1.ts
@@ -0,0 +1,42 @@
+/* Serve metrics */
+let metricsUpdateLoop
+const Metrics = metrics.observeMetrics()
+app.get('/metrics', security.denyAll(), metrics.serveMetrics())
+errorhandler.title = `${config.get('application.name')} (Express ${utils.version('express')})`
+
+const registerWebsocketEvents = require('./lib/startup/registerWebsocketEvents')
+const customizeApplication = require('./lib/startup/customizeApplication')
+
+export async function start (readyCallback: Function) {
+ const datacreatorEnd = startupGauge.startTimer({ task: 'datacreator' })
+ await sequelize.sync({ force: true })
+ await datacreator()
+ datacreatorEnd()
+ const port = process.env.PORT ?? config.get('server.port')
+ process.env.BASE_PATH = process.env.BASE_PATH ?? config.get('server.basePath')
+
+ metricsUpdateLoop = Metrics.updateLoop()
+
+ server.listen(port, () => {
+ logger.info(colors.cyan(`Server listening on port ${colors.bold(port)}`))
+ startupGauge.set({ task: 'ready' }, (Date.now() - startTime) / 1000)
+ if (process.env.BASE_PATH !== '') {
+ logger.info(colors.cyan(`Server using proxy base path ${colors.bold(process.env.BASE_PATH)} for redirects`))
+ }
+ registerWebsocketEvents(server)
+ if (readyCallback) {
+ readyCallback()
+ }
+ })
+
+}
+
+export function close (exitCode: number | undefined) {
+ if (server) {
+ clearInterval(metricsUpdateLoop)
+ server.close()
+ }
+ if (exitCode !== undefined) {
+ process.exit(exitCode)
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/exposedMetricsChallenge_2.ts b/data/static/codefixes/exposedMetricsChallenge_2.ts
new file mode 100644
index 00000000000..31b781b9f61
--- /dev/null
+++ b/data/static/codefixes/exposedMetricsChallenge_2.ts
@@ -0,0 +1,38 @@
+/* Serve metrics */
+const Metrics = metrics.observeMetrics()
+app.get('/metrics', metrics.serveMetrics())
+errorhandler.title = `${config.get('application.name')} (Express ${utils.version('express')})`
+
+const registerWebsocketEvents = require('./lib/startup/registerWebsocketEvents')
+const customizeApplication = require('./lib/startup/customizeApplication')
+
+export async function start (readyCallback: Function) {
+ const datacreatorEnd = startupGauge.startTimer({ task: 'datacreator' })
+ await sequelize.sync({ force: true })
+ await datacreator()
+ datacreatorEnd()
+ const port = process.env.PORT ?? config.get('server.port')
+ process.env.BASE_PATH = process.env.BASE_PATH ?? config.get('server.basePath')
+
+ server.listen(port, () => {
+ logger.info(colors.cyan(`Server listening on port ${colors.bold(port)}`))
+ startupGauge.set({ task: 'ready' }, (Date.now() - startTime) / 1000)
+ if (process.env.BASE_PATH !== '') {
+ logger.info(colors.cyan(`Server using proxy base path ${colors.bold(process.env.BASE_PATH)} for redirects`))
+ }
+ registerWebsocketEvents(server)
+ if (readyCallback) {
+ readyCallback()
+ }
+ })
+
+}
+
+export function close (exitCode: number | undefined) {
+ if (server) {
+ server.close()
+ }
+ if (exitCode !== undefined) {
+ process.exit(exitCode)
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/exposedMetricsChallenge_3_correct.ts b/data/static/codefixes/exposedMetricsChallenge_3_correct.ts
new file mode 100644
index 00000000000..a554133df73
--- /dev/null
+++ b/data/static/codefixes/exposedMetricsChallenge_3_correct.ts
@@ -0,0 +1,42 @@
+/* Serve metrics */
+let metricsUpdateLoop
+const Metrics = metrics.observeMetrics()
+app.get('/metrics', security.isAdmin(), metrics.serveMetrics())
+errorhandler.title = `${config.get('application.name')} (Express ${utils.version('express')})`
+
+const registerWebsocketEvents = require('./lib/startup/registerWebsocketEvents')
+const customizeApplication = require('./lib/startup/customizeApplication')
+
+export async function start (readyCallback: Function) {
+ const datacreatorEnd = startupGauge.startTimer({ task: 'datacreator' })
+ await sequelize.sync({ force: true })
+ await datacreator()
+ datacreatorEnd()
+ const port = process.env.PORT ?? config.get('server.port')
+ process.env.BASE_PATH = process.env.BASE_PATH ?? config.get('server.basePath')
+
+ metricsUpdateLoop = Metrics.updateLoop()
+
+ server.listen(port, () => {
+ logger.info(colors.cyan(`Server listening on port ${colors.bold(port)}`))
+ startupGauge.set({ task: 'ready' }, (Date.now() - startTime) / 1000)
+ if (process.env.BASE_PATH !== '') {
+ logger.info(colors.cyan(`Server using proxy base path ${colors.bold(process.env.BASE_PATH)} for redirects`))
+ }
+ registerWebsocketEvents(server)
+ if (readyCallback) {
+ readyCallback()
+ }
+ })
+
+}
+
+export function close (exitCode: number | undefined) {
+ if (server) {
+ clearInterval(metricsUpdateLoop)
+ server.close()
+ }
+ if (exitCode !== undefined) {
+ process.exit(exitCode)
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/forgedReviewChallenge.info.yml b/data/static/codefixes/forgedReviewChallenge.info.yml
new file mode 100644
index 00000000000..d10c24ed725
--- /dev/null
+++ b/data/static/codefixes/forgedReviewChallenge.info.yml
@@ -0,0 +1,12 @@
+fixes:
+ - id: 1
+ explanation: "This solution would reassign an updated review to the last editor, but it would not prevent to change other user's reviews in the first place."
+ - id: 2
+ explanation: 'Setting the author on server-side based on the user retrieved from the authentication token in the HTTP request is the right call. It prevents users from just passing any author email they like along with the request.'
+ - id: 3
+ explanation: "Removing the option to update multiple documents at once is a good idea and might actually help against another flaw in this code. But it does not fix the problem of allowing users to update other user's reviews."
+hints:
+ - "To find the culprit lines, you need to understand how MongoDB handles updating records."
+ - "Did you notice that the developers retrieved a reference to the user but never actually use it for anything? This might be part of the problem."
+ - "Another problematic line you need to select, is actually missing something that ties the user to the review."
+
diff --git a/data/static/codefixes/forgedReviewChallenge_1.ts b/data/static/codefixes/forgedReviewChallenge_1.ts
new file mode 100644
index 00000000000..9c69b1fa97e
--- /dev/null
+++ b/data/static/codefixes/forgedReviewChallenge_1.ts
@@ -0,0 +1,15 @@
+module.exports = function productReviews () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ const user = security.authenticatedUsers.from(req)
+ db.reviews.update(
+ { _id: req.body.id },
+ { $set: { message: req.body.message, author: user.data.email } },
+ { multi: true }
+ ).then(
+ (result: { modified: number, original: Array<{ author: any }> }) => {
+ res.json(result)
+ }, (err: unknown) => {
+ res.status(500).json(err)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/forgedReviewChallenge_2_correct.ts b/data/static/codefixes/forgedReviewChallenge_2_correct.ts
new file mode 100644
index 00000000000..20050ee21de
--- /dev/null
+++ b/data/static/codefixes/forgedReviewChallenge_2_correct.ts
@@ -0,0 +1,15 @@
+module.exports = function productReviews () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ const user = security.authenticatedUsers.from(req)
+ db.reviews.update(
+ { _id: req.body.id, author: user.data.email },
+ { $set: { message: req.body.message } },
+ { multi: true }
+ ).then(
+ (result: { modified: number, original: Array<{ author: any }> }) => {
+ res.json(result)
+ }, (err: unknown) => {
+ res.status(500).json(err)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/forgedReviewChallenge_3.ts b/data/static/codefixes/forgedReviewChallenge_3.ts
new file mode 100644
index 00000000000..6f7eb15938f
--- /dev/null
+++ b/data/static/codefixes/forgedReviewChallenge_3.ts
@@ -0,0 +1,14 @@
+module.exports = function productReviews () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ const user = security.authenticatedUsers.from(req)
+ db.reviews.update(
+ { _id: req.body.id },
+ { $set: { message: req.body.message } }
+ ).then(
+ (result: { modified: number, original: Array<{ author: any }> }) => {
+ res.json(result)
+ }, (err: unknown) => {
+ res.status(500).json(err)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/localXssChallenge.info.yml b/data/static/codefixes/localXssChallenge.info.yml
new file mode 100644
index 00000000000..3ef6eefacd2
--- /dev/null
+++ b/data/static/codefixes/localXssChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: 'Using bypassSecurityTrustResourceUrl() instead of bypassSecurityTrustHtml() changes the context for which input sanitization is bypassed. This switch might only accidentally keep XSS prevention intact, but the new URL context does not make any sense here.'
+ - id: 2
+ explanation: "Removing the bypass of sanitization entirely is the best way to fix this vulnerability. Fiddling with Angular's built-in sanitization was entirely unnecessary as the user input for a text search should not be expected to contain HTML that needs to be rendered but merely plain text."
+ - id: 3
+ explanation: 'Using bypassSecurityTrustScript() instead of bypassSecurityTrustHtml() changes the context for which input sanitization is bypassed. If at all, this switch might only accidentally keep XSS prevention intact. The context where the parameter is used is not a script either, so this switch would be nonsensical.'
+ - id: 4
+ explanation: 'Using bypassSecurityTrustStyle() instead of bypassSecurityTrustHtml() changes the context for which input sanitization is bypassed. If at all, this switch might only accidentally keep XSS prevention intact. The context where the parameter is used is not CSS, making this switch totally pointless.'
+hints:
+ - "Try to identify where (potentially malicious) user input is coming into the code."
+ - "What is the code doing with the user input other than using it to filter the data source?"
+ - "Look for a line where the developers fiddled with Angular's built-in security model."
diff --git a/data/static/codefixes/localXssChallenge_1.ts b/data/static/codefixes/localXssChallenge_1.ts
new file mode 100644
index 00000000000..85ad5efc878
--- /dev/null
+++ b/data/static/codefixes/localXssChallenge_1.ts
@@ -0,0 +1,19 @@
+filterTable () {
+ let queryParam: string = this.route.snapshot.queryParams.q
+ if (queryParam) {
+ queryParam = queryParam.trim()
+ this.dataSource.filter = queryParam.toLowerCase()
+ this.searchValue = this.sanitizer.bypassSecurityTrustResourceUrl(queryParam)
+ this.gridDataSource.subscribe((result: any) => {
+ if (result.length === 0) {
+ this.emptyState = true
+ } else {
+ this.emptyState = false
+ }
+ })
+ } else {
+ this.dataSource.filter = ''
+ this.searchValue = undefined
+ this.emptyState = false
+ }
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/localXssChallenge_2_correct.ts b/data/static/codefixes/localXssChallenge_2_correct.ts
new file mode 100644
index 00000000000..d2ccfbde82e
--- /dev/null
+++ b/data/static/codefixes/localXssChallenge_2_correct.ts
@@ -0,0 +1,19 @@
+filterTable () {
+ let queryParam: string = this.route.snapshot.queryParams.q
+ if (queryParam) {
+ queryParam = queryParam.trim()
+ this.dataSource.filter = queryParam.toLowerCase()
+ this.searchValue = queryParam
+ this.gridDataSource.subscribe((result: any) => {
+ if (result.length === 0) {
+ this.emptyState = true
+ } else {
+ this.emptyState = false
+ }
+ })
+ } else {
+ this.dataSource.filter = ''
+ this.searchValue = undefined
+ this.emptyState = false
+ }
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/localXssChallenge_3.ts b/data/static/codefixes/localXssChallenge_3.ts
new file mode 100644
index 00000000000..58fbc41eaee
--- /dev/null
+++ b/data/static/codefixes/localXssChallenge_3.ts
@@ -0,0 +1,19 @@
+filterTable () {
+ let queryParam: string = this.route.snapshot.queryParams.q
+ if (queryParam) {
+ queryParam = queryParam.trim()
+ this.dataSource.filter = queryParam.toLowerCase()
+ this.searchValue = this.sanitizer.bypassSecurityTrustScript(queryParam)
+ this.gridDataSource.subscribe((result: any) => {
+ if (result.length === 0) {
+ this.emptyState = true
+ } else {
+ this.emptyState = false
+ }
+ })
+ } else {
+ this.dataSource.filter = ''
+ this.searchValue = undefined
+ this.emptyState = false
+ }
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/localXssChallenge_4.ts b/data/static/codefixes/localXssChallenge_4.ts
new file mode 100644
index 00000000000..a7a3071f640
--- /dev/null
+++ b/data/static/codefixes/localXssChallenge_4.ts
@@ -0,0 +1,19 @@
+filterTable () {
+ let queryParam: string = this.route.snapshot.queryParams.q
+ if (queryParam) {
+ queryParam = queryParam.trim()
+ this.dataSource.filter = queryParam.toLowerCase()
+ this.searchValue = this.sanitizer.bypassSecurityTrustStyle(queryParam)
+ this.gridDataSource.subscribe((result: any) => {
+ if (result.length === 0) {
+ this.emptyState = true
+ } else {
+ this.emptyState = false
+ }
+ })
+ } else {
+ this.dataSource.filter = ''
+ this.searchValue = undefined
+ this.emptyState = false
+ }
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginAdminChallenge.info.yml b/data/static/codefixes/loginAdminChallenge.info.yml
new file mode 100644
index 00000000000..3a61abae918
--- /dev/null
+++ b/data/static/codefixes/loginAdminChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: "Trying to prevent any injection attacks with a custom-built blocklist mechanism is doomed to fail. It might work for some simpler attack payloads but an attacker with time and skills can likely bypass it at some point."
+ - id: 2
+ explanation: 'This fix unfortunately goes only half the way to using the binding mechanism of Sequelize. Such a Prepared Statement still concatenated from user input, is still wide open for SQL Injection attacks.'
+ - id: 3
+ explanation: 'This fix uses the binding mechanism of Sequelize to create the equivalent of a Prepared Statement, which is great. Unfortunately this fix also introduces a critical functional bug into the authentication process.'
+ - id: 4
+ explanation: 'Using the built-in binding (or replacement) mechanism of Sequelize is equivalent to creating a Prepared Statement. This prevents tampering with the query syntax through malicious user input as it is "set in stone" before the criteria parameter is inserted.'
+hints:
+ - "Try to identify any variables in the code that might contain arbitrary user input."
+ - "Follow the user input through the function call and try to spot places where it might be abused for malicious purposes."
+ - "Can you spot a place where a SQL query is being cobbled together in an unsafe way?"
diff --git a/data/static/codefixes/loginAdminChallenge_1.ts b/data/static/codefixes/loginAdminChallenge_1.ts
new file mode 100644
index 00000000000..7595fe42a0e
--- /dev/null
+++ b/data/static/codefixes/loginAdminChallenge_1.ts
@@ -0,0 +1,41 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ if (req.body.email.match(/.*['-;].*/) || req.body.password.match(/.*['-;].*/)) {
+ res.status(451).send(res.__('SQL Injection detected.'))
+ }
+ models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginAdminChallenge_2.ts b/data/static/codefixes/loginAdminChallenge_2.ts
new file mode 100644
index 00000000000..1c37b71d7e6
--- /dev/null
+++ b/data/static/codefixes/loginAdminChallenge_2.ts
@@ -0,0 +1,39 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = $1 AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`,
+ { bind: [ req.body.email ], model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginAdminChallenge_3.ts b/data/static/codefixes/loginAdminChallenge_3.ts
new file mode 100644
index 00000000000..71983546712
--- /dev/null
+++ b/data/static/codefixes/loginAdminChallenge_3.ts
@@ -0,0 +1,39 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = $1 AND password = $2 AND deletedAt IS NULL`,
+ { bind: [ req.body.email, req.body.password ], model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginAdminChallenge_4_correct.ts b/data/static/codefixes/loginAdminChallenge_4_correct.ts
new file mode 100644
index 00000000000..83ba17408fa
--- /dev/null
+++ b/data/static/codefixes/loginAdminChallenge_4_correct.ts
@@ -0,0 +1,39 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = $1 AND password = $2 AND deletedAt IS NULL`,
+ { bind: [ req.body.email, security.hash(req.body.password) ], model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginBenderChallenge.info.yml b/data/static/codefixes/loginBenderChallenge.info.yml
new file mode 100644
index 00000000000..1d918278395
--- /dev/null
+++ b/data/static/codefixes/loginBenderChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: "Trying to prevent any injection attacks with a custom-built blocklist mechanism is doomed to fail. It might work for some simpler attack payloads but an attacker with time and skills can likely bypass it at some point."
+ - id: 2
+ explanation: 'Using the built-in binding (or replacement) mechanism of Sequelize is equivalent to creating a Prepared Statement. This prevents tampering with the query syntax through malicious user input as it is "set in stone" before the criteria parameter is inserted.'
+ - id: 3
+ explanation: 'This fix unfortunately goes only half the way to using the replacement mechanism of Sequelize. Such a Prepared Statement still concatenated from user input, is still wide open for SQL Injection attacks.'
+ - id: 4
+ explanation: 'Turning off the "plain" flag will let Sequelize return all matching rows instead of just the first one. This neither makes sense from a functional point of view in a login function, not could it prevent SQL Injection attacks.'
+hints:
+ - "Try to identify any variables in the code that might contain arbitrary user input."
+ - "Follow the user input through the function call and try to spot places where it might be abused for malicious purposes."
+ - "Can you spot a place where a SQL query is being cobbled together in an unsafe way?"
diff --git a/data/static/codefixes/loginBenderChallenge_1.ts b/data/static/codefixes/loginBenderChallenge_1.ts
new file mode 100644
index 00000000000..7595fe42a0e
--- /dev/null
+++ b/data/static/codefixes/loginBenderChallenge_1.ts
@@ -0,0 +1,41 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ if (req.body.email.match(/.*['-;].*/) || req.body.password.match(/.*['-;].*/)) {
+ res.status(451).send(res.__('SQL Injection detected.'))
+ }
+ models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginBenderChallenge_2_correct.ts b/data/static/codefixes/loginBenderChallenge_2_correct.ts
new file mode 100644
index 00000000000..57628926730
--- /dev/null
+++ b/data/static/codefixes/loginBenderChallenge_2_correct.ts
@@ -0,0 +1,39 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = $mail AND password = $pass AND deletedAt IS NULL`,
+ { bind: { mail: req.body.email, pass: security.hash(req.body.password) }, model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginBenderChallenge_3.ts b/data/static/codefixes/loginBenderChallenge_3.ts
new file mode 100644
index 00000000000..312440bc1fe
--- /dev/null
+++ b/data/static/codefixes/loginBenderChallenge_3.ts
@@ -0,0 +1,39 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = :mail AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`,
+ { replacements: { mail: req.body.email }, model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginBenderChallenge_4.ts b/data/static/codefixes/loginBenderChallenge_4.ts
new file mode 100644
index 00000000000..bb28fbab418
--- /dev/null
+++ b/data/static/codefixes/loginBenderChallenge_4.ts
@@ -0,0 +1,38 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: models.User, plain: false })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginJimChallenge.info.yml b/data/static/codefixes/loginJimChallenge.info.yml
new file mode 100644
index 00000000000..88aecd9773f
--- /dev/null
+++ b/data/static/codefixes/loginJimChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: 'Using the built-in binding (or replacement) mechanism of Sequelize is equivalent to creating a Prepared Statement. This prevents tampering with the query syntax through malicious user input as it is "set in stone" before the criteria parameter is inserted.'
+ - id: 2
+ explanation: 'Turning off the "plain" flag will let Sequelize return all matching rows instead of just the first one. This neither makes sense from a functional point of view in a login function, not could it prevent SQL Injection attacks.'
+ - id: 3
+ explanation: 'This fix uses the binding mechanism of Sequelize to create the equivalent of a Prepared Statement, which is great. Unfortunately this fix also introduces a critical functional bug into the authentication process.'
+ - id: 4
+ explanation: "Trying to prevent any injection attacks with a custom-built blocklist mechanism is doomed to fail. It might work for some simpler attack payloads but an attacker with time and skills can likely bypass it at some point."
+hints:
+ - "Try to identify any variables in the code that might contain arbitrary user input."
+ - "Follow the user input through the function call and try to spot places where it might be abused for malicious purposes."
+ - "Can you spot a place where a SQL query is being cobbled together in an unsafe way?"
diff --git a/data/static/codefixes/loginJimChallenge_1_correct.ts b/data/static/codefixes/loginJimChallenge_1_correct.ts
new file mode 100644
index 00000000000..83ba17408fa
--- /dev/null
+++ b/data/static/codefixes/loginJimChallenge_1_correct.ts
@@ -0,0 +1,39 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = $1 AND password = $2 AND deletedAt IS NULL`,
+ { bind: [ req.body.email, security.hash(req.body.password) ], model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginJimChallenge_2.ts b/data/static/codefixes/loginJimChallenge_2.ts
new file mode 100644
index 00000000000..bb28fbab418
--- /dev/null
+++ b/data/static/codefixes/loginJimChallenge_2.ts
@@ -0,0 +1,38 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: models.User, plain: false })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginJimChallenge_3.ts b/data/static/codefixes/loginJimChallenge_3.ts
new file mode 100644
index 00000000000..4a443e0f722
--- /dev/null
+++ b/data/static/codefixes/loginJimChallenge_3.ts
@@ -0,0 +1,39 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ models.sequelize.query(`SELECT * FROM Users WHERE email = ? AND password = ? AND deletedAt IS NULL`,
+ { replacements: [ req.body.email, req.body.password ], model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/loginJimChallenge_4.ts b/data/static/codefixes/loginJimChallenge_4.ts
new file mode 100644
index 00000000000..7595fe42a0e
--- /dev/null
+++ b/data/static/codefixes/loginJimChallenge_4.ts
@@ -0,0 +1,41 @@
+import {BasketModel} from "../../../models/basket";
+
+module.exports = function login () {
+ function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) {
+ BasketModel.findOrCreate({ where: { UserId: user.data.id } })
+ .then(([basket]: [BasketModel, boolean]) => {
+ const token = security.authorize(user)
+ user.bid = basket.id // keep track of original basket
+ security.authenticatedUsers.put(token, user)
+ res.json({ authentication: { token, bid: basket.id, umail: user.data.email } })
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ if (req.body.email.match(/.*['-;].*/) || req.body.password.match(/.*['-;].*/)) {
+ res.status(451).send(res.__('SQL Injection detected.'))
+ }
+ models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: models.User, plain: true })
+ .then((authenticatedUser: { data: User }) => {
+ const user = utils.queryResultToJson(authenticatedUser)
+ if (user.data?.id && user.data.totpSecret !== '') {
+ res.status(401).json({
+ status: 'totp_token_required',
+ data: {
+ tmpToken: security.authorize({
+ userId: user.data.id,
+ type: 'password_valid_needs_second_factor_token'
+ })
+ }
+ })
+ } else if (user.data?.id) {
+ afterLogin(user, res, next)
+ } else {
+ res.status(401).send(res.__('Invalid email or password.'))
+ }
+ }).catch((error: Error) => {
+ next(error)
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/noSqlReviewsChallenge.info.yml b/data/static/codefixes/noSqlReviewsChallenge.info.yml
new file mode 100644
index 00000000000..9c215f33605
--- /dev/null
+++ b/data/static/codefixes/noSqlReviewsChallenge.info.yml
@@ -0,0 +1,12 @@
+fixes:
+ - id: 1
+ explanation: 'Removing the option to update multiple documents at once combined with avoiding a "not-equal"-based injection is insufficient against any attacker with at least moderate MongoDB query knowledge.'
+ - id: 2
+ explanation: 'Removing the option to update multiple documents at once is definitely necessary. But it is unfortunately not a sufficient fix, as an attacker might still be able to "add back" the multi-update behavior.'
+ - id: 3
+ explanation: 'Removing the option to update multiple documents at once combined with only allowing plain strings in the ID parameter is the right call. This will prevent any attacker from injecting their own JSON payload to manipulate the query in their favor.'
+hints:
+ - "To find the culprit lines, you need to understand how MongoDB handles updating records."
+ - "Does this query really need to allow updating more than one review at once?"
+ - "Consider the query parameters under control of the attacker and try to find the one where they might inject some query-altering command."
+
diff --git a/data/static/codefixes/noSqlReviewsChallenge_1.ts b/data/static/codefixes/noSqlReviewsChallenge_1.ts
new file mode 100644
index 00000000000..58e13043aad
--- /dev/null
+++ b/data/static/codefixes/noSqlReviewsChallenge_1.ts
@@ -0,0 +1,20 @@
+module.exports = function productReviews () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ const user = security.authenticatedUsers.from(req)
+
+ if (req.body.id['$ne'] !== undefined) {
+ res.status(400).send()
+ return
+ }
+
+ db.reviews.update(
+ { _id: req.body.id },
+ { $set: { message: req.body.message } }
+ ).then(
+ (result: { modified: number, original: Array<{ author: any }> }) => {
+ res.json(result)
+ }, (err: unknown) => {
+ res.status(500).json(err)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/noSqlReviewsChallenge_2.ts b/data/static/codefixes/noSqlReviewsChallenge_2.ts
new file mode 100644
index 00000000000..6f7eb15938f
--- /dev/null
+++ b/data/static/codefixes/noSqlReviewsChallenge_2.ts
@@ -0,0 +1,14 @@
+module.exports = function productReviews () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ const user = security.authenticatedUsers.from(req)
+ db.reviews.update(
+ { _id: req.body.id },
+ { $set: { message: req.body.message } }
+ ).then(
+ (result: { modified: number, original: Array<{ author: any }> }) => {
+ res.json(result)
+ }, (err: unknown) => {
+ res.status(500).json(err)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/noSqlReviewsChallenge_3_correct.ts b/data/static/codefixes/noSqlReviewsChallenge_3_correct.ts
new file mode 100644
index 00000000000..56696db9eff
--- /dev/null
+++ b/data/static/codefixes/noSqlReviewsChallenge_3_correct.ts
@@ -0,0 +1,20 @@
+module.exports = function productReviews () {
+ return (req: Request, res: Response, next: NextFunction) => {
+ const user = security.authenticatedUsers.from(req)
+
+ if (typeof req.body.id !== 'string') {
+ res.status(400).send()
+ return
+ }
+
+ db.reviews.update(
+ { _id: req.body.id },
+ { $set: { message: req.body.message } }
+ ).then(
+ (result: { modified: number, original: Array<{ author: any }> }) => {
+ res.json(result)
+ }, (err: unknown) => {
+ res.status(500).json(err)
+ })
+ }
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectChallenge.info.yml b/data/static/codefixes/redirectChallenge.info.yml
new file mode 100644
index 00000000000..0e041d5f26c
--- /dev/null
+++ b/data/static/codefixes/redirectChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: "The open redirect flaw in this code cannot be fixed by applying URL encoding to the target URL. In fact, it would break the entire redirect mechanism for allow-listed URLs as they are not URL-encoded and would therefore never match."
+ - id: 2
+ explanation: 'Changing from logical "or" to logical "and" here does not do anything for security but entirely breaks the redirect mechanism as "allowed" can never be true after the loop.'
+ - id: 3
+ explanation: "HTML-escaping is completely wrong in this situation because the code is dealing with URLs and not HTML input."
+ - id: 4
+ explanation: "Using indexOf allowed any URLs as long as they contained any allow-listed URL, even if it just would be as a parameter. Replacing this with an actual equality check mitigates this lapse and makes the redirect only work for allow-listed URLs."
+hints:
+ - "You should take a close look at how this code checks for allowed vs. forbidded URLs to redirect to."
+ - "Try to play through how the logical operators and used standard functions work in this situation."
+ - "Could you somehow make the code believe that it is dealing with an allow-listed URL while it actually isn't?"
diff --git a/data/static/codefixes/redirectChallenge_1.ts b/data/static/codefixes/redirectChallenge_1.ts
new file mode 100644
index 00000000000..4a67fb11ee7
--- /dev/null
+++ b/data/static/codefixes/redirectChallenge_1.ts
@@ -0,0 +1,19 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm',
+ 'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW',
+ 'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed || url.includes(encodeURI(allowedUrl))
+ }
+ return allowed
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectChallenge_2.ts b/data/static/codefixes/redirectChallenge_2.ts
new file mode 100644
index 00000000000..7491b8c622d
--- /dev/null
+++ b/data/static/codefixes/redirectChallenge_2.ts
@@ -0,0 +1,19 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm',
+ 'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW',
+ 'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed && url.includes(allowedUrl)
+ }
+ return allowed
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectChallenge_3.ts b/data/static/codefixes/redirectChallenge_3.ts
new file mode 100644
index 00000000000..c72e969c31a
--- /dev/null
+++ b/data/static/codefixes/redirectChallenge_3.ts
@@ -0,0 +1,32 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm',
+ 'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW',
+ 'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed || url.includes(escapeHTML(allowedUrl))
+ }
+ return allowed
+}
+
+const escapeHTML = str => {
+ return str.replace(/[&<>'"]/g,
+ tag => {
+ return ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ "'": ''',
+ '"': '"'
+ }[tag])
+ })
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectChallenge_4_correct.ts b/data/static/codefixes/redirectChallenge_4_correct.ts
new file mode 100644
index 00000000000..dfb43a5637e
--- /dev/null
+++ b/data/static/codefixes/redirectChallenge_4_correct.ts
@@ -0,0 +1,19 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm',
+ 'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW',
+ 'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed || url === allowedUrl
+ }
+ return allowed
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectCryptoCurrencyChallenge.info.yml b/data/static/codefixes/redirectCryptoCurrencyChallenge.info.yml
new file mode 100644
index 00000000000..c6ce2001648
--- /dev/null
+++ b/data/static/codefixes/redirectCryptoCurrencyChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: "This fix removes one deprecated crypto currency address from the allow list but forgets to deal with two other ones."
+ - id: 2
+ explanation: "This fix removes one deprecated crypto currency address from the allow list but forgets to deal with two other ones."
+ - id: 3
+ explanation: "When cleaning up any allow list of deprecated entries, it is crucial to be thorough and re-check the list regularly. Otherwise allow lists tend to become weaker over time."
+ - id: 4
+ explanation: "This fix removes one deprecated crypto currency address from the allow list but forgets to deal with two other ones."
+hints:
+ - "Can you identify the lines which have something to do with crypto currency addresses?"
+ - "Did you notice there is a constant containing allowed redirect web addresses?"
+ - "Make sure to select all three lines responsible for crypto currency addresses which are not promoted any longer."
diff --git a/data/static/codefixes/redirectCryptoCurrencyChallenge_1.ts b/data/static/codefixes/redirectCryptoCurrencyChallenge_1.ts
new file mode 100644
index 00000000000..7c41ed8e336
--- /dev/null
+++ b/data/static/codefixes/redirectCryptoCurrencyChallenge_1.ts
@@ -0,0 +1,18 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW',
+ 'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed || url.includes(allowedUrl)
+ }
+ return allowed
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectCryptoCurrencyChallenge_2.ts b/data/static/codefixes/redirectCryptoCurrencyChallenge_2.ts
new file mode 100644
index 00000000000..2101f393ecb
--- /dev/null
+++ b/data/static/codefixes/redirectCryptoCurrencyChallenge_2.ts
@@ -0,0 +1,18 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm',
+ 'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed || url.includes(allowedUrl)
+ }
+ return allowed
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectCryptoCurrencyChallenge_3_correct.ts b/data/static/codefixes/redirectCryptoCurrencyChallenge_3_correct.ts
new file mode 100644
index 00000000000..7c6dbfbff19
--- /dev/null
+++ b/data/static/codefixes/redirectCryptoCurrencyChallenge_3_correct.ts
@@ -0,0 +1,16 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed || url.includes(allowedUrl)
+ }
+ return allowed
+}
\ No newline at end of file
diff --git a/data/static/codefixes/redirectCryptoCurrencyChallenge_4.ts b/data/static/codefixes/redirectCryptoCurrencyChallenge_4.ts
new file mode 100644
index 00000000000..5150c77c03c
--- /dev/null
+++ b/data/static/codefixes/redirectCryptoCurrencyChallenge_4.ts
@@ -0,0 +1,18 @@
+const redirectAllowlist = new Set([
+ 'https://github.com/bkimminich/juice-shop',
+ 'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm',
+ 'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW',
+ 'http://shop.spreadshirt.com/juiceshop',
+ 'http://shop.spreadshirt.de/juiceshop',
+ 'https://www.stickeryou.com/products/owasp-juice-shop/794',
+ 'http://leanpub.com/juice-shop'
+])
+exports.redirectAllowlist = redirectAllowlist
+
+exports.isRedirectAllowed = (url: string) => {
+ let allowed = false
+ for (const allowedUrl of redirectAllowlist) {
+ allowed = allowed || url.includes(allowedUrl)
+ }
+ return allowed
+}
\ No newline at end of file
diff --git a/data/static/codefixes/registerAdminChallenge.info.yml b/data/static/codefixes/registerAdminChallenge.info.yml
new file mode 100644
index 00000000000..2975029e6af
--- /dev/null
+++ b/data/static/codefixes/registerAdminChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: 'This code change will check if a role is already defined on the user entity. If so, it will keep it. If not, it will set "customer" as a fallback role. This still allows anyone to pick their own prefered role, though.'
+ - id: 2
+ explanation: "Removing the interceptor function completely not only keeps the role assignment possible, it also breaks functionality by no longer creating digital wallets for new users."
+ - id: 3
+ explanation: 'This actually fixes the role assignment issue, by overriding any value pre-set via the POST request with a static "customer" default role.'
+ - id: 4
+ explanation: 'This change results in the "role" property not being returned in any User-API responses. This will not prevent setting an arbitrary role during user creation but probably also break some functionality in the client that relies on the role being present.'
+hints:
+ - "Which entity is this challenge most likely about? Try to find all code places where that entity is somehow processed."
+ - "In this snippet you must look for a place where something is missing that, if present, would negate an arbitrary role assignment."
+ - "Make sure that you do not select any lines that are contained in the vulnerable function but themselves have nothing to do with the vulberability."
diff --git a/data/static/codefixes/registerAdminChallenge_1.ts b/data/static/codefixes/registerAdminChallenge_1.ts
new file mode 100644
index 00000000000..2f4c1a031d2
--- /dev/null
+++ b/data/static/codefixes/registerAdminChallenge_1.ts
@@ -0,0 +1,36 @@
+/* Generated API endpoints */
+ finale.initialize({ app, sequelize })
+
+ const autoModels = [
+ { name: 'User', exclude: ['password', 'totpSecret'], model: UserModel },
+ { name: 'Product', exclude: [], model: ProductModel },
+ { name: 'Feedback', exclude: [], model: FeedbackModel },
+ { name: 'BasketItem', exclude: [], model: BasketItemModel },
+ { name: 'Challenge', exclude: [], model: ChallengeModel },
+ { name: 'Complaint', exclude: [], model: ComplaintModel },
+ { name: 'Recycle', exclude: [], model: RecycleModel },
+ { name: 'SecurityQuestion', exclude: [], model: SecurityQuestionModel },
+ { name: 'SecurityAnswer', exclude: [], model: SecurityAnswerModel },
+ { name: 'Address', exclude: [], model: AddressModel },
+ { name: 'PrivacyRequest', exclude: [], model: PrivacyRequestModel },
+ { name: 'Card', exclude: [], model: CardModel },
+ { name: 'Quantity', exclude: [], model: QuantityModel }
+ ]
+
+ for (const { name, exclude, model } of autoModels) {
+ const resource = finale.resource({
+ model,
+ endpoints: [`/api/${name}s`, `/api/${name}s/:id`],
+ excludeAttributes: exclude
+ })
+
+ // create a wallet when a new user is registered using API
+ if (name === 'User') {
+ resource.create.send.before((req: Request, res: Response, context: { instance: { id: any }, continue: any }) => {
+ WalletModel.create({ UserId: context.instance.id }).catch((err: unknown) => {
+ console.log(err)
+ })
+ context.instance.role = context.instance.role ? context.instance.role : 'customer'
+ return context.continue
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/registerAdminChallenge_2.ts b/data/static/codefixes/registerAdminChallenge_2.ts
new file mode 100644
index 00000000000..946b2ed198a
--- /dev/null
+++ b/data/static/codefixes/registerAdminChallenge_2.ts
@@ -0,0 +1,24 @@
+/* Generated API endpoints */
+ finale.initialize({ app, sequelize })
+
+ const autoModels = [
+ { name: 'Product', exclude: [], model: ProductModel },
+ { name: 'Feedback', exclude: [], model: FeedbackModel },
+ { name: 'BasketItem', exclude: [], model: BasketItemModel },
+ { name: 'Challenge', exclude: [], model: ChallengeModel },
+ { name: 'Complaint', exclude: [], model: ComplaintModel },
+ { name: 'Recycle', exclude: [], model: RecycleModel },
+ { name: 'SecurityQuestion', exclude: [], model: SecurityQuestionModel },
+ { name: 'SecurityAnswer', exclude: [], model: SecurityAnswerModel },
+ { name: 'Address', exclude: [], model: AddressModel },
+ { name: 'PrivacyRequest', exclude: [], model: PrivacyRequestModel },
+ { name: 'Card', exclude: [], model: CardModel },
+ { name: 'Quantity', exclude: [], model: QuantityModel }
+ ]
+
+ for (const { name, exclude, model } of autoModels) {
+ const resource = finale.resource({
+ model,
+ endpoints: [`/api/${name}s`, `/api/${name}s/:id`],
+ excludeAttributes: exclude
+ })
\ No newline at end of file
diff --git a/data/static/codefixes/registerAdminChallenge_3_correct.ts b/data/static/codefixes/registerAdminChallenge_3_correct.ts
new file mode 100644
index 00000000000..6acec5621a0
--- /dev/null
+++ b/data/static/codefixes/registerAdminChallenge_3_correct.ts
@@ -0,0 +1,36 @@
+/* Generated API endpoints */
+ finale.initialize({ app, sequelize })
+
+ const autoModels = [
+ { name: 'User', exclude: ['password', 'totpSecret'], model: UserModel },
+ { name: 'Product', exclude: [], model: ProductModel },
+ { name: 'Feedback', exclude: [], model: FeedbackModel },
+ { name: 'BasketItem', exclude: [], model: BasketItemModel },
+ { name: 'Challenge', exclude: [], model: ChallengeModel },
+ { name: 'Complaint', exclude: [], model: ComplaintModel },
+ { name: 'Recycle', exclude: [], model: RecycleModel },
+ { name: 'SecurityQuestion', exclude: [], model: SecurityQuestionModel },
+ { name: 'SecurityAnswer', exclude: [], model: SecurityAnswerModel },
+ { name: 'Address', exclude: [], model: AddressModel },
+ { name: 'PrivacyRequest', exclude: [], model: PrivacyRequestModel },
+ { name: 'Card', exclude: [], model: CardModel },
+ { name: 'Quantity', exclude: [], model: QuantityModel }
+ ]
+
+ for (const { name, exclude, model } of autoModels) {
+ const resource = finale.resource({
+ model,
+ endpoints: [`/api/${name}s`, `/api/${name}s/:id`],
+ excludeAttributes: exclude
+ })
+
+ // create a wallet when a new user is registered using API
+ if (name === 'User') {
+ resource.create.send.before((req: Request, res: Response, context: { instance: { id: any }, continue: any }) => {
+ WalletModel.create({ UserId: context.instance.id }).catch((err: unknown) => {
+ console.log(err)
+ })
+ context.instance.role = 'customer'
+ return context.continue
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/registerAdminChallenge_4.ts b/data/static/codefixes/registerAdminChallenge_4.ts
new file mode 100644
index 00000000000..2a8379ed178
--- /dev/null
+++ b/data/static/codefixes/registerAdminChallenge_4.ts
@@ -0,0 +1,35 @@
+/* Generated API endpoints */
+ finale.initialize({ app, sequelize })
+
+ const autoModels = [
+ { name: 'User', exclude: ['password', 'totpSecret', 'role'], model: UserModel },
+ { name: 'Product', exclude: [], model: ProductModel },
+ { name: 'Feedback', exclude: [], model: FeedbackModel },
+ { name: 'BasketItem', exclude: [], model: BasketItemModel },
+ { name: 'Challenge', exclude: [], model: ChallengeModel },
+ { name: 'Complaint', exclude: [], model: ComplaintModel },
+ { name: 'Recycle', exclude: [], model: RecycleModel },
+ { name: 'SecurityQuestion', exclude: [], model: SecurityQuestionModel },
+ { name: 'SecurityAnswer', exclude: [], model: SecurityAnswerModel },
+ { name: 'Address', exclude: [], model: AddressModel },
+ { name: 'PrivacyRequest', exclude: [], model: PrivacyRequestModel },
+ { name: 'Card', exclude: [], model: CardModel },
+ { name: 'Quantity', exclude: [], model: QuantityModel }
+ ]
+
+ for (const { name, exclude, model } of autoModels) {
+ const resource = finale.resource({
+ model,
+ endpoints: [`/api/${name}s`, `/api/${name}s/:id`],
+ excludeAttributes: exclude
+ })
+
+ // create a wallet when a new user is registered using API
+ if (name === 'User') {
+ resource.create.send.before((req: Request, res: Response, context: { instance: { id: any }, continue: any }) => {
+ WalletModel.create({ UserId: context.instance.id }).catch((err: unknown) => {
+ console.log(err)
+ })
+ return context.continue
+ })
+ }
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBenderChallenge.info.yml b/data/static/codefixes/resetPasswordBenderChallenge.info.yml
new file mode 100644
index 00000000000..26ddc23f443
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBenderChallenge.info.yml
@@ -0,0 +1,10 @@
+fixes:
+ - id: 1
+ explanation: "While not necessarily as trivial to research via a user's LinkedIn profile, the question is still easy to research or brute force when answered truthfully."
+ - id: 2
+ explanation: "When answered truthfully, all security questions are susceptible to online research (on Facebook, LinkedIn etc.) and often even brute force. If at all, they should not be used as the only factor for a security-relevant function."
+ - id: 3
+ explanation: 'Exchanging "company" with "organization" is only a vocabulary change and has no effect on security.'
+hints:
+ - "Do you remember the security question that Bender used for his account?"
+ - "This question is the source of the security risk in this challenge."
diff --git a/data/static/codefixes/resetPasswordBenderChallenge_1.yml b/data/static/codefixes/resetPasswordBenderChallenge_1.yml
new file mode 100644
index 00000000000..3a3047f6924
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBenderChallenge_1.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'First job you had as a teenager?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBenderChallenge_2_correct.yml b/data/static/codefixes/resetPasswordBenderChallenge_2_correct.yml
new file mode 100644
index 00000000000..a5b448799f7
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBenderChallenge_2_correct.yml
@@ -0,0 +1,4 @@
+# Provide password reset option via a one-time link with
+# short expiration span to registered email address instead
+# of allowing reset on-the-fly by answering a security
+# question.
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBenderChallenge_3.yml b/data/static/codefixes/resetPasswordBenderChallenge_3.yml
new file mode 100644
index 00000000000..9a479307490
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBenderChallenge_3.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'Organization you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBjoernChallenge.info.yml b/data/static/codefixes/resetPasswordBjoernChallenge.info.yml
new file mode 100644
index 00000000000..bb3d2aff382
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernChallenge.info.yml
@@ -0,0 +1,10 @@
+fixes:
+ - id: 1
+ explanation: "When answered truthfully, all security questions are susceptible to online research (on Facebook, LinkedIn etc.) and often even brute force. If at all, they should not be used as the only factor for a security-relevant function."
+ - id: 2
+ explanation: 'When changing the scope of this question from "teenager" to "toddler", researching a past place of residence still is the only (low) hurdle for the attacker.'
+ - id: 3
+ explanation: "Researching someone's current place of residence is probably even easier than a past one."
+hints:
+ - "Do you remember the security question that Bjoern used for his account?"
+ - "This question is the source of the security risk in this challenge."
diff --git a/data/static/codefixes/resetPasswordBjoernChallenge_1_correct.yml b/data/static/codefixes/resetPasswordBjoernChallenge_1_correct.yml
new file mode 100644
index 00000000000..a5b448799f7
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernChallenge_1_correct.yml
@@ -0,0 +1,4 @@
+# Provide password reset option via a one-time link with
+# short expiration span to registered email address instead
+# of allowing reset on-the-fly by answering a security
+# question.
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBjoernChallenge_2.yml b/data/static/codefixes/resetPasswordBjoernChallenge_2.yml
new file mode 100644
index 00000000000..cf055e8c19f
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernChallenge_2.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a toddler?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBjoernChallenge_3.yml b/data/static/codefixes/resetPasswordBjoernChallenge_3.yml
new file mode 100644
index 00000000000..78fe6df8fe4
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernChallenge_3.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code where you live today?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBjoernOwaspChallenge.info.yml b/data/static/codefixes/resetPasswordBjoernOwaspChallenge.info.yml
new file mode 100644
index 00000000000..8feae1e2102
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernOwaspChallenge.info.yml
@@ -0,0 +1,10 @@
+fixes:
+ - id: 1
+ explanation: 'This fix option is obviously (?) a joke. But it should still illustrate that narrowing the scope of a question reduces the solution space accordingly, thus making "social stalking" and brute force much easier.'
+ - id: 2
+ explanation: "When answered truthfully, all security questions are susceptible to online research (on Facebook, LinkedIn etc.) and often even brute force. If at all, they should not be used as the only factor for a security-relevant function."
+ - id: 3
+ explanation: "There are even less car brands in the world than potential pet names. Therefore, changing the security questions has even a negative effect on overall security as it makes guessing and brute forcing much easier."
+hints:
+ - "Do you remember the security question that Bjoern used for his OWASP account?"
+ - "This question is the source of the security risk in this challenge."
diff --git a/data/static/codefixes/resetPasswordBjoernOwaspChallenge_1.yml b/data/static/codefixes/resetPasswordBjoernOwaspChallenge_1.yml
new file mode 100644
index 00000000000..3fc5a02a777
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernOwaspChallenge_1.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite super-cute three-legged cat?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBjoernOwaspChallenge_2_correct.yml b/data/static/codefixes/resetPasswordBjoernOwaspChallenge_2_correct.yml
new file mode 100644
index 00000000000..a5b448799f7
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernOwaspChallenge_2_correct.yml
@@ -0,0 +1,4 @@
+# Provide password reset option via a one-time link with
+# short expiration span to registered email address instead
+# of allowing reset on-the-fly by answering a security
+# question.
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordBjoernOwaspChallenge_3.yml b/data/static/codefixes/resetPasswordBjoernOwaspChallenge_3.yml
new file mode 100644
index 00000000000..e4557f80391
--- /dev/null
+++ b/data/static/codefixes/resetPasswordBjoernOwaspChallenge_3.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Brand of your favorite car?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordJimChallenge.info.yml b/data/static/codefixes/resetPasswordJimChallenge.info.yml
new file mode 100644
index 00000000000..41ce5a9005a
--- /dev/null
+++ b/data/static/codefixes/resetPasswordJimChallenge.info.yml
@@ -0,0 +1,10 @@
+fixes:
+ - id: 1
+ explanation: 'Widening the scope from an "eldest sibling" to "any family member" still allows the question to be easily researched online (on Facebook etc.) or brute forced when answered truthfully.'
+ - id: 2
+ explanation: 'Tightening the scope from an "eldest sibling" to "eldest brother" reduces any brute force effort to only male forenames, assuming the question is answered truthfully.'
+ - id: 3
+ explanation: "When answered truthfully, all security questions are susceptible to online research (on Facebook, LinkedIn etc.) and often even brute force. If at all, they should not be used as the only factor for a security-relevant function."
+hints:
+ - "Do you remember the security question that Jim used for his account?"
+ - "This question is the source of the security risk in this challenge."
diff --git a/data/static/codefixes/resetPasswordJimChallenge_1.yml b/data/static/codefixes/resetPasswordJimChallenge_1.yml
new file mode 100644
index 00000000000..d96682c0bd3
--- /dev/null
+++ b/data/static/codefixes/resetPasswordJimChallenge_1.yml
@@ -0,0 +1,28 @@
+-
+ question: "Any family member's middle name?"
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordJimChallenge_2.yml b/data/static/codefixes/resetPasswordJimChallenge_2.yml
new file mode 100644
index 00000000000..19714eb66ae
--- /dev/null
+++ b/data/static/codefixes/resetPasswordJimChallenge_2.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest brothers middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite movie?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordJimChallenge_3_correct.yml b/data/static/codefixes/resetPasswordJimChallenge_3_correct.yml
new file mode 100644
index 00000000000..a5b448799f7
--- /dev/null
+++ b/data/static/codefixes/resetPasswordJimChallenge_3_correct.yml
@@ -0,0 +1,4 @@
+# Provide password reset option via a one-time link with
+# short expiration span to registered email address instead
+# of allowing reset on-the-fly by answering a security
+# question.
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordMortyChallenge.info.yml b/data/static/codefixes/resetPasswordMortyChallenge.info.yml
new file mode 100644
index 00000000000..02e0e0a89ec
--- /dev/null
+++ b/data/static/codefixes/resetPasswordMortyChallenge.info.yml
@@ -0,0 +1,13 @@
+fixes:
+ - id: 1
+ explanation: "Removing the setting to trust proxies does not improve security of the rate limiting. It might have some unforseen or unintended functional side-effects, though."
+ - id: 2
+ explanation: 'Replacing the "X-Forwarded-For" header with its standardized alternative "Forwarded" does not close the security flaw of how this header is actually being used and can be abused by attackers.'
+ - id: 3
+ explanation: "Reducing the rate limit from 100 requests in 5min to 10 reqests in 3min could be seen as a security improvement, if there wasn't an entirely unrelated misconfiguration at play here."
+ - id: 4
+ explanation: "Removing the custom key generator that lets an arbitrary HTTP header take precedence over the client IP is the best option here. Now an attacker at least needs to fake their actual IP to bypass the rate limiting, as this is the default key for the RateLimit module used here. There is a functional downside though, as now users behin e.g. corporate proxies might be rate limited as a group and not individually. But with 100 allowed password resets in 5min this should not occur too frequently."
+hints:
+ - "The security flaw has something to do with the rate limiting configuration."
+ - "Do you think the time window or number of requests is the actual problem here? Maybe there is something else going wrong..."
+ - 'Take a close look at the HTTP header being used here and ask yourself: "Could an attacker do anything with it to bypass rate limiting?"'
diff --git a/data/static/codefixes/resetPasswordMortyChallenge_1.ts b/data/static/codefixes/resetPasswordMortyChallenge_1.ts
new file mode 100644
index 00000000000..61c0e3f861d
--- /dev/null
+++ b/data/static/codefixes/resetPasswordMortyChallenge_1.ts
@@ -0,0 +1,6 @@
+/* Rate limiting */
+ app.use('/rest/user/reset-password', new RateLimit({
+ windowMs: 5 * 60 * 1000,
+ max: 100,
+ keyGenerator ({ headers, ip }) { return headers['X-Forwarded-For'] || ip }
+ }))
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordMortyChallenge_2.ts b/data/static/codefixes/resetPasswordMortyChallenge_2.ts
new file mode 100644
index 00000000000..10ff1fe3fa5
--- /dev/null
+++ b/data/static/codefixes/resetPasswordMortyChallenge_2.ts
@@ -0,0 +1,7 @@
+/* Rate limiting */
+ app.enable('trust proxy')
+ app.use('/rest/user/reset-password', new RateLimit({
+ windowMs: 5 * 60 * 1000,
+ max: 100,
+ keyGenerator ({ headers, ip }) { return headers['Forwarded'] || ip }
+ }))
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordMortyChallenge_3.ts b/data/static/codefixes/resetPasswordMortyChallenge_3.ts
new file mode 100644
index 00000000000..e6a68f6c258
--- /dev/null
+++ b/data/static/codefixes/resetPasswordMortyChallenge_3.ts
@@ -0,0 +1,7 @@
+/* Rate limiting */
+ app.enable('trust proxy')
+ app.use('/rest/user/reset-password', new RateLimit({
+ windowMs: 3 * 60 * 1000,
+ max: 10,
+ keyGenerator ({ headers, ip }) { return headers['X-Forwarded-For'] || ip }
+ }))
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordMortyChallenge_4_correct.ts b/data/static/codefixes/resetPasswordMortyChallenge_4_correct.ts
new file mode 100644
index 00000000000..8a75dc4fc29
--- /dev/null
+++ b/data/static/codefixes/resetPasswordMortyChallenge_4_correct.ts
@@ -0,0 +1,6 @@
+/* Rate limiting */
+ app.enable('trust proxy')
+ app.use('/rest/user/reset-password', new RateLimit({
+ windowMs: 5 * 60 * 1000,
+ max: 100,
+ }))
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordUvoginChallenge.info.yml b/data/static/codefixes/resetPasswordUvoginChallenge.info.yml
new file mode 100644
index 00000000000..5acf46c0092
--- /dev/null
+++ b/data/static/codefixes/resetPasswordUvoginChallenge.info.yml
@@ -0,0 +1,10 @@
+fixes:
+ - id: 1
+ explanation: 'Narrowing the scope of the question from "movie" to "animé" dramatically reduces the solution space, thus making guessing and brute force attacks a lot easier.'
+ - id: 2
+ explanation: 'When changing the scope of this question from "movie" to "actor/actress", researching and brute forcing is probably just as easy for the attacker.'
+ - id: 3
+ explanation: "When answered truthfully, all security questions are susceptible to online research (on Facebook, LinkedIn etc.) and often even brute force. If at all, they should not be used as the only factor for a security-relevant function."
+hints:
+ - "Do you remember the security question that Uvogin used for his account?"
+ - "This question is the source of the security risk in this challenge."
diff --git a/data/static/codefixes/resetPasswordUvoginChallenge_1.yml b/data/static/codefixes/resetPasswordUvoginChallenge_1.yml
new file mode 100644
index 00000000000..d0d3f220d8c
--- /dev/null
+++ b/data/static/codefixes/resetPasswordUvoginChallenge_1.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite animé?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordUvoginChallenge_2.yml b/data/static/codefixes/resetPasswordUvoginChallenge_2.yml
new file mode 100644
index 00000000000..4628887f7d6
--- /dev/null
+++ b/data/static/codefixes/resetPasswordUvoginChallenge_2.yml
@@ -0,0 +1,28 @@
+-
+ question: 'Your eldest siblings middle name?'
+-
+ question: "Mother's maiden name?"
+-
+ question: "Mother's birth date? (MM/DD/YY)"
+-
+ question: "Father's birth date? (MM/DD/YY)"
+-
+ question: "Maternal grandmother's first name?"
+-
+ question: "Paternal grandmother's first name?"
+-
+ question: 'Name of your favorite pet?'
+-
+ question: "Last name of dentist when you were a teenager? (Do not include 'Dr.')"
+-
+ question: 'Your ZIP/postal code when you were a teenager?'
+-
+ question: 'Company you first work for as an adult?'
+-
+ question: 'Your favorite book?'
+-
+ question: 'Your favorite actor or actress?'
+-
+ question: 'Number of one of your customer or ID cards?'
+-
+ question: "What's your favorite place to go hiking?"
\ No newline at end of file
diff --git a/data/static/codefixes/resetPasswordUvoginChallenge_3_correct.yml b/data/static/codefixes/resetPasswordUvoginChallenge_3_correct.yml
new file mode 100644
index 00000000000..a5b448799f7
--- /dev/null
+++ b/data/static/codefixes/resetPasswordUvoginChallenge_3_correct.yml
@@ -0,0 +1,4 @@
+# Provide password reset option via a one-time link with
+# short expiration span to registered email address instead
+# of allowing reset on-the-fly by answering a security
+# question.
\ No newline at end of file
diff --git a/data/static/codefixes/restfulXssChallenge.info.yml b/data/static/codefixes/restfulXssChallenge.info.yml
new file mode 100644
index 00000000000..a4487eb2127
--- /dev/null
+++ b/data/static/codefixes/restfulXssChallenge.info.yml
@@ -0,0 +1,12 @@
+fixes:
+ - id: 1
+ explanation: 'Removing the bypass of sanitization entirely is the best way to fix the XSS vulnerability here. It should be noted, that XSS is only a consequence of broken autheorization in this case, as users should not be allowed to change product descriptions in the first place.'
+ - id: 2
+ explanation: 'Manually encoding the angular brackets of the HTML tags does not add any security. It is likely to break descriptions with legitimate HTML tags for styling or links, though.'
+ - id: 3
+ explanation: 'The removed code block deals with handling of different screen sizes and is entirely unrelated to the given XSS vulnerability.'
+ - id: 4
+ explanation: 'Using bypassSecurityTrustScript() instead of bypassSecurityTrustHtml() changes the context for which input sanitization is bypassed. If at all, this switch might only accidentally keep XSS prevention intact.'
+hints:
+ - "Find all places in the code which are handling the product descriptions."
+ - "Look for a line where the developers fiddled with Angular's built-in security model."
diff --git a/data/static/codefixes/restfulXssChallenge_1_correct.ts b/data/static/codefixes/restfulXssChallenge_1_correct.ts
new file mode 100644
index 00000000000..0e211e4cd44
--- /dev/null
+++ b/data/static/codefixes/restfulXssChallenge_1_correct.ts
@@ -0,0 +1,54 @@
+ngAfterViewInit () {
+ const products = this.productService.search('')
+ const quantities = this.quantityService.getAll()
+ forkJoin([quantities, products]).subscribe(([quantities, products]) => {
+ const dataTable: TableEntry[] = []
+ this.tableData = products
+ for (const product of products) {
+ dataTable.push({
+ name: product.name,
+ price: product.price,
+ deluxePrice: product.deluxePrice,
+ id: product.id,
+ image: product.image,
+ description: product.description
+ })
+ }
+ for (const quantity of quantities) {
+ const entry = dataTable.find((dataTableEntry) => {
+ return dataTableEntry.id === quantity.ProductId
+ })
+ if (entry === undefined) {
+ continue
+ }
+ entry.quantity = quantity.quantity
+ }
+ this.dataSource = new MatTableDataSource