CVE-2022-23623: Improper Input Validation (CWE-20)
value: Frourio is a full stack framework, for TypeScript. Frourio users who uses frourio 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 or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 8.8
Published: 2022-02-07T23:15Z
Repository: https://github.com/frouriojs/frourio
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/createControllersText.ts, yarn.lock
Commit date: 2022-01-31 19:33:31 +0900
Commit message: feat(validation): use class-transformer to support validation of nested objects

Diff: diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts
index a8d2945..dfcee35 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 364e808..7938177 100644
--- a/package.json
+++ b/package.json
@@ -93,10 +93,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",
@@ -107,10 +108,11 @@
     "fastify": "^3.18.1",
     "fastify-multipart": "^4.0.7",
     "jest": "^27.0.6",
     "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 88abc85..e8cc420 100644
--- a/servers/all/$server.ts
+++ b/servers/all/$server.ts
@@ -1,11 +1,15 @@
 /* eslint-disable */
 // prettier-ignore
-import multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
+import 'reflect-metadata'
+// prettier-ignore
+import { ClassTransformOptions, plainToInstance } from 'class-transformer'
 // prettier-ignore
 import { validateOrReject, ValidatorOptions } from 'class-validator'
 // prettier-ignore
+import multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
+// prettier-ignore
 import * as Validators from './validators'
 // prettier-ignore
 import hooksFn0 from './api/hooks'
 // prettier-ignore
 import hooksFn1 from './api/empty/hooks'
@@ -41,10 +45,11 @@ import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams }
 import type { FastifyInstance, RouteHandlerMethod, preValidationHookHandler, FastifyRequest, RouteShorthandOptions } from 'fastify'
 
 // prettier-ignore
 export type FrourioOptions = {
   basePath?: string
+  transformer?: ClassTransformOptions
   validator?: ValidatorOptions
   multipart?: FastifyMultipartAttactFieldsToBodyOptions
 }
 
 // prettier-ignore
@@ -258,10 +263,11 @@ const asyncMethodToHandler = (
 }
 
 // prettier-ignore
 export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
   const basePath = options.basePath ?? ''
+  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
   const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }
   const hooks0 = hooksFn0(fastify)
   const hooks1 = hooksFn1(fastify)
   const hooks2 = hooksFn2(fastify)
   const hooks3 = hooksFn3(fastify)
@@ -290,11 +296,11 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
       preValidation: [
         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]])),
         normalizeQuery,
         createValidateHandler(req => [
-          Object.keys(req.query as any).length ? validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions) : null
+          Object.keys(req.query as any).length ? validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions) : null
         ])
       ]
     },
     asyncMethodToHandler(controller0.get)
   )
@@ -308,12 +314,12 @@ export default (fastify: FastifyInstance, 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]]),
         formatMultipartData([]),
         normalizeQuery,
         createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions),
-          validateOrReject(Object.assign(new Validators.Body(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions),
+          validateOrReject(plainToInstance(Validators.Body, req.body as any, transformerOptions), validatorOptions)
         ])
       ]
     },
     methodToHandler(controller0.post)
   )
@@ -342,11 +348,11 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
       onRequest: hooks0.onRequest,
       preParsing: hooks0.preParsing,
       preValidation: [
         formatMultipartData([['requiredArr', false], ['optionalArr', true], ['empty', true], ['vals', false], ['files', false]]),
         createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.MultiForm(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.MultiForm, req.body as any, transformerOptions), validatorOptions)
         ])
       ]
     },
     methodToHandler(controller3.post)
   )
@@ -402,11 +408,11 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
     `${basePath}/users`,
     {
       onRequest: [...hooks0.onRequest, hooks2.onRequest],
       preParsing: hooks0.preParsing,
       preValidation: createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.UserInfo(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.UserInfo, req.body as any, transformerOptions), validatorOptions)
         ]),
       preHandler: ctrlHooks1.preHandler
     } as RouteShorthandOptions,
     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 31aef00..040c344 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 a0e2bd9..ccfb92c 100644
