CVE-2022-23624 (TypeScript, published 2022-02-07T23:15Z)
Description: value: Frourio-express is a minimal full stack framework, for TypeScript. Frourio-express users who uses frourio-express version prior to v0.26.0 and integration with class-validator through `validators/` folder are subject to a input validation vulnerability. Validators do not work properly for request bodies and queries in specific situations and some input is not validated at all. Users are advised to update frourio to v0.26.0 or later and to install `class-transformer` and `reflect-metadata`.
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-1321 \u2014 Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
Repository: https://github.com/frouriojs/frourio-express
Files changed:
__test__/index.spec.ts
package.json
servers/all/$server.ts
servers/all/api/users/_userId@number/controller.ts
servers/all/api/users/controller.ts
servers/all/validators/index.ts
servers/noMulter/$server.ts
servers/noTypedParams/$server.ts
src/buildServerFile.ts
src/checkRequisites.ts
src/createControllersText.ts
yarn.lock
Commit message: feat(validation): use class-transformer to support validation of nested objects

Co-authored-by: SegaraRai <SegaraRai@users.noreply.github.com>


Vulnerable code:
import { validateOrReject, ValidatorOptions } from 'class-validator'
// prettier-ignore
      Object.keys(req.query).length ? validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions) : null
      validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions),
      validateOrReject(Object.assign(new Validators.Body(), req.body), validatorOptions)
      validateOrReject(Object.assign(new Validators.MultiForm(), req.body), validatorOptions)
      validateOrReject(Object.assign(new Validators.UserInfo(), req.body), validatorOptions)
  get: ({ params }) => ({ status: 200, body: { id: params.userId, name: 'bbb' } })
  get: async () => ({ status: 200, body: [{ id: 1, name: 'aa' }] }),
  ArrayNotEmpty
import express, { Express, RequestHandler, Request } from 'express'
      Object.keys(req.query).length ? validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions) : null
      validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions),
      validateOrReject(Object.assign(new Validators.Body(), req.body), validatorOptions)
      validateOrReject(Object.assign(new Validators.UserInfo(), req.body), validatorOptions)
import { validateOrReject, ValidatorOptions } from 'class-validator'
// prettier-ignore
      Object.keys(req.query).length ? validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions) : null
      validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions),
      validateOrReject(Object.assign(new Validators.Body(), req.body), validatorOptions)
      validateOrReject(Object.assign(new Validators.MultiForm(), req.body), validatorOptions)
      validateOrReject(Object.assign(new Validators.UserInfo(), req.body), validatorOptions)
    text: addPrettierIgnore(`/* eslint-disable */${hasMulter ? "\nimport path from 'path'" : ''}
      hasValidator ? "\nimport { validateOrReject, ValidatorOptions } from 'class-validator'" : ''
    }${
${hasValidator ? '  validator?: ValidatorOptions\n' : ''}${
    ? '  const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }\n'
        }validateOrReject(Object.assign(new Validators.${checker.typeToString(v.type)}(), req.${
        }), validatorOptions)${v.hasQuestion ? ' : null' : ''}`

Fixed code:
test('POST: nested validation', async () => {
  const res1 = await client.users.post({
    body: {
      id: 123,
      name: 'foo',
      location: {
        country: 'JP',
        stateProvince: 'Tokyo'
      }
    }
  })
  expect(res1.status).toBe(204)

  // Note that extraneous properties are allowed by default
  const res2 = await client.users.post({
    body: {
      id: 123,
      name: 'foo',
      location: {
        country: 'JP',
        stateProvince: 'Tokyo',
        extra1: {
          extra1a: 'bar',
          extra1b: 'baz'
        }
      },
      extra2: 'qux'
    } as any
  })
  expect(res2.status).toBe(204)
})

test('POST: 400 (nested validation)', async () => {
  // id is not a number
  await expect(
    client.users.post({
      body: {
        id: '123',
        name: 'foo',
        location: {
          country: 'JP',
          stateProvince: 'Tokyo'
        }
      } as any
    })
  ).rejects.toHaveProperty('response.status', 400)

  // location is missing
  await expect(
    client.users.post({
      body: { id: 123, name: 'foo' } as any
    })
  ).rejects.toHaveProperty('response.status', 400)

  // country is not a valid 2-letter country code
  await expect(
    client.users.post({
      body: {
        id: 123,
        name: 'foo',
        location: {
          country: 'XX',
          stateProvince: 'Tokyo'
        }
      } as any
    })
  ).rejects.toHaveProperty('response.status', 400)

  // stateProvince is not a string
  await expect(
    client.users.post({
      body: {
        id: 123,
        name: 'foo',
        location: {
          country: 'JP',
          stateProvince: 1234
        }
      } as any
    })
  ).rejects.toHaveProperty('response.status', 400)
})

    "class-transformer": "^0.5.1",
    "reflect-metadata": "^0.1.13",
import 'reflect-metadata'
// prettier-ignore
import { ClassTransformOptions, plainToInstance } from 'class-transformer'
// prettier-ignore
import { validateOrReject, ValidatorOptions } from 'class-validator'
// prettier-ignore
  transformer?: ClassTransformOptions
  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
      Object.keys(req.query).length ? validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions) : null
      validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions),
      validateOrReject(plainToInstance(Validators.Body, req.body, transformerOptions), validatorOptions)
      validateOrReject(plainToInstance(Validators.MultiForm, req.body, transformerOptions), validatorOptions)
      validateOrReject(plainToInstance(Validators.UserInfo, req.body, transformerOptions), validatorOptions)
  get: ({ params }) => ({
    status: 200,
    body: {
      id: params.userId,
      name: 'bbb',
      location: {
        country: 'JP',
        stateProvince: 'Tokyo'
      }
    }
  })
  get: async () => ({
    status: 200,
    body: [
      {
        id: 1,
        name: 'aa',
        location: {
          country: 'JP',
          stateProvince: 'Tokyo'
        }
      }
    ]
  }),
