CVE-2022-23624: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
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`.
CWE description: 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.
CVSS v3 base score: 8.8
Published: 2022-02-07T23:15Z
Repository: https://github.com/frouriojs/frourio-express
Language: TypeScript
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 date: 2022-02-04 17:58:54 +0900
Commit message: feat(validation): use class-transformer to support validation of nested objects

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

Diff: diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts
index 4c38c80..d28a9c1 100644
--- a/__test__/index.spec.ts
+++ b/__test__/index.spec.ts
@@ -202,10 +202,93 @@ test('POST: 400', async () => {
       headers: form.getHeaders()
     })
   ).rejects.toHaveProperty('response.status', 400)
 })
 
+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)
+})
+
 test('controller dependency injection', async () => {
   let val = 0
   const id = '5'
   const injectedController = controller
     .inject({
diff --git a/package.json b/package.json
index 0ab5f02..94c2aac 100644
--- a/package.json
+++ b/package.json
@@ -95,10 +95,11 @@
     "@types/node-fetch": "^2.5.10",
     "@types/rimraf": "^3.0.0",
     "@typescript-eslint/eslint-plugin": "^4.28.1",
     "@typescript-eslint/parser": "^4.28.1",
     "axios": "^0.21.1",
+    "class-transformer": "^0.5.1",
     "class-validator": "^0.13.1",
     "eslint": "^7.30.0",
     "eslint-config-prettier": "^8.3.0",
     "eslint-config-standard": "^16.0.3",
     "eslint-plugin-import": "^2.23.4",
@@ -109,10 +110,11 @@
     "express": "^4.17.1",
     "jest": "^27.0.6",
     "multer": "^1.4.2",
     "node-fetch": "^2.6.1",
     "prettier": "^2.3.2",
+    "reflect-metadata": "^0.1.13",
     "rimraf": "^3.0.2",
     "standard-version": "^9.3.0",
     "ts-jest": "^27.0.3",
     "ts-node": "^10.0.0",
     "typescript": "^4.3.5"
diff --git a/servers/all/$server.ts b/servers/all/$server.ts
index cbd4b70..782bf50 100644
--- a/servers/all/$server.ts
+++ b/servers/all/$server.ts
@@ -1,15 +1,19 @@
 /* eslint-disable */
 // prettier-ignore
+import 'reflect-metadata'
+// prettier-ignore
+import { ClassTransformOptions, plainToInstance } from 'class-transformer'
+// prettier-ignore
+import { validateOrReject, ValidatorOptions } from 'class-validator'
+// prettier-ignore
 import path from 'path'
 // prettier-ignore
 import express, { Express, RequestHandler, Request } from 'express'
 // prettier-ignore
 import multer, { Options } from 'multer'
 // prettier-ignore
-import { validateOrReject, ValidatorOptions } from 'class-validator'
-// prettier-ignore
 import fastJson, { Schema } from 'fast-json-stringify'
 // prettier-ignore
 import * as Validators from './validators'
 // prettier-ignore
 import hooksFn0 from './api/hooks'
@@ -45,10 +49,11 @@ import type { ReadStream } from 'fs'
 import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } from 'aspida'
 
 // prettier-ignore
 export type FrourioOptions = {
   basePath?: string
+  transformer?: ClassTransformOptions
   validator?: ValidatorOptions
   multer?: Options
 }
 
 // prettier-ignore
@@ -303,10 +308,11 @@ const asyncMethodToHandlerWithSchema = (
 }
 
 // prettier-ignore
 export default (app: Express, options: FrourioOptions = {}) => {
   const basePath = options.basePath ?? ''
+  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
   const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }
   const hooks0 = hooksFn0(app)
   const hooks1 = hooksFn1(app)
   const hooks2 = hooksFn2(app)
   const hooks3 = hooksFn3(app)
@@ -330,11 +336,11 @@ export default (app: Express, options: FrourioOptions = {}) => {
     ctrlHooks0.onRequest,
     hooks0.preParsing,
     callParserIfExistsQuery(parseNumberTypeQueryParams([['requiredNum', false, false], ['optionalNum', true, false], ['optionalNumArr', true, true], ['emptyNum', true, false], ['requiredNumArr', false, true]])),
     callParserIfExistsQuery(parseBooleanTypeQueryParams([['bool', false, false], ['optionalBool', true, false], ['boolArray', false, true], ['optionalBoolArray', true, true]])),
     createValidateHandler(req => [
-      Object.keys(req.query).length ? validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions) : null
+      Object.keys(req.query).length ? validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions) : null
     ]),
     asyncMethodToHandlerWithSchema(controller0.get, responseSchema0.get)
   ])
 
   app.post(`${basePath}/`, [
@@ -344,12 +350,12 @@ export default (app: Express, options: FrourioOptions = {}) => {
     parseNumberTypeQueryParams([['requiredNum', false, false], ['optionalNum', true, false], ['optionalNumArr', true, true], ['emptyNum', true, false], ['requiredNumArr', false, true]]),
     parseBooleanTypeQueryParams([['bool', false, false], ['optionalBool', true, false], ['boolArray', false, true], ['optionalBoolArray', true, true]]),
     uploader,
     formatMulterData([]),
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions),
-      validateOrReject(Object.assign(new Validators.Body(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions),
+      validateOrReject(plainToInstance(Validators.Body, req.body, transformerOptions), validatorOptions)
     ]),
     methodToHandler(controller0.post)
   ])
 
   app.get(`${basePath}/500`, [
@@ -370,11 +376,11 @@ export default (app: Express, options: FrourioOptions = {}) => {
     ...hooks0.onRequest,
     hooks0.preParsing,
     uploader,
     formatMulterData([['requiredArr', false], ['optionalArr', true], ['empty', true], ['vals', false], ['files', false]]),
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.MultiForm(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.MultiForm, req.body, transformerOptions), validatorOptions)
     ]),
     methodToHandler(controller3.post)
   ])
 
   app.get(`${basePath}/texts`, [
@@ -415,11 +421,11 @@ export default (app: Express, options: FrourioOptions = {}) => {
     ...hooks0.onRequest,
     hooks2.onRequest,
     hooks0.preParsing,
     parseJSONBoby,
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.UserInfo(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.UserInfo, req.body, transformerOptions), validatorOptions)
     ]),
     ...ctrlHooks1.preHandler,
     methodToHandler(controller7.post)
   ])
 
diff --git a/servers/all/api/users/_userId@number/controller.ts b/servers/all/api/users/_userId@number/controller.ts
index c6acbb2..5889807 100644
--- a/servers/all/api/users/_userId@number/controller.ts
+++ b/servers/all/api/users/_userId@number/controller.ts
@@ -3,7 +3,17 @@ import { defineController } from './$relay'
 export type AdditionalRequest = {
   name: string
 }
 
 export default defineController(() => ({
-  get: ({ params }) => ({ status: 200, body: { id: params.userId, name: 'bbb' } })
+  get: ({ params }) => ({
+    status: 200,
+    body: {
+      id: params.userId,
+      name: 'bbb',
+      location: {
+        country: 'JP',
+        stateProvince: 'Tokyo'
+      }
+    }
+  })
 }))
diff --git a/servers/all/api/users/controller.ts b/servers/all/api/users/controller.ts
index 0cad61d..acef73b 100644
--- a/servers/all/api/users/controller.ts
+++ b/servers/all/api/users/controller.ts
@@ -14,8 +14,20 @@ const hooks = defineHooks(() => ({
 }))
 
 export { hooks, AdditionalRequest }
 
 export default defineController(() => ({
-  get: async () => ({ status: 200, body: [{ id: 1, name: 'aa' }] }),
+  get: async () => ({
+    status: 200,
+    body: [
+      {
+        id: 1,
+        name: 'aa',
+        location: {
+          country: 'JP',
+          stateProvince: 'Tokyo'
+        }
+      }
+    ]
+  }),
   post: () => ({ status: 204 })
 }))
diff --git a/servers/all/validators/index.ts b/servers/all/validators/index.ts
index 2397f34..68e5116 100644
--- a/servers/all/validators/index.ts
+++ b/servers/all/validators/index.ts
@@ -1,16 +1,20 @@
+import { Type } from 'class-transformer'
 import {
   IsNumberString,
   IsBooleanString,
   IsBoolean,
   IsPort,
   IsInt,
   MaxLength,
   IsString,
   Allow,
   IsOptional,
-  ArrayNotEmpty
+  ArrayNotEmpty,
+  IsISO31661Alpha2,
+  ValidateNested,
+  IsObject
 } from 'class-validator'
 import type { ReadStream } from 'fs'
 
 export class Query {
   requiredNum: number
@@ -50,16 +54,31 @@ export class Body {
   port: string
 
   file: File | ReadStream
 }
 
+export class UserInfoLocation {
+  @IsISO31661Alpha2()
+  country: string
+
+  @IsString()
+  stateProvince: string
+}
+
 export class UserInfo {
   @IsInt()
   id: number
 
   @MaxLength(20)
   name: 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
 }
 
 export class MultiForm {
   requiredArr: string[]
   optionalArr?: string[]
diff --git a/servers/noMulter/$server.ts b/servers/noMulter/$server.ts
index 4cde229..509d6a3 100644
--- a/servers/noMulter/$server.ts
+++ b/servers/noMulter/$server.ts
@@ -1,11 +1,15 @@
 /* eslint-disable */
 // prettier-ignore
-import express, { Express, RequestHandler, Request } from 'express'
+import 'reflect-metadata'
+// prettier-ignore
+import { ClassTransformOptions, plainToInstance } from 'class-transformer'
 // prettier-ignore
 import { validateOrReject, ValidatorOptions } from 'class-validator'
 // prettier-ignore
+import express, { Express, RequestHandler, Request } from 'express'
+// prettier-ignore
 import * as Validators from './validators'
 // prettier-ignore
 import hooksFn0 from './api/hooks'
 // prettier-ignore
 import hooksFn1 from './api/users/hooks'
@@ -25,10 +29,11 @@ import controllerFn5 from './api/users/_userId@number/controller'
 import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } from 'aspida'
 
 // prettier-ignore
 export type FrourioOptions = {
   basePath?: string
+  transformer?: ClassTransformOptions
   validator?: ValidatorOptions
 }
 
 // prettier-ignore
 type HttpStatusNoOk = 301 | 302 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 409 | 500 | 501 | 502 | 503 | 504 | 505
@@ -142,10 +147,11 @@ const asyncMethodToHandler = (
 }
 
 // prettier-ignore
 export default (app: Express, options: FrourioOptions = {}) => {
   const basePath = options.basePath ?? ''
+  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
   const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }
   const hooks0 = hooksFn0(app)
   const hooks1 = hooksFn1(app)
   const ctrlHooks0 = ctrlHooksFn0(app)
   const ctrlHooks1 = ctrlHooksFn1(app)
@@ -158,22 +164,22 @@ export default (app: Express, options: FrourioOptions = {}) => {
 
   app.get(`${basePath}/`, [
     hooks0.onRequest,
     ctrlHooks0.onRequest,
     createValidateHandler(req => [
-      Object.keys(req.query).length ? validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions) : null
+      Object.keys(req.query).length ? validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions) : null
     ]),
     asyncMethodToHandler(controller0.get)
   ])
 
   app.post(`${basePath}/`, [
     hooks0.onRequest,
     ctrlHooks0.onRequest,
     parseJSONBoby,
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions),
-      validateOrReject(Object.assign(new Validators.Body(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions),
+      validateOrReject(plainToInstance(Validators.Body, req.body, transformerOptions), validatorOptions)
     ]),
     methodToHandler(controller0.post)
   ])
 
   app.get(`${basePath}/empty/noEmpty`, [
@@ -207,11 +213,11 @@ export default (app: Express, options: FrourioOptions = {}) => {
   app.post(`${basePath}/users`, [
     hooks0.onRequest,
     hooks1.onRequest,
     parseJSONBoby,
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.UserInfo(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.UserInfo, req.body, transformerOptions), validatorOptions)
     ]),
     ...ctrlHooks1.preHandler,
     methodToHandler(controller4.post)
   ])
 
diff --git a/servers/noTypedParams/$server.ts b/servers/noTypedParams/$server.ts
index f7aff89..6e09091 100644
--- a/servers/noTypedParams/$server.ts
+++ b/servers/noTypedParams/$server.ts
@@ -1,15 +1,19 @@
 /* eslint-disable */
 // prettier-ignore
+import 'reflect-metadata'
+// prettier-ignore
+import { ClassTransformOptions, plainToInstance } from 'class-transformer'
+// prettier-ignore
+import { validateOrReject, ValidatorOptions } from 'class-validator'
+// prettier-ignore
 import path from 'path'
 // prettier-ignore
 import express, { Express, RequestHandler, Request } from 'express'
 // prettier-ignore
 import multer, { Options } from 'multer'
 // prettier-ignore
-import { validateOrReject, ValidatorOptions } from 'class-validator'
-// prettier-ignore
 import * as Validators from './validators'
 // prettier-ignore
 import hooksFn0 from './api/hooks'
 // prettier-ignore
 import hooksFn1 from './api/users/hooks'
@@ -31,10 +35,11 @@ import type { ReadStream } from 'fs'
 import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } from 'aspida'
 
 // prettier-ignore
 export type FrourioOptions = {
   basePath?: string
+  transformer?: ClassTransformOptions
   validator?: ValidatorOptions
   multer?: Options
 }
 
 // prettier-ignore
@@ -172,10 +177,11 @@ const asyncMethodToHandler = (
 }
 
 // prettier-ignore
 export default (app: Express, options: FrourioOptions = {}) => {
   const basePath = options.basePath ?? ''
+  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
   const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }
   const hooks0 = hooksFn0(app)
   const hooks1 = hooksFn1(app)
   const ctrlHooks0 = ctrlHooksFn0(app)
   const ctrlHooks1 = ctrlHooksFn1(app)
@@ -189,23 +195,23 @@ export default (app: Express, options: FrourioOptions = {}) => {
 
   app.get(`${basePath}/`, [
     hooks0.onRequest,
     ctrlHooks0.onRequest,
     createValidateHandler(req => [
-      Object.keys(req.query).length ? validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions) : null
+      Object.keys(req.query).length ? validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions) : null
     ]),
     asyncMethodToHandler(controller0.get)
   ])
 
   app.post(`${basePath}/`, [
     hooks0.onRequest,
     ctrlHooks0.onRequest,
     uploader,
     formatMulterData([]),
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.Query(), req.query), validatorOptions),
-      validateOrReject(Object.assign(new Validators.Body(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.Query, req.query, transformerOptions), validatorOptions),
+      validateOrReject(plainToInstance(Validators.Body, req.body, transformerOptions), validatorOptions)
     ]),
     methodToHandler(controller0.post)
   ])
 
   app.get(`${basePath}/empty/noEmpty`, [
@@ -216,11 +222,11 @@ export default (app: Express, options: FrourioOptions = {}) => {
   app.post(`${basePath}/multiForm`, [
     hooks0.onRequest,
     uploader,
     formatMulterData([['empty', false], ['vals', false], ['files', false]]),
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.MultiForm(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.MultiForm, req.body, transformerOptions), validatorOptions)
     ]),
     methodToHandler(controller2.post)
   ])
 
   app.get(`${basePath}/texts`, [
@@ -249,11 +255,11 @@ export default (app: Express, options: FrourioOptions = {}) => {
   app.post(`${basePath}/users`, [
     hooks0.onRequest,
     hooks1.onRequest,
     parseJSONBoby,
     createValidateHandler(req => [
-      validateOrReject(Object.assign(new Validators.UserInfo(), req.body), validatorOptions)
+      validateOrReject(plainToInstance(Validators.UserInfo, req.body, transformerOptions), validatorOptions)
     ]),
     ...ctrlHooks1.preHandler,
     methodToHandler(controller5.post)
   ])
 
diff --git a/src/buildServerFile.ts b/src/buildServerFile.ts
index cbd9b6d..aefc242 100644
--- a/src/buildServerFile.ts
+++ b/src/buildServerFile.ts
@@ -1,8 +1,9 @@
 import path from 'path'
 import { addPrettierIgnore } from './addPrettierIgnore'
 import createControllersText from './createControllersText'
+import checkRequisites from './checkRequisites'
 
 const genHandlerText = (isAsync: boolean) => `
 const ${isAsync ? 'asyncM' : 'm'}ethodToHandler = (
   methodCallback: ServerMethods<any, any>[LowerHttpMethod]
 ): RequestHandler => ${isAsync ? 'async ' : ''}(req, res, next) => {
@@ -75,28 +76,34 @@ export default (input: string, project?: string) => {
   const hasMethodToHandler = controllers.includes(' methodToHandler(')
   const hasAsyncMethodToHandler = controllers.includes(' asyncMethodToHandler(')
   const hasMethodToHandlerWithSchema = controllers.includes(' methodToHandlerWithSchema(')
   const hasAsyncMethodToHandlerWithSchema = controllers.includes(' asyncMethodToHandlerWithSchema(')
 
+  checkRequisites({ hasValidator })
+
   return {
-    text: addPrettierIgnore(`/* eslint-disable */${hasMulter ? "\nimport path from 'path'" : ''}
+    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'" : ''}
 import ${hasJSONBody ? 'express, ' : ''}{ Express, RequestHandler${
       hasValidator ? ', Request' : ''
     } } from 'express'${hasMulter ? "\nimport multer, { Options } from 'multer'" : ''}${
-      hasValidator ? "\nimport { validateOrReject, ValidatorOptions } from 'class-validator'" : ''
-    }${
       hasMethodToHandlerWithSchema || hasAsyncMethodToHandlerWithSchema
         ? "\nimport fastJson, { Schema } from 'fast-json-stringify'"
         : ''
     }
 ${hasValidator ? `import * as Validators from './validators'\n` : ''}${imports}${
       hasMulter ? "import type { ReadStream } from 'fs'\n" : ''
     }import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } from 'aspida'
 
 export type FrourioOptions = {
   basePath?: string
-${hasValidator ? '  validator?: ValidatorOptions\n' : ''}${
+${hasValidator ? '  transformer?: ClassTransformOptions\n  validator?: ValidatorOptions\n' : ''}${
       hasMulter
         ? `  multer?: Options
 }
 
 export type MulterFile = Express.Multer.File`
@@ -297,11 +304,12 @@ const formatMulterData = (arrayTypeKeys: [string, boolean][]): RequestHandler =>
     }
 export default (app: Express, options: FrourioOptions = {}) => {
   const basePath = options.basePath ?? ''
 ${
   hasValidator
-    ? '  const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }\n'
+    ? '  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }\n' +
+      '  const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }\n'
     : ''
 }${consts}${
       hasMulter
         ? "  const uploader = multer({ dest: path.join(__dirname, '.upload'), limits: { fileSize: 1024 ** 3 }, ...options.multer }).any()\n"
         : ''
diff --git a/src/checkRequisites.ts b/src/checkRequisites.ts
new file mode 100644
index 0000000..71da0ac
--- /dev/null
+++ b/src/checkRequisites.ts
@@ -0,0 +1,21 @@
+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')
+  }
+}
diff --git a/src/createControllersText.ts b/src/createControllersText.ts
index 59b1f75..6d6cc95 100644
--- a/src/createControllersText.ts
+++ b/src/createControllersText.ts
@@ -468,13 +468,13 @@ export default (appDir: string, project: string) => {
 ${validateInfo
   .map(v =>
     v.type
       ? `      ${
           v.hasQuestion ? `Object.keys(req.${v.name}).length ? ` : ''
-        }validateOrReject(Object.assign(new Validators.${checker.typeToString(v.type)}(), req.${
+        }validateOrReject(plainToInstance(Validators.${checker.typeToString(v.type)}, req.${
           v.name
-        }), validatorOptions)${v.hasQuestion ? ' : null' : ''}`
+        }, transformerOptions), validatorOptions)${v.hasQuestion ? ' : null' : ''}`
       : ''
   )
   .join(',\n')}\n    ])`
                   : '',
                 dirPath.includes('@number')
diff --git a/yarn.lock b/yarn.lock
index e6e8db9..f093384 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1680,10 +1680,15 @@ ci-info@^3.1.1:
 cjs-module-lexer@^1.0.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.1.tgz#2fd46d9906a126965aa541345c499aaa18e8cd73"
   integrity sha512-jVamGdJPDeuQilKhvVn1h3knuMOZzr8QDnpk+M9aMlCaMkTDd6fBWPhiDqFvFZ07pL0liqabAiuy8SY4jGHeaw==
 
+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==
+
 class-validator@^0.13.1:
   version "0.13.1"
   resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.1.tgz#381b2001ee6b9e05afd133671fbdf760da7dec67"
   integrity sha512-zWIeYFhUitvAHBwNhDdCRK09hWx+P0HUwFE8US8/CxFpMVzkUK8RJl7yOIE+BVu2lxyPNgeOaFv78tLE47jBIg==
   dependencies:
@@ -4929,10 +4934,15 @@ redent@^3.0.0:
   integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
   dependencies:
     indent-string "^4.0.0"
     strip-indent "^3.0.0"
 
+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==
+
 regexpp@^3.0.0, regexpp@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
   integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
 

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==