Validation bypass in frourio-express
Description
Frourio-express before v0.26.0 with class-validator has input validation bypass for request bodies and queries in specific situations.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Frourio-express before v0.26.0 with class-validator has input validation bypass for request bodies and queries in specific situations.
Vulnerability
Frourio-express versions prior to v0.26.0, when integrated with class-validator via the validators/ folder, exhibit a validation bypass vulnerability. In specific cases, validators do not correctly process request bodies and queries, allowing some input to pass without validation. This occurs due to insufficient transformation of plain objects before validation, which is addressed in the fix that introduces class-transformer usage [1][4].
Exploitation
An attacker can send crafted HTTP requests containing invalid or unexpected input that escapes validation. The attack requires network access to the application but no special authentication or privileges. The specific conditions involve nested objects or certain data types that are not correctly validated [4].
Impact
Successful exploitation may lead to acceptance of malformed or malicious input, potentially causing injection attacks, logic errors, or unintended application behavior. The exact impact depends on how the application uses the unvalidated data, but it could range from data corruption to more severe security breaches [1].
Mitigation
Update to frourio-express v0.26.0 or later. Additionally, install class-transformer and reflect-metadata as dependencies. As a workaround, validate objects manually using class-transformer in controllers, or avoid using validators [4].
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 |
|---|---|---|
frourio-expressnpm | < 0.26.0 | 0.26.0 |
Affected products
2- frouriojs/frourio-expressv5Range: < 0.26.0
Patches
173ded5c6f9f1feat(validation): use class-transformer to support validation of nested objects
12 files changed · +212 −29
package.json+2 −0 modified@@ -97,6 +97,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", @@ -111,6 +112,7 @@ "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",
servers/all/$server.ts+13 −7 modified@@ -1,13 +1,17 @@ /* 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' @@ -47,6 +51,7 @@ import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } // prettier-ignore export type FrourioOptions = { basePath?: string + transformer?: ClassTransformOptions validator?: ValidatorOptions multer?: Options } @@ -305,6 +310,7 @@ 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) @@ -332,7 +338,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -346,8 +352,8 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -372,7 +378,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -417,7 +423,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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)
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+11 −5 modified@@ -1,9 +1,13 @@ /* 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' @@ -27,6 +31,7 @@ import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } // prettier-ignore export type FrourioOptions = { basePath?: string + transformer?: ClassTransformOptions validator?: ValidatorOptions } @@ -144,6 +149,7 @@ 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) @@ -160,7 +166,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -170,8 +176,8 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -209,7 +215,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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)
servers/noTypedParams/$server.ts+13 −7 modified@@ -1,13 +1,17 @@ /* 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' @@ -33,6 +37,7 @@ import type { LowerHttpMethod, AspidaMethods, HttpStatusOk, AspidaMethodParams } // prettier-ignore export type FrourioOptions = { basePath?: string + transformer?: ClassTransformOptions validator?: ValidatorOptions multer?: Options } @@ -174,6 +179,7 @@ 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) @@ -191,7 +197,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -202,8 +208,8 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -218,7 +224,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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) ]) @@ -251,7 +257,7 @@ export default (app: Express, options: FrourioOptions = {}) => { 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)
src/buildServerFile.ts+13 −5 modified@@ -1,6 +1,7 @@ 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 = ( @@ -77,13 +78,19 @@ export default (input: string, project?: string) => { 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'" : '' @@ -94,7 +101,7 @@ ${hasValidator ? `import * as Validators from './validators'\n` : ''}${imports}$ export type FrourioOptions = { basePath?: string -${hasValidator ? ' validator?: ValidatorOptions\n' : ''}${ +${hasValidator ? ' transformer?: ClassTransformOptions\n validator?: ValidatorOptions\n' : ''}${ hasMulter ? ` multer?: Options } @@ -299,7 +306,8 @@ 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
src/checkRequisites.ts+21 −0 added@@ -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') + } +}
src/createControllersText.ts+2 −2 modified@@ -470,9 +470,9 @@ ${validateInfo 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 ])`
__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@@ -1682,6 +1682,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" @@ -4931,6 +4936,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-mmj4-777p-fpq9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-23624ghsaADVISORY
- github.com/frouriojs/frourio-express/commit/73ded5c6f9f1c126c0cb2d05c0505e9e4db142d2ghsax_refsource_MISCWEB
- github.com/frouriojs/frourio-express/security/advisories/GHSA-mmj4-777p-fpq9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.