import { Type } from 'class-transformer'
  ArrayNotEmpty,
  IsISO31661Alpha2,
  ValidateNested,
  IsObject
export class UserInfoLocation {
  @IsISO31661Alpha2()
  country: string

  @IsString()
  stateProvince: string
}


  // @Type decorator is required to validate nested object properly
  // @IsObject decorator is required or class-validator will not throw an error when the property is missing
  @ValidateNested()
  @IsObject()
  @Type(() => UserInfoLocation)
  location: UserInfoLocation
import 'reflect-metadata'
// prettier-ignore
import { ClassTransformOptions, plainToInstance } from 'class-transformer'
import express, { Express, RequestHandler, Request } from 'express'
// prettier-ignore
  transformer?: ClassTransformOptions
  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
      Object.keys(req.query).length ? validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions) : null
      validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions),
      validateOrReject(plainToInstance(Validators.Body, req.body, transformerOptions), validatorOptions)
      validateOrReject(plainToInstance(Validators.UserInfo, req.body, transformerOptions), validatorOptions)
import 'reflect-metadata'
// prettier-ignore
import { ClassTransformOptions, plainToInstance } from 'class-transformer'
// prettier-ignore
import { validateOrReject, ValidatorOptions } from 'class-validator'
// prettier-ignore
  transformer?: ClassTransformOptions
  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
      Object.keys(req.query).length ? validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions) : null
      validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions),
      validateOrReject(plainToInstance(Validators.Body, req.body, transformerOptions), validatorOptions)
      validateOrReject(plainToInstance(Validators.MultiForm, req.body, transformerOptions), validatorOptions)
      validateOrReject(plainToInstance(Validators.UserInfo, req.body, transformerOptions), validatorOptions)
import checkRequisites from './checkRequisites'
  checkRequisites({ hasValidator })

    text: addPrettierIgnore(`/* eslint-disable */${
      hasValidator
        ? "\nimport 'reflect-metadata'" +
          "\nimport { ClassTransformOptions, plainToInstance } from 'class-transformer'" +
          "\nimport { validateOrReject, ValidatorOptions } from 'class-validator'"
        : ''
    }${hasMulter ? "\nimport path from 'path'" : ''}
${hasValidator ? '  transformer?: ClassTransformOptions\n  validator?: ValidatorOptions\n' : ''}${
    ? '  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }\n' +
      '  const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }\n'
const shouldRequirePacakge = (name: string) => {
  try {
    require.resolve(name)
  } catch (e: unknown) {
    if (e instanceof Error && (e as any).code === 'MODULE_NOT_FOUND') {
      console.warn(
        `[WARN] Package "${name}" is necessary but not importable. Did you forget to install?`
      )
    } else {
      throw e
    }
  }
}

export default ({ hasValidator }: { hasValidator: boolean }): void => {
  if (hasValidator) {
    shouldRequirePacakge('class-validator')
    shouldRequirePacakge('class-transformer')
    shouldRequirePacakge('reflect-metadata')
  }
}
        }validateOrReject(plainToInstance(Validators.${checker.typeToString(v.type)}, req.${
        }, transformerOptions), validatorOptions)${v.hasQuestion ? ' : null' : ''}`
class-transformer@^0.5.1:
  version "0.5.1"
  resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336"
  integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==

reflect-metadata@^0.1.13:
  version "0.1.13"
  resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
  integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==