juice-shop/juice-shop

View on GitHub
test/api/feedbackApiSpec.ts

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * Copyright (c) 2014-2024 Bjoern Kimminich & the OWASP Juice Shop contributors.
 * SPDX-License-Identifier: MIT
 */
import { challenges } from '../../data/datacache'
import frisby = require('frisby')
import { expect } from '@jest/globals'
const Joi = frisby.Joi
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 ' + security.authorize(), 'content-type': /application\/json/ }
const jsonHeader = { 'content-type': 'application/json' }

describe('/api/Feedbacks', () => {
  it('GET all feedback', () => {
    return frisby.get(API_URL + '/Feedbacks')
      .expect('status', 200)
  })

  it('POST sanitizes unsafe HTML from comment', () => {
    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: 'I am a harm<script>steal-cookie</script><img src="csrf-attack"/><iframe src="evil-content"></iframe>less comment.',
            rating: 1,
            captchaId: json.captchaId,
            captcha: json.answer
          }
        })
          .expect('status', 201)
          .expect('json', 'data', {
            comment: 'I am a harmless comment.'
          })
      })
  })

  if (utils.isChallengeEnabled(challenges.persistedXssFeedbackChallenge)) {
    it('POST fails to sanitize masked XSS-attack by not applying sanitization recursively', () => {
      return frisby.get(REST_URL + '/captcha')
        .expect('status', 200)
        .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: <<script>Foo</script>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: <iframe src="javascript:alert(`xss`)">'
            })
        })
    })
  }

  it('POST feedback in another users name as anonymous user', () => {
    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: 'Lousy crap! You use sequelize 1.7.x? Welcome to SQL Injection-land, morons! As if that is not bad enough, you use z85/base85 and hashids for crypto? Even MD5 to hash passwords! Srsly?!?!',
            rating: 1,
            UserId: 3,
            captchaId: json.captchaId,
            captcha: json.answer
          }
        })
          .expect('status', 201)
          .expect('header', 'content-type', /application\/json/)
          .expect('json', 'data', {
            UserId: 3
          })
      })
  })

  it('POST feedback in a non-existing users name as anonymous user fails with constraint error', () => {
    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: 'Pickle Rick says your express-jwt 0.1.3 has Eurogium Edule and Hueteroneel in it!',
            rating: 0,
            UserId: 4711,
            captchaId: json.captchaId,
            captcha: json.answer
          }
        })
          .expect('status', 500)
          .expect('header', 'content-type', /application\/json/)
          .then(({ json }) => {
            expect(json.errors).toContain('SQLITE_CONSTRAINT: FOREIGN KEY constraint failed')
          })
      })
  })

  it('POST feedback is associated with current user', () => {
    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 + '/captcha')
          .expect('status', 200)
          .expect('header', 'content-type', /application\/json/)
          .then(({ json }) => {
            return frisby.post(API_URL + '/Feedbacks', {
              headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' },
              body: {
                comment: 'Stupid JWT secret "' + security.defaultSecret + '" and being typosquatted by epilogue-js and anuglar2-qrcode!',
                rating: 5,
                UserId: 4,
                captchaId: json.captchaId,
                captcha: json.answer
              }
            })
              .expect('status', 201)
              .expect('header', 'content-type', /application\/json/)
              .expect('json', 'data', {
                UserId: 4
              })
          })
      })
  })

  it('POST feedback is associated with any passed user ID', () => {
    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 + '/captcha')
          .expect('status', 200)
          .expect('header', 'content-type', /application\/json/)
          .then(({ json }) => {
            return frisby.post(API_URL + '/Feedbacks', {
              headers: { Authorization: 'Bearer ' + jsonLogin.authentication.token, 'content-type': 'application/json' },
              body: {
                comment: 'Bender\'s choice award!',
                rating: 5,
                UserId: 3,
                captchaId: json.captchaId,
                captcha: json.answer
              }
            })
              .expect('status', 201)
              .expect('header', 'content-type', /application\/json/)
              .expect('json', 'data', {
                UserId: 3
              })
          })
      })
  })

  it('POST feedback can be created without actually supplying comment', () => {
    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: {
            rating: 1,
            captchaId: json.captchaId,
            captcha: json.answer
          }
        })
          .expect('status', 201)
          .expect('header', 'content-type', /application\/json/)
          .expect('json', 'data', {
            comment: null,
            rating: 1
          })
      })
  })

  it('POST feedback cannot be created without actually supplying rating', () => {
    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: {
            captchaId: json.captchaId,
            captcha: json.answer
          }
        })
          .expect('status', 400)
          .expect('header', 'content-type', /application\/json/)
          .expect('jsonTypes', {
            message: Joi.string()
          })
          .then(({ json }) => {
            expect(json.message.match(/notNull Violation: (Feedback\.)?rating cannot be null/))
          })
      })
  })

  it('POST feedback cannot be created with wrong CAPTCHA answer', () => {
    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: {
            rating: 1,
            captchaId: json.captchaId,
            captcha: (json.answer + 1)
          }
        })
          .expect('status', 401)
      })
  })

  it('POST feedback cannot be created with invalid CAPTCHA id', () => {
    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: {
            rating: 1,
            captchaId: 999999,
            captcha: 42
          }
        })
          .expect('status', 401)
      })
  })
})

describe('/api/Feedbacks/:id', () => {
  it('GET existing feedback by id is forbidden via public API', () => {
    return frisby.get(API_URL + '/Feedbacks/1')
      .expect('status', 401)
  })

  it('GET existing feedback by id', () => {
    return frisby.get(API_URL + '/Feedbacks/1', { headers: authHeader })
      .expect('status', 200)
  })

  it('PUT update existing feedback is forbidden via public API', () => {
    return frisby.put(API_URL + '/Feedbacks/1', {
      headers: jsonHeader,
      body: {
        comment: 'This sucks like nothing has ever sucked before',
        rating: 1
      }
    })
      .expect('status', 401)
  })

  it('PUT update existing feedback', () => {
    return frisby.put(API_URL + '/Feedbacks/2', {
      headers: authHeader,
      body: {
        rating: 0
      }
    })
      .expect('status', 401)
  })

  it('DELETE existing feedback is forbidden via public API', () => {
    return frisby.del(API_URL + '/Feedbacks/1')
      .expect('status', 401)
  })

  it('DELETE existing feedback', () => {
    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: 'I will be gone soon!',
            rating: 1,
            captchaId: json.captchaId,
            captcha: json.answer
          }
        })
          .expect('status', 201)
          .expect('jsonTypes', 'data', { id: Joi.number() })
          .then(({ json }) => {
            return frisby.del(API_URL + '/Feedbacks/' + json.data.id, { headers: authHeader })
              .expect('status', 200)
          }
          )
      })
  })
})