diff --git a/2024-11-01-ZAP-Report-.html b/2024-11-01-ZAP-Report-.html new file mode 100644 index 00000000000..dbdd8928ed2 --- /dev/null +++ b/2024-11-01-ZAP-Report-.html @@ -0,0 +1,574 @@ + + + + + +ZAP by Checkmarx Scanning Report + + + + + +
+

ZAP by Checkmarx Scanning Report

+

+ Generated with ZAP + on Fri 1 Nov 2024, at 03:30:50 +

+

ZAP Version: 2.15.0

+

+ ZAP by Checkmarx +

+
+ +
+ +
+

Contents

+ +
+ +
+

About this report

+ + + +
+

Report parameters

+
+

Contexts

+ + +

No contexts were selected, so all contexts were included by default.

+ + +

Sites

+ +

The following sites were included:

+
    +
  • http://cdnjs.cloudflare.com
  • +
  • http://localhost:3000
  • +
+ +

(If no sites were selected, all sites were included by default.)

+

An included site must also be within one of the included contexts for its data to be included in the report.

+ +

Risk levels

+

+ Included: + + High, Medium, Low, Informational +

+

+ Excluded: + None + +

+ +

Confidence levels

+

+ Included: + + + User Confirmed, High, Medium, Low +

+

+ Excluded: + + + User Confirmed, High, Medium, Low, False Positive +

+
+
+
+ + +
+ +
+ +
+

Summaries

+ +
+

Alert counts by risk and confidence

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

This table shows the number of alerts for each level of risk and confidence included in the report.

+

(The percentages in brackets represent the count as a percentage of the total number of alerts included in the report, rounded to one decimal place.)

+
Confidence
User ConfirmedHighMediumLowTotal
RiskHigh0
(0.0%)
0
(0.0%)
0
(0.0%)
1
(100.0%)
1
(100.0%)
Medium0
(0.0%)
0
(0.0%)
0
(0.0%)
0
(0.0%)
0
(0.0%)
Low0
(0.0%)
0
(0.0%)
0
(0.0%)
0
(0.0%)
0
(0.0%)
Informational0
(0.0%)
0
(0.0%)
0
(0.0%)
0
(0.0%)
0
(0.0%)
Total0
(0.0%)
0
(0.0%)
0
(0.0%)
1
(100.0%)
1
(100%)
+
+ +
+

Alert counts by site and risk

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

This table shows, for each site for which one or more alerts were raised, the number of alerts raised at each risk level.

+

Alerts with a confidence level of "False Positive" have been excluded from these counts.

+

(The numbers in brackets are the number of alerts raised for the site at or above that risk level.)

+
Risk
+ High
(= High) +
+ Medium
(>= Medium) +
+ Low
(>= Low) +
+ Informational
(>= Informational) +
Sitehttp://localhost:30001
(1)
0
(1)
0
(1)
0
(1)
+
+ +
+

Alert counts by alert type

+ + + + + + + + + + + + + + + + + + + + + + + +
+

This table shows the number of alerts of each alert type, together with the alert type's risk level.

+

(The percentages in brackets represent each count as a percentage, rounded to one decimal place, of the total number of alerts included in this report.)

+
Alert typeRiskCount
Cloud Metadata Potentially ExposedHigh1
(100.0%)
Total1
+
+
+ +
+

Alerts