--- a/servers/noMulter/$server.ts
+++ b/servers/noMulter/$server.ts
@@ -1,7 +1,11 @@
 /* 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 * as Validators from './validators'
 // prettier-ignore
 import hooksFn0 from './api/hooks'
@@ -25,10 +29,11 @@ import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams }
 import type { FastifyInstance, RouteHandlerMethod, preValidationHookHandler, FastifyRequest, RouteShorthandOptions } from 'fastify'
 
 // 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
@@ -120,10 +125,11 @@ const asyncMethodToHandler = (
 }
 
 // prettier-ignore
 export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
   const basePath = options.basePath ?? ''
+  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
   const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }
   const hooks0 = hooksFn0(fastify)
   const hooks1 = hooksFn1(fastify)
   const ctrlHooks0 = ctrlHooksFn0(fastify)
   const ctrlHooks1 = ctrlHooksFn1(fastify)
@@ -137,23 +143,23 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
   fastify.get(
     basePath || '/',
     {
       onRequest: [hooks0.onRequest, ctrlHooks0.onRequest],
       preValidation: createValidateHandler(req => [
-          Object.keys(req.query as any).length ? validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions) : null
+          Object.keys(req.query as any).length ? validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions) : null
         ])
     },
     asyncMethodToHandler(controller0.get)
   )
 
   fastify.post(
     basePath || '/',
     {
       onRequest: [hooks0.onRequest, ctrlHooks0.onRequest],
       preValidation: createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions),
-          validateOrReject(Object.assign(new Validators.Body(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions),
+          validateOrReject(plainToInstance(Validators.Body, req.body as any, transformerOptions), validatorOptions)
         ])
     },
     methodToHandler(controller0.post)
   )
 
@@ -201,11 +207,11 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
   fastify.post(
     `${basePath}/users`,
     {
       onRequest: [hooks0.onRequest, hooks1.onRequest],
       preValidation: createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.UserInfo(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.UserInfo, req.body as any, transformerOptions), validatorOptions)
         ]),
       preHandler: ctrlHooks1.preHandler
     } as RouteShorthandOptions,
     methodToHandler(controller4.post)
   )
diff --git a/servers/noTypedParams/$server.ts b/servers/noTypedParams/$server.ts
index 0e1a3f9..f3eb808 100644
--- a/servers/noTypedParams/$server.ts
+++ b/servers/noTypedParams/$server.ts
@@ -1,11 +1,15 @@
 /* eslint-disable */
 // prettier-ignore
-import multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
+import 'reflect-metadata'
+// prettier-ignore
+import { ClassTransformOptions, plainToInstance } from 'class-transformer'
 // prettier-ignore
 import { validateOrReject, ValidatorOptions } from 'class-validator'
 // prettier-ignore
+import multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
+// prettier-ignore
 import * as Validators from './validators'
 // prettier-ignore
 import hooksFn0 from './api/hooks'
 // prettier-ignore
 import hooksFn1 from './api/users/hooks'
@@ -29,10 +33,11 @@ import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams }
 import type { FastifyInstance, RouteHandlerMethod, preValidationHookHandler, FastifyRequest, RouteShorthandOptions } from 'fastify'
 
 // prettier-ignore
 export type FrourioOptions = {
   basePath?: string
+  transformer?: ClassTransformOptions
   validator?: ValidatorOptions
   multipart?: FastifyMultipartAttactFieldsToBodyOptions
 }
 
 // prettier-ignore
@@ -144,10 +149,11 @@ const asyncMethodToHandler = (
 }
 
 // prettier-ignore
 export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
   const basePath = options.basePath ?? ''
+  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
   const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }
   const hooks0 = hooksFn0(fastify)
   const hooks1 = hooksFn1(fastify)
   const ctrlHooks0 = ctrlHooksFn0(fastify)
   const ctrlHooks1 = ctrlHooksFn1(fastify)
@@ -163,11 +169,11 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
   fastify.get(
     basePath || '/',
     {
       onRequest: [hooks0.onRequest, ctrlHooks0.onRequest],
       preValidation: createValidateHandler(req => [
-          Object.keys(req.query as any).length ? validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions) : null
+          Object.keys(req.query as any).length ? validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions) : null
         ])
     },
     asyncMethodToHandler(controller0.get)
   )
 
@@ -176,12 +182,12 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
     {
       onRequest: [hooks0.onRequest, ctrlHooks0.onRequest],
       preValidation: [
         formatMultipartData([]),
         createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions),
-          validateOrReject(Object.assign(new Validators.Body(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions),
+          validateOrReject(plainToInstance(Validators.Body, req.body as any, transformerOptions), validatorOptions)
         ])
       ]
     },
     methodToHandler(controller0.post)
   )
