VYPR
High severityNVD Advisory· Published Feb 7, 2022· Updated Apr 23, 2025

Validation bypass in frourio

CVE-2022-23623

Description

Frourio prior to v0.26.0 fails to properly validate nested request bodies and queries when using class-validator, allowing bypassed input validation.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Frourio prior to v0.26.0 fails to properly validate nested request bodies and queries when using class-validator, allowing bypassed input validation.

Vulnerability

Frourio versions prior to v0.26.0 contain an input validation vulnerability in the integration with class-validator through the validators/ folder [1][3]. When users define custom validator classes, nested validators do not work correctly for request bodies and queries in specific situations. Additionally, some inputs are not validated at all (false negatives) [3]. The issue stems from the missing plainToInstance transformation that was added in the fix commit [2]; without it, class-validator does not properly instantiate nested validation classes from plain request data.

Exploitation

An attacker can send a crafted HTTP request to a Frourio application that uses validators/ with class-validator. The request may contain nested objects or specific input values that bypass validation logic [3]. The attacker does not require authentication if the vulnerable endpoint is publicly accessible. The specific conditions where validation fails are not detailed further, but the advisory indicates that both request bodies and query parameters can be affected [3].

Impact

Successful exploitation leads to unvalidated input being accepted by the application, potentially allowing malformed or malicious data to reach business logic [1][3]. The impact depends on what the application does with the unvalidated data; this could include data corruption, unexpected behavior, or security bypass (e.g., injection attacks if the input is used unsafely). The CIA impact is context-dependent, but the core issue is a failure of input validation integrity.

Mitigation

Update frourio to v0.26.0 or later [1][3]. Additionally, projects using frourio must install the class-transformer and reflect-metadata packages as dependencies [1][3]. The patch commit shows the addition of plainToInstance from class-transformer to properly transform plain request objects into validator class instances [2]. A workaround is to manually validate objects from the request using class-transformer in controllers, or to avoid using the validators/ feature entirely until upgrading [3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
frourionpm
< 0.26.00.26.0

Affected products

2

Patches

1
7c19ac536330

feat(validation): use class-transformer to support validation of nested objects

https://github.com/frouriojs/frourioSegaraRaiJan 31, 2022via ghsa
11 files changed · +185 24
  • package.json+2 0 modified
    @@ -95,6 +95,7 @@
         "@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",
    @@ -109,6 +110,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",
    
  • servers/all/$server.ts+12 6 modified
    @@ -1,9 +1,13 @@
     /* 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'
    @@ -43,6 +47,7 @@ import type { FastifyInstance, RouteHandlerMethod, preValidationHookHandler, Fas
     // prettier-ignore
     export type FrourioOptions = {
       basePath?: string
    +  transformer?: ClassTransformOptions
       validator?: ValidatorOptions
       multipart?: FastifyMultipartAttactFieldsToBodyOptions
     }
    @@ -260,6 +265,7 @@ 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)
    @@ -292,7 +298,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
             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
             ])
           ]
         },
    @@ -310,8 +316,8 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
             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)
             ])
           ]
         },
    @@ -344,7 +350,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
           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)
             ])
           ]
         },
    @@ -404,7 +410,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
           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,
    
  • servers/all/api/users/controller.ts+13 1 modified
    @@ -16,6 +16,18 @@ 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 })
     }))
    
  • servers/all/api/users/_userId@number/controller.ts+11 1 modified
    @@ -5,5 +5,15 @@ export type AdditionalRequest = {
     }
     
     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'
    +      }
    +    }
    +  })
     }))
    
  • servers/all/validators/index.ts+20 1 modified
    @@ -1,3 +1,4 @@
    +import { Type } from 'class-transformer'
     import {
       IsNumberString,
       IsBooleanString,
    @@ -8,7 +9,10 @@ import {
       IsString,
       Allow,
       IsOptional,
    -  ArrayNotEmpty
    +  ArrayNotEmpty,
    +  IsISO31661Alpha2,
    +  ValidateNested,
    +  IsObject
     } from 'class-validator'
     import type { ReadStream } from 'fs'
     
    @@ -52,12 +56,27 @@ export class Body {
       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 {
    
  • servers/noMulter/$server.ts+10 4 modified
    @@ -1,5 +1,9 @@
     /* 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'
    @@ -27,6 +31,7 @@ import type { FastifyInstance, RouteHandlerMethod, preValidationHookHandler, Fas
     // prettier-ignore
     export type FrourioOptions = {
       basePath?: string
    +  transformer?: ClassTransformOptions
       validator?: ValidatorOptions
     }
     
    @@ -122,6 +127,7 @@ 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)
    @@ -139,7 +145,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
         {
           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)
    @@ -150,8 +156,8 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
         {
           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)
    @@ -203,7 +209,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
         {
           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,
    
  • servers/noTypedParams/$server.ts+12 6 modified
    @@ -1,9 +1,13 @@
     /* 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'
    @@ -31,6 +35,7 @@ import type { FastifyInstance, RouteHandlerMethod, preValidationHookHandler, Fas
     // prettier-ignore
     export type FrourioOptions = {
       basePath?: string
    +  transformer?: ClassTransformOptions
       validator?: ValidatorOptions
       multipart?: FastifyMultipartAttactFieldsToBodyOptions
     }
    @@ -146,6 +151,7 @@ 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)
    @@ -165,7 +171,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
         {
           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)
    @@ -178,8 +184,8 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
           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)
             ])
           ]
         },
    @@ -201,7 +207,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
           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)
             ])
           ]
         },
    @@ -246,7 +252,7 @@ export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
         {
           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,
    
  • src/buildServerFile.ts+10 3 modified
    @@ -29,10 +29,16 @@ export default (input: string, project?: string) => {
     
       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'
    @@ -46,7 +52,7 @@ import type { FastifyInstance, RouteHandlerMethod${
     
     export type FrourioOptions = {
       basePath?: string
    -${hasValidator ? '  validator?: ValidatorOptions\n' : ''}${
    +${hasValidator ? '  transformer?: ClassTransformOptions\n  validator?: ValidatorOptions\n' : ''}${
           hasMultipart ? '  multipart?: FastifyMultipartAttactFieldsToBodyOptions\n' : ''
         }}
     
    @@ -262,7 +268,8 @@ 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}
     ${
    
  • src/createControllersText.ts+2 2 modified
    @@ -482,9 +482,9 @@ ${validateInfo
         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        ])`
    
  • __test__/index.spec.ts+83 0 modified
    @@ -204,6 +204,89 @@ test('POST: 400', async () => {
       ).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'
    
  • yarn.lock+10 0 modified
    @@ -1555,6 +1555,11 @@ cjs-module-lexer@^1.0.0:
       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"
    @@ -4636,6 +4641,11 @@ redent@^3.0.0:
         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"
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.