diff --git a/packages/ruleset/src/functions/integer-attributes.js b/packages/ruleset/src/functions/integer-attributes.js index 917545b87..2f53e0a10 100644 --- a/packages/ruleset/src/functions/integer-attributes.js +++ b/packages/ruleset/src/functions/integer-attributes.js @@ -29,6 +29,15 @@ module.exports = function (schema, _opts, context) { // These are the valid format values that can be used with integer schemas. const intFormats = ['int32', 'int64']; +//These are the minimum and maximum values for integer schemas +const int32Minimum = -2147483648; +const int32Maximum = 2147483647; + +// Constrained for interoperability per +// https://datatracker.ietf.org/doc/html/rfc7159#section-6 +const int64Minimum = -9007199254740991; // -(2^53-1) or Number.MIN_SAFE_INTEGER +const int64Maximum = 9007199254740991; // 2^53 or Number.MAX_SAFE_INTEGER + /** * This function performs various checks on an integer schema property to make sure it * contains the "minimum", "maximum" attributes. @@ -50,10 +59,13 @@ function integerBoundaryErrors(schema, path) { const errors = []; - if (isDefined(format) && !intFormats.includes(format)) { + if ( + !isDefined(format) || + (isDefined(format) && !intFormats.includes(format)) + ) { errors.push({ message: `Integer schemas should specify format as one of ${intFormats.join( - '.' + ', ' )}`, path, }); @@ -63,12 +75,40 @@ function integerBoundaryErrors(schema, path) { message: `Integer schemas should define property 'minimum'`, path, }); + } else if (format === 'int32') { + if (minimum > int32Maximum || minimum < int32Minimum) { + errors.push({ + message: `'minimum' value is out of safe range`, + path, + }); + } + } else { + if (minimum > int64Maximum || minimum < int64Minimum) { + errors.push({ + message: `'minimum' value is out of safe range`, + path, + }); + } } if (!isDefined(maximum)) { errors.push({ message: `Integer schemas should define property 'maximum'`, path, }); + } else if (format === 'int32') { + if (maximum > int32Maximum || maximum < int32Minimum) { + errors.push({ + message: `'maximum' value is out of safe range`, + path, + }); + } + } else { + if (maximum > int64Maximum || maximum < int64Minimum) { + errors.push({ + message: `'maximum' value is out of safe range`, + path, + }); + } } if (isDefined(minimum) && isDefined(maximum) && minimum > maximum) { errors.push({ diff --git a/packages/ruleset/test/rules/integer-attributes.test.js b/packages/ruleset/test/rules/integer-attributes.test.js index b5d294509..5687de888 100644 --- a/packages/ruleset/test/rules/integer-attributes.test.js +++ b/packages/ruleset/test/rules/integer-attributes.test.js @@ -117,6 +117,7 @@ describe(`Spectral rule: ${ruleId}`, () => { testDocument.components.schemas['RuleTester2'] = { description: 'Tests integer fields within allOf', type: ['integer'], + format: 'int32', allOf: [ { minimum: 1, @@ -142,6 +143,7 @@ describe(`Spectral rule: ${ruleId}`, () => { in: 'query', schema: { type: 'integer', + format: 'int32', minimum: 1, }, }, @@ -166,6 +168,7 @@ describe(`Spectral rule: ${ruleId}`, () => { in: 'query', schema: { type: 'integer', + format: 'int64', maximum: 720, }, }, @@ -216,6 +219,7 @@ describe(`Spectral rule: ${ruleId}`, () => { in: 'query', schema: { type: 'integer', + format: 'int32', allOf: [{ maximum: 10 }, { maximum: 25 }], }, }, @@ -240,6 +244,7 @@ describe(`Spectral rule: ${ruleId}`, () => { in: 'query', schema: { type: 'integer', + format: 'int32', allOf: [ { oneOf: [{ minimum: 0 }, { minimum: 3 }], @@ -300,6 +305,7 @@ describe(`Spectral rule: ${ruleId}`, () => { in: 'query', schema: { type: 'integer', + format: 'int64', allOf: [{ minimum: 10 }, { maximum: 9 }], }, }, @@ -328,6 +334,7 @@ describe(`Spectral rule: ${ruleId}`, () => { in: 'query', schema: { type: ['integer'], + format: 'int32', minimum: 16, maximum: 15, }, @@ -404,6 +411,7 @@ describe(`Spectral rule: ${ruleId}`, () => { properties: { size: { type: 'integer', + format: 'int32', description: 'no validation', }, }, @@ -437,6 +445,7 @@ describe(`Spectral rule: ${ruleId}`, () => { ] = { schema: { type: 'integer', + format: 'int32', description: 'no validation', }, }; @@ -464,5 +473,97 @@ describe(`Spectral rule: ${ruleId}`, () => { expect(results[i].path.join('.')).toBe(expectedPaths[i]); } }); + it('Integer schema int32 minimum is out of safe range', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/movies'].post.parameters = [ + { + name: 'max_movie_length', + in: 'query', + schema: { + type: 'integer', + format: 'int32', + minimum: -9007199254740991, + maximum: 2147483649, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPath = 'paths./v1/movies.post.parameters.0.schema'; + + const expectedMessages = [ + `'minimum' value is out of safe range`, + `'maximum' value is out of safe range`, + ]; + + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedMessages[i]); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPath); + } + }); + it('Integer schema int64 minimum is out of safe range', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/movies'].post.parameters = [ + { + name: 'max_movie_length', + in: 'query', + schema: { + type: 'integer', + format: 'int64', + minimum: -9007199254740992, + maximum: 9007199254740992, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(2); + + const expectedPath = 'paths./v1/movies.post.parameters.0.schema'; + + const expectedMessages = [ + `'minimum' value is out of safe range`, + `'maximum' value is out of safe range`, + ]; + + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toBe(expectedMessages[i]); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPath); + } + }); + it('Integer schema doesnt have format defined', async () => { + const testDocument = makeCopy(rootDocument); + testDocument.paths['/v1/movies'].post.parameters = [ + { + name: 'max_movie_length', + in: 'query', + schema: { + type: 'integer', + minimum: 2, + maximum: 3, + }, + }, + ]; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(1); + + const expectedPath = 'paths./v1/movies.post.parameters.0.schema'; + + const expectedMessage = 'Integer schemas should specify format as one of'; + + for (let i = 0; i < results.length; i++) { + expect(results[i].code).toBe(ruleId); + expect(results[i].message).toContain(expectedMessage); + expect(results[i].severity).toBe(expectedSeverity); + expect(results[i].path.join('.')).toBe(expectedPath); + } + }); }); }); diff --git a/packages/ruleset/test/test-utils/root-document.js b/packages/ruleset/test/test-utils/root-document.js index 3aa727f98..ac6b89b96 100644 --- a/packages/ruleset/test/test-utils/root-document.js +++ b/packages/ruleset/test/test-utils/root-document.js @@ -1044,6 +1044,7 @@ module.exports = { }, status_code: { type: 'integer', + format: 'int32', minimum: 0, maximum: 599, description: 'The HTTP status code.', diff --git a/packages/validator/test/cli-validator/mock-files/oas3/component-path-example.yaml b/packages/validator/test/cli-validator/mock-files/oas3/component-path-example.yaml index ae6ed7352..5d0b42777 100644 --- a/packages/validator/test/cli-validator/mock-files/oas3/component-path-example.yaml +++ b/packages/validator/test/cli-validator/mock-files/oas3/component-path-example.yaml @@ -114,11 +114,13 @@ components: type: string limit: type: integer + format: int32 minimum: 1 maximum: 100 description: for pagination offset: type: integer + format: int32 minimum: 0 maximum: 1024000 description: for pagination diff --git a/packages/validator/test/cli-validator/mock-files/oas31/component-path-example.yaml b/packages/validator/test/cli-validator/mock-files/oas31/component-path-example.yaml index 0ad4c4aad..5ac3141fd 100644 --- a/packages/validator/test/cli-validator/mock-files/oas31/component-path-example.yaml +++ b/packages/validator/test/cli-validator/mock-files/oas31/component-path-example.yaml @@ -114,11 +114,13 @@ components: type: string limit: type: integer + format: int32 minimum: 1 maximum: 100 description: for pagination offset: type: integer + format: int32 minimum: 0 maximum: 1024000 description: for pagination