@@ -199,11 +205,11 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
     {
       onRequest: hooks0.onRequest,
       preValidation: [
         formatMultipartData([['empty', false], ['vals', false], ['files', false]]),
         createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.MultiForm(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.MultiForm, req.body as any, transformerOptions), validatorOptions)
         ])
       ]
     },
     methodToHandler(controller2.post)
   )
@@ -244,11 +250,11 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
   fastify.post(
     `${basePath}/users`,
     {
       onRequest: [hooks0.onRequest, hooks1.onRequest],
       preValidation: createValidateHandler(req => [
-          validateOrReject(Object.assign(new Validators.UserInfo(), req.body as any), validatorOptions)
+          validateOrReject(plainToInstance(Validators.UserInfo, req.body as any, transformerOptions), validatorOptions)
         ]),
       preHandler: ctrlHooks1.preHandler
     } as RouteShorthandOptions,
     methodToHandler(controller5.post)
   )
diff --git a/src/buildServerFile.ts b/src/buildServerFile.ts
index f638d0c..d358d29 100644
--- a/src/buildServerFile.ts
+++ b/src/buildServerFile.ts
@@ -27,14 +27,20 @@ export default (input: string, project?: string) => {
   const hasAsyncMethodToHandler = controllers.includes(' asyncMethodToHandler(')
   const hasRouteShorthandOptions = controllers.includes(' as RouteShorthandOptions,')
 
   return {
     text: addPrettierIgnore(`/* eslint-disable */${
+      hasValidator
+        ? "\nimport 'reflect-metadata'" +
+          "\nimport { ClassTransformOptions, plainToInstance } from 'class-transformer'" +
+          "\nimport { validateOrReject, ValidatorOptions } from 'class-validator'"
+        : ''
+    }${
       hasMultipart
         ? "\nimport multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'"
         : ''
-    }${hasValidator ? "\nimport { validateOrReject, ValidatorOptions } from 'class-validator'" : ''}
+    }
 ${hasValidator ? "import * as Validators from './validators'\n" : ''}${imports}${
       hasMultipart ? "import type { ReadStream } from 'fs'\n" : ''
     }import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } from 'aspida'
 import type { FastifyInstance, RouteHandlerMethod${
       hasNumberTypeQuery || hasBooleanTypeQuery || hasTypedParams || hasValidator || hasMultipart
@@ -44,11 +50,11 @@ import type { FastifyInstance, RouteHandlerMethod${
       hasRouteShorthandOptions ? ', RouteShorthandOptions' : ''
     } } from 'fastify'
 
 export type FrourioOptions = {
   basePath?: string
-${hasValidator ? '  validator?: ValidatorOptions\n' : ''}${
+${hasValidator ? '  transformer?: ClassTransformOptions\n  validator?: ValidatorOptions\n' : ''}${
       hasMultipart ? '  multipart?: FastifyMultipartAttactFieldsToBodyOptions\n' : ''
     }}
 
 type HttpStatusNoOk = 301 | 302 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 409 | 500 | 501 | 502 | 503 | 504 | 505
 
@@ -260,11 +266,12 @@ const formatMultipartData = (arrayTypeKeys: [string, boolean][]): preValidationH
     }
 export default (fastify: FastifyInstance, 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}
 ${
   hasMultipart
     ? '  fastify.register(multipart, { attachFieldsToBody: true, limits: { fileSize: 1024 ** 3 }, ...options.multipart })\n\n'
diff --git a/src/createControllersText.ts b/src/createControllersText.ts
index 2588053..3dae9ef 100644
--- a/src/createControllersText.ts
+++ b/src/createControllersText.ts
@@ -480,13 +480,13 @@ export default (appDir: string, project: string) => {
 ${validateInfo
   .map(v =>
     v.type
       ? `          ${
           v.hasQuestion ? `Object.keys(req.${v.name} as any).length ? ` : ''
-        }validateOrReject(Object.assign(new Validators.${checker.typeToString(v.type)}(), req.${
+        }validateOrReject(plainToInstance(Validators.${checker.typeToString(v.type)}, req.${
           v.name
-        } as any), validatorOptions)${v.hasQuestion ? ' : null' : ''}`
+        } as any, transformerOptions), validatorOptions)${v.hasQuestion ? ' : null' : ''}`
       : ''
   )
   .join(',\n')}\n        ])`
                         : '',
                       dirPath.includes('@number')
diff --git a/yarn.lock b/yarn.lock
index 1c3a25a..a8edb65 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1553,10 +1553,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:
@@ -4634,10 +4639,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 multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
          Object.keys(req.query as any).length ? validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions) : null
          validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions),
          validateOrReject(Object.assign(new Validators.Body(), req.body as any), validatorOptions)
          validateOrReject(Object.assign(new Validators.MultiForm(), req.body as any), validatorOptions)
          validateOrReject(Object.assign(new Validators.UserInfo(), req.body as any), validatorOptions)
  get: ({ params }) => ({ status: 200, body: { id: params.userId, name: 'bbb' } })
  get: async () => ({ status: 200, body: [{ id: 1, name: 'aa' }] }),
  ArrayNotEmpty
          Object.keys(req.query as any).length ? validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions) : null
          validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions),
          validateOrReject(Object.assign(new Validators.Body(), req.body as any), validatorOptions)
          validateOrReject(Object.assign(new Validators.UserInfo(), req.body as any), validatorOptions)
import multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
          Object.keys(req.query as any).length ? validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions) : null
          validateOrReject(Object.assign(new Validators.Query(), req.query as any), validatorOptions),
          validateOrReject(Object.assign(new Validators.Body(), req.body as any), validatorOptions)
          validateOrReject(Object.assign(new Validators.MultiForm(), req.body as any), validatorOptions)
          validateOrReject(Object.assign(new Validators.UserInfo(), req.body as any), validatorOptions)
    }${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.${
        } as any), 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'
import multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
// prettier-ignore
  transformer?: ClassTransformOptions
  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
          Object.keys(req.query as any).length ? validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions) : null
          validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions),
          validateOrReject(plainToInstance(Validators.Body, req.body as any, transformerOptions), validatorOptions)
          validateOrReject(plainToInstance(Validators.MultiForm, req.body as any, transformerOptions), validatorOptions)
          validateOrReject(plainToInstance(Validators.UserInfo, req.body as any, 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'
// prettier-ignore
  transformer?: ClassTransformOptions
  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
          Object.keys(req.query as any).length ? validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions) : null
          validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions),
          validateOrReject(plainToInstance(Validators.Body, req.body as any, transformerOptions), validatorOptions)
          validateOrReject(plainToInstance(Validators.UserInfo, req.body as any, transformerOptions), validatorOptions)
import 'reflect-metadata'
// prettier-ignore
import { ClassTransformOptions, plainToInstance } from 'class-transformer'
import multipart, { FastifyMultipartAttactFieldsToBodyOptions, Multipart } from 'fastify-multipart'
// prettier-ignore
  transformer?: ClassTransformOptions
  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }
          Object.keys(req.query as any).length ? validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions) : null
          validateOrReject(plainToInstance(Validators.Query, req.query as any, transformerOptions), validatorOptions),
          validateOrReject(plainToInstance(Validators.Body, req.body as any, transformerOptions), validatorOptions)
          validateOrReject(plainToInstance(Validators.MultiForm, req.body as any, transformerOptions), validatorOptions)
          validateOrReject(plainToInstance(Validators.UserInfo, req.body as any, transformerOptions), validatorOptions)
      hasValidator
        ? "\nimport 'reflect-metadata'" +
          "\nimport { ClassTransformOptions, plainToInstance } from 'class-transformer'" +
          "\nimport { validateOrReject, ValidatorOptions } from 'class-validator'"
        : ''
    }${
    }
${hasValidator ? '  transformer?: ClassTransformOptions\n  validator?: ValidatorOptions\n' : ''}${
    ? '  const transformerOptions: ClassTransformOptions = { enableCircularCheck: true, ...options.transformer }\n' +
      '  const validatorOptions: ValidatorOptions = { validationError: { target: false }, ...options.validator }\n'
        }validateOrReject(plainToInstance(Validators.${checker.typeToString(v.type)}, req.${
        } as any, 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==