From aa2549601bf4936013185fbe66bb388a81d65f64 Mon Sep 17 00:00:00 2001 From: alexandroslagios Date: Thu, 15 May 2025 13:41:00 +0300 Subject: [PATCH 1/4] fix: Validate JSON request body for multipart/form-data and application/x-www-form-urlencoded Related to the following issues: https://github.com/openapistack/openapi-backend/issues/94 https://github.com/openapistack/openapi-backend/issues/229 --- src/validation.test.ts | 58 ++++++++++++++++++++++++++++++++++++++++++ src/validation.ts | 49 ++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/validation.test.ts b/src/validation.test.ts index 35bbde56..1d04a516 100644 --- a/src/validation.test.ts +++ b/src/validation.test.ts @@ -473,6 +473,20 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts }, required: ['name'], }; + const petScheduleSchema: OpenAPIV3_1.SchemaObject = { + type: 'object', + additionalProperties: false, + properties: { + title: { + type: 'string', + }, + file: { + type: 'string', + format: 'binary', + }, + }, + required: ['title', 'file'], + }; beforeAll(() => { validator = new OpenAPIValidator({ definition: { @@ -505,6 +519,19 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts }, }, }, + '/pets/schedule': { + post: { + operationId: 'createPetSchedule', + responses: { 200: { description: 'ok' } }, + requestBody: { + content: { + 'multipart/form-data': { + schema: petScheduleSchema, + }, + }, + }, + }, + }, ...circularRefDefinition.paths, }, ...constructorOpts, @@ -625,6 +652,37 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts expect(valid.errors).toBeFalsy(); }); + test('passes validation for PUT /pets with multipart/form-data', async () => { + const valid = validator.validateRequest({ + path: '/pets/schedule', + method: 'post', + body: { + title: 'Garfield Schedule', + }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + expect(valid.errors).toBeFalsy(); + }); + + test('fails validation for PUT /pets with multipart/form-data and missing required field', async () => { + const valid = validator.validateRequest({ + path: '/pets/schedule', + method: 'post', + body: { + ages: [1, 2, 3], + }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + expect(valid.errors).toHaveLength(1); + expect(valid.errors?.[0]?.params?.missingProperty).toBe('title'); + }); + test.each([ ['something'], // string [123], // number diff --git a/src/validation.ts b/src/validation.ts index 2a1a0ba7..6510d9e6 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -564,7 +564,10 @@ export class OpenAPIValidator { OpenAPIV3.RequestBodyObject, OpenAPIV3_1.RequestBodyObject >; - const jsonbody = requestBody.content['application/json']; + const jsonbody = + requestBody.content['application/json'] || + requestBody.content['multipart/form-data'] || + requestBody.content['application/x-www-form-urlencoded']; if (jsonbody && jsonbody.schema) { const requestBodySchema: InputValidationSchema = { title: 'Request', @@ -582,6 +585,7 @@ export class OpenAPIValidator { // add compiled params schema to schemas for this operation id const requestBodyValidator = this.getAjv(ValidationContext.RequestBody); + this.removeBinaryPropertiesFromRequired(requestBodySchema); validators.push(OpenAPIValidator.compileSchema(requestBodyValidator, requestBodySchema)); } } @@ -669,6 +673,49 @@ export class OpenAPIValidator { return validators; } + /** + * Removes binary properties from the required array in JSON schema, since they cannot be validated. + * + * @param {any} schema + * @memberof OpenAPIValidator + */ + private removeBinaryPropertiesFromRequired(schema: any): void { + if (typeof schema !== 'object' || !schema?.required) { + return; + } + + // If this is a schema with properties + if (schema.properties && schema.required && Array.isArray(schema.required)) { + // Find properties with binary format to exclude from required + const binaryProperties = Object.keys(schema.properties).filter((propName) => { + const prop = schema.properties[propName]; + return prop && prop.type === 'string' && prop.format === 'binary'; + }); + + // Remove binary properties from required array + if (binaryProperties.length > 0) { + schema.required = schema.required.filter((prop: string) => !binaryProperties.includes(prop)); + } + } + + // Recursively process nested objects and arrays + if (schema.properties) { + Object.values(schema.properties).forEach((prop) => this.removeBinaryPropertiesFromRequired(prop)); + } + + // Handle array items + if (schema.items) { + this.removeBinaryPropertiesFromRequired(schema.items); + } + + // Handle allOf, anyOf, oneOf + ['allOf', 'anyOf', 'oneOf'].forEach((key) => { + if (Array.isArray(schema[key])) { + schema[key].forEach((subSchema: any) => this.removeBinaryPropertiesFromRequired(subSchema)); + } + }); + } + /** * Get response validator function for an operation by operationId * From 4a0fe4d0e6a7d029798fc996c3901047d795f09c Mon Sep 17 00:00:00 2001 From: alexandroslagios Date: Thu, 15 May 2025 13:46:39 +0300 Subject: [PATCH 2/4] refactor: Remove unnecessary handling for arrays, since form-data doesn't support arrays --- src/validation.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/validation.ts b/src/validation.ts index 6510d9e6..3671c6e2 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import Ajv, { Options as AjvOpts, ErrorObject, FormatDefinition, ValidateFunction } from 'ajv'; -import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; +import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; import { OpenAPIRouter, Request, Operation } from './router'; import OpenAPIUtils from './utils'; import { PickVersionElement, SetMatchType } from './backend'; @@ -679,7 +679,7 @@ export class OpenAPIValidator { * @param {any} schema * @memberof OpenAPIValidator */ - private removeBinaryPropertiesFromRequired(schema: any): void { + private removeBinaryPropertiesFromRequired(schema: OpenAPIV3_1.SchemaObject): void { if (typeof schema !== 'object' || !schema?.required) { return; } @@ -688,7 +688,7 @@ export class OpenAPIValidator { if (schema.properties && schema.required && Array.isArray(schema.required)) { // Find properties with binary format to exclude from required const binaryProperties = Object.keys(schema.properties).filter((propName) => { - const prop = schema.properties[propName]; + const prop: OpenAPIV3_1.SchemaObject = schema.properties[propName]; return prop && prop.type === 'string' && prop.format === 'binary'; }); @@ -703,11 +703,6 @@ export class OpenAPIValidator { Object.values(schema.properties).forEach((prop) => this.removeBinaryPropertiesFromRequired(prop)); } - // Handle array items - if (schema.items) { - this.removeBinaryPropertiesFromRequired(schema.items); - } - // Handle allOf, anyOf, oneOf ['allOf', 'anyOf', 'oneOf'].forEach((key) => { if (Array.isArray(schema[key])) { From d817b23f105c1b81133fdba150ab531a5cfff95d Mon Sep 17 00:00:00 2001 From: alexandroslagios Date: Thu, 15 May 2025 13:47:31 +0300 Subject: [PATCH 3/4] refactor: Remove unnecessary comments --- src/validation.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/validation.ts b/src/validation.ts index 3671c6e2..af4fbba2 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -684,15 +684,12 @@ export class OpenAPIValidator { return; } - // If this is a schema with properties if (schema.properties && schema.required && Array.isArray(schema.required)) { - // Find properties with binary format to exclude from required const binaryProperties = Object.keys(schema.properties).filter((propName) => { const prop: OpenAPIV3_1.SchemaObject = schema.properties[propName]; return prop && prop.type === 'string' && prop.format === 'binary'; }); - // Remove binary properties from required array if (binaryProperties.length > 0) { schema.required = schema.required.filter((prop: string) => !binaryProperties.includes(prop)); } @@ -703,7 +700,6 @@ export class OpenAPIValidator { Object.values(schema.properties).forEach((prop) => this.removeBinaryPropertiesFromRequired(prop)); } - // Handle allOf, anyOf, oneOf ['allOf', 'anyOf', 'oneOf'].forEach((key) => { if (Array.isArray(schema[key])) { schema[key].forEach((subSchema: any) => this.removeBinaryPropertiesFromRequired(subSchema)); From f158d63473b805d213e9a80e474dc9290ca472ec Mon Sep 17 00:00:00 2001 From: alexandroslagios Date: Thu, 15 May 2025 13:48:20 +0300 Subject: [PATCH 4/4] docs: Fix type in JSDoc --- src/validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation.ts b/src/validation.ts index af4fbba2..88ed6f51 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -676,7 +676,7 @@ export class OpenAPIValidator { /** * Removes binary properties from the required array in JSON schema, since they cannot be validated. * - * @param {any} schema + * @param {OpenAPIV3_1.SchemaObject} schema * @memberof OpenAPIValidator */ private removeBinaryPropertiesFromRequired(schema: OpenAPIV3_1.SchemaObject): void {