+
    + + + + + + + + +
  1. +

    + Risk=High, Confidence=Low (1) +

    +
      + +
    1. +

      + http://localhost:3000 (1) +

      +
        + +
      1. +
        + Cloud Metadata Potentially Exposed (1) +
        +
          +
        1. + + GET http://localhost:3000/latest/meta-data/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
          Alert tags + +
          Alert description +

          The Cloud Metadata Attack attempts to abuse a misconfigured NGINX server in order to access the instance metadata maintained by cloud service providers such as AWS, GCP and Azure.

          + +

          All of these providers provide metadata via an internal unroutable IP address '169.254.169.254' - this can be exposed by incorrectly configured NGINX servers and accessed by using this IP address in the Host header field.

          +
          Other info +

          Based on the successful response status code cloud metadata may have been returned in the response. Check the response data to see if any cloud metadata has been returned.

          + +

          The meta data returned can include information that would allow an attacker to completely compromise the system.

          +
          Request
          + Request line and header section (216 bytes) + +
          GET http://localhost:3000/latest/meta-data/ HTTP/1.1
          +host: 169.254.169.254
          +user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
          +pragma: no-cache
          +cache-control: no-cache
          +
          +
          + + +
          + Request body (0 bytes) + +
          + + +
          Response
          + Status line and header section (466 bytes) + +
          HTTP/1.1 200 OK
          +Access-Control-Allow-Origin: *
          +X-Content-Type-Options: nosniff
          +X-Frame-Options: SAMEORIGIN
          +Feature-Policy: payment 'self'
          +X-Recruiting: /#/jobs
          +Accept-Ranges: bytes
          +Cache-Control: public, max-age=0
          +Last-Modified: Thu, 31 Oct 2024 14:59:54 GMT
          +ETag: W/"7bc-192e3171820"
          +Content-Type: text/html; charset=UTF-8
          +Content-Length: 1980
          +Vary: Accept-Encoding
          +Date: Thu, 31 Oct 2024 16:53:28 GMT
          +Connection: keep-alive
          +Keep-Alive: timeout=5
          +
          +
          + + +
          + Response body (1980 bytes) + +
          <!--
          +  ~ Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors.
          +  ~ SPDX-License-Identifier: MIT
          +  --><!DOCTYPE html><html lang="en"><head>
          +  <meta charset="utf-8">
          +  <title>Guardian Store</title>
          +  <meta name="description" content="Probably the most modern and sophisticated insecure web application">
          +  <meta name="viewport" content="width=device-width, initial-scale=1">
          +  <link id="favicon" rel="icon" type="image/x-icon" href="assets/public/favicon_js.ico">
          +  <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.1.0/cookieconsent.min.css">
          +  <script src="//cdnjs.cloudflare.com/ajax/libs/cookieconsent2/3.1.0/cookieconsent.min.js"></script>
          +  <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
          +  <script>
          +    window.addEventListener("load", function(){
          +      window.cookieconsent.initialise({
          +        "palette": {
          +          "popup": { "background": "#546e7a", "text": "#ffffff" },
          +          "button": { "background": "#558b2f", "text": "#ffffff" }
          +        },
          +        "theme": "classic",
          +        "position": "bottom-right",
          +        "content": { "message": "This website uses fruit cookies to ensure you get the juiciest tracking experience.", "dismiss": "Me want it!", "link": "But me wait!", "href": "https://www.youtube.com/watch?v=9PnbKL3wuH4" }
          +      })});
          +  </script>
          +<style>.bluegrey-lightgreen-theme.mat-app-background{background-color:#303030;color:#fff}@charset "UTF-8";@media screen and (-webkit-min-device-pixel-ratio:0){}</style><link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.css"></noscript></head>
          +<body class="mat-app-background blue-lightblue-theme">
          +  <app-root></app-root>
          +<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script>
          +
          +</body></html>
          + + +
          Attack
          169.254.169.254
          Solution +

          Do not trust any user data in NGINX configs. In this case it is probably the use of the $host variable which is set from the 'Host' header and can be controlled by an attacker.

          +
          + +
        2. +
        +
      2. + +
      +
    2. + +
    +
  2. + + + + +
+
+ +
+

Appendix

+ +
+

Alert types

+

This section contains additional information on the types of alerts in the report.

+
    +
  1. +

    Cloud Metadata Potentially Exposed

    + + + + + + + + + + + +
    Source + + raised by an active scanner (Cloud Metadata Potentially Exposed) + +
    Reference +
      +
    1. https://www.nginx.com/blog/trust-no-one-perils-of-trusting-user-input/
    2. +
    +
    +
  2. +
+
+
+ +
+ + + + + diff --git a/routes/login.ts b/routes/login.ts index fac90717155..78c3bae3d45 100644 --- a/routes/login.ts +++ b/routes/login.ts @@ -1,84 +1,94 @@ -/* - * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. - * SPDX-License-Identifier: MIT - */ - -import models = require('../models/index') -import { Request, Response, NextFunction } from 'express' -import { User } from '../data/types' -import { BasketModel } from '../models/basket' -import { UserModel } from '../models/user' -import challengeUtils = require('../lib/challengeUtils') - -const utils = require('../lib/utils') -const security = require('../lib/insecurity') -const challenges = require('../data/datacache').challenges -const users = require('../data/datacache').users -const config = require('config') - -// vuln-code-snippet start loginAdminChallenge loginBenderChallenge loginJimChallenge -module.exports = function login () { - function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) { - verifyPostLoginChallenges(user) // vuln-code-snippet hide-line - 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) => { - verifyPreLoginChallenges(req) // vuln-code-snippet hide-line - models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge - .then((authenticatedUser: { data: User }) => { // vuln-code-snippet neutral-line loginAdminChallenge loginBenderChallenge loginJimChallenge - 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) - }) - } - // vuln-code-snippet end loginAdminChallenge loginBenderChallenge loginJimChallenge - - function verifyPreLoginChallenges (req: Request) { - challengeUtils.solveIf(challenges.weakPasswordChallenge, () => { return req.body.email === 'admin@' + config.get('application.domain') && req.body.password === 'admin123' }) - challengeUtils.solveIf(challenges.loginSupportChallenge, () => { return req.body.email === 'support@' + config.get('application.domain') && req.body.password === 'J6aVjTgOpRs@?5l!Zkq2AYnCE@RF$P' }) - challengeUtils.solveIf(challenges.loginRapperChallenge, () => { return req.body.email === 'mc.safesearch@' + config.get('application.domain') && req.body.password === 'Mr. N00dles' }) - challengeUtils.solveIf(challenges.loginAmyChallenge, () => { return req.body.email === 'amy@' + config.get('application.domain') && req.body.password === 'K1f.....................' }) - challengeUtils.solveIf(challenges.dlpPasswordSprayingChallenge, () => { return req.body.email === 'J12934@' + config.get('application.domain') && req.body.password === '0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB' }) - challengeUtils.solveIf(challenges.oauthUserPasswordChallenge, () => { return req.body.email === 'bjoern.kimminich@gmail.com' && req.body.password === 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' }) - } - - function verifyPostLoginChallenges (user: { data: User }) { - challengeUtils.solveIf(challenges.loginAdminChallenge, () => { return user.data.id === users.admin.id }) - challengeUtils.solveIf(challenges.loginJimChallenge, () => { return user.data.id === users.jim.id }) - challengeUtils.solveIf(challenges.loginBenderChallenge, () => { return user.data.id === users.bender.id }) - challengeUtils.solveIf(challenges.ghostLoginChallenge, () => { return user.data.id === users.chris.id }) - if (challengeUtils.notSolved(challenges.ephemeralAccountantChallenge) && user.data.email === 'acc0unt4nt@' + config.get('application.domain') && user.data.role === 'accounting') { - UserModel.count({ where: { email: 'acc0unt4nt@' + config.get('application.domain') } }).then((count: number) => { - if (count === 0) { - challengeUtils.solve(challenges.ephemeralAccountantChallenge) - } - }).catch(() => { - throw new Error('Unable to verify challenges! Try again') - }) - } - } -} +/* + * Copyright (c) 2014-2022 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + +import models = require('../models/index') +import { Request, Response, NextFunction } from 'express' +import { User } from '../data/types' +import { BasketModel } from '../models/basket' +import { UserModel } from '../models/user' +import challengeUtils = require('../lib/challengeUtils') + +const utils = require('../lib/utils') +const security = require('../lib/insecurity') +const challenges = require('../data/datacache').challenges +const users = require('../data/datacache').users +const config = require('config') + +// vuln-code-snippet start loginAdminChallenge loginBenderChallenge loginJimChallenge +module.exports = function login () { + function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) { + verifyPostLoginChallenges(user) // vuln-code-snippet hide-line + 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) => { + verifyPreLoginChallenges(req) // vuln-code-snippet hide-line + + UserModel.findOne({ + where: { + email: req.body.email, + password: security.hash(req.body.password) + } + }) + .then((authenticatedUser: UserModel | null) => { + if(authenticatedUser){ + 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) + }) + } + } + // vuln-code-snippet end loginAdminChallenge loginBenderChallenge loginJimChallenge + + function verifyPreLoginChallenges (req: Request) { + challengeUtils.solveIf(challenges.weakPasswordChallenge, () => { return req.body.email === 'admin@' + config.get('application.domain') && req.body.password === 'admin123' }) + challengeUtils.solveIf(challenges.loginSupportChallenge, () => { return req.body.email === 'support@' + config.get('application.domain') && req.body.password === 'J6aVjTgOpRs@?5l!Zkq2AYnCE@RF$P' }) + challengeUtils.solveIf(challenges.loginRapperChallenge, () => { return req.body.email === 'mc.safesearch@' + config.get('application.domain') && req.body.password === 'Mr. N00dles' }) + challengeUtils.solveIf(challenges.loginAmyChallenge, () => { return req.body.email === 'amy@' + config.get('application.domain') && req.body.password === 'K1f.....................' }) + challengeUtils.solveIf(challenges.dlpPasswordSprayingChallenge, () => { return req.body.email === 'J12934@' + config.get('application.domain') && req.body.password === '0Y8rMnww$*9VFYE§59-!Fg1L6t&6lB' }) + challengeUtils.solveIf(challenges.oauthUserPasswordChallenge, () => { return req.body.email === 'bjoern.kimminich@gmail.com' && req.body.password === 'bW9jLmxpYW1nQGhjaW5pbW1pay5ucmVvamI=' }) + } + + function verifyPostLoginChallenges (user: { data: User }) { + challengeUtils.solveIf(challenges.loginAdminChallenge, () => { return user.data.id === users.admin.id }) + challengeUtils.solveIf(challenges.loginJimChallenge, () => { return user.data.id === users.jim.id }) + challengeUtils.solveIf(challenges.loginBenderChallenge, () => { return user.data.id === users.bender.id }) + challengeUtils.solveIf(challenges.ghostLoginChallenge, () => { return user.data.id === users.chris.id }) + if (challengeUtils.notSolved(challenges.ephemeralAccountantChallenge) && user.data.email === 'acc0unt4nt@' + config.get('application.domain') && user.data.role === 'accounting') { + UserModel.count({ where: { email: 'acc0unt4nt@' + config.get('application.domain') } }).then((count: number) => { + if (count === 0) { + challengeUtils.solve(challenges.ephemeralAccountantChallenge) + } + }).catch(() => { + throw new Error('Unable to verify challenges! Try again') + }) + } + } + diff --git a/workflows/vul_discovery.yml b/workflows/vul_discovery.yml new file mode 100644 index 00000000000..39b6977e1c2 --- /dev/null +++ b/workflows/vul_discovery.yml @@ -0,0 +1,70 @@ +name: Vulnerability Discovery + +on: + pull_request: + branches: [develop] + push: + branches: [develop] + +jobs: + codeql-analysis: + name: CodeQL Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'typescript' ] + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Install dependencies + run: | + npm install + + - name: Build application + run: | + npm run build # Adjust if your build command differs + + - name: Run CodeQL Analysis + uses: github/codeql-action/analyze@v2 + + zap-scan: + name: OWASP ZAP Scan + runs-on: ubuntu-latest + needs: codeql-analysis + + steps: + - name: Start OWASP ZAP Docker container + run: docker run -d --name zap -u zap -p 8080:8080 owasp/zap2docker-stable zap.sh -daemon -host 0.0.0.0 -port 8080 + + - name: Run ZAP Active Scan + env: + TARGET_URL: ${{ secrets.STAGING_URL }} # Set your staging URL as a GitHub Secret + run: | + docker exec zap zap-cli quick-scan --self-contained $TARGET_URL + + - name: Generate ZAP Report + run: | + docker exec zap zap-cli report -o /zap/reports/zap-report.html -f html + docker cp zap:/zap/reports/zap-report.html . + + - name: Upload ZAP Report as Artifact + uses: actions/upload-artifact@v2 + with: + name: zap-report + path: zap-report.html + + - name: Stop ZAP container + run: docker stop zap