Validation bypass in frourio
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.
| Package | Affected versions | Patched versions |
|---|---|---|
frourionpm | < 0.26.0 | 0.26.0 |
Affected products
2Patches
17c19ac536330feat(validation): use class-transformer to support validation of nested objects
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- github.com/advisories/GHSA-8xxm-h73r-ghfjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-23623ghsaADVISORY
- github.com/frouriojs/frourio/commit/7c19ac5363305b81b1c6b5232620228763d427afghsax_refsource_MISCWEB
- github.com/frouriojs/frourio/security/advisories/GHSA-8xxm-h73r-ghfjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.