Skip to content

Commit

Permalink
Merge pull request #24 from criteria-labs/invalid-openapi-guards
Browse files Browse the repository at this point in the history
Guard against invalid OpenAPI documents when dereferencing
  • Loading branch information
jcmosc authored Feb 28, 2024
2 parents 3dfc608 + 0f973c7 commit 5027177
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { visitOpenAPIObjects } from './visitOpenAPIObjects'

describe('visitOpenAPIObjects()', () => {
describe('with invalid OpenAPI document', () => {
test('should not throw', () => {
const openAPI = {
openapi: '3.1.0',
paths: {
'/endpoint': null // not an object
}
}
expect(() => {
visitOpenAPIObjects(openAPI, 'openapi', {}, () => {})
}).not.toThrow()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export type OpenAPIObject<ReferenceType extends Reference | never> =
| PathItem<ReferenceType>
| ReferenceType

function isObject(value: any): value is object {
return typeof value === 'object' && value !== null
}

function isArray(value: any): value is object {
return typeof value === 'object' && value !== null && Array.isArray(value)
}

function appendJSONPointer(path: JSONPointer[], jsonPointer: JSONPointer): JSONPointer[] {
return [...path.slice(0, -1), `${path[path.length - 1]}${jsonPointer}`]
}
Expand Down Expand Up @@ -80,6 +88,9 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
states: State[],
visitor: (element: T, path: JSONPointer[], states: State[]) => void
) => {
if (!isObject(map)) {
return false
}
for (const [key, value] of Object.entries(map)) {
const stop = Boolean(visitor(value, appendJSONPointer(path, `/${escapeReferenceToken(key)}`), states))
if (stop) {
Expand All @@ -95,6 +106,9 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
states: State[],
visitor: (element: T, path: JSONPointer[], states: State[]) => void
) => {
if (!isArray(list)) {
return false
}
for (let index = 0; index < list.length; index++) {
const stop = Boolean(visitor(list[index], appendJSONPointer(path, `/${index}`), states))
if (stop) {
Expand All @@ -105,6 +119,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitOpenAPI = (openapi: OpenAPI<ReferenceType>, path: JSONPointer[], states: State[]) => {
if (!isObject(openapi)) {
return false
}

let stop = false

if (!stop && openapi.paths) {
Expand All @@ -121,6 +139,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitPaths = (paths: Paths<ReferenceType>, states: State[]) => {
if (!isObject(paths)) {
return false
}

let stop = false

for (const [key, pathItem] of Object.entries(paths)) {
Expand All @@ -136,6 +158,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitPathItem = (pathItem: PathItem<ReferenceType> | ReferenceType, path: JSONPointer[], states: State[]) => {
if (!isObject(pathItem)) {
return false
}

if (seen.has(pathItem)) {
return false
}
Expand Down Expand Up @@ -194,6 +220,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
path: JSONPointer[],
states: State[]
) => {
if (!isObject(operation)) {
return false
}

if (seen.has(operation)) {
return false
}
Expand Down Expand Up @@ -228,6 +258,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitSchema = (schema: Schema<ReferenceType> | ReferenceType, path: JSONPointer[], states: State[]) => {
if (!isObject(schema) && typeof schema !== 'boolean') {
return false
}

const newState = { ...states[states.length - 1] }
states = [...states, newState]

Expand Down Expand Up @@ -255,6 +289,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitResponse = (response: Response<ReferenceType> | ReferenceType, path: JSONPointer[], states: State[]) => {
if (!isObject(response)) {
return false
}

if (seen.has(response)) {
return false
}
Expand Down Expand Up @@ -290,6 +328,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
path: JSONPointer[],
states: State[]
) => {
if (!isObject(parameter)) {
return false
}

if (seen.has(parameter)) {
return false
}
Expand All @@ -307,7 +349,7 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
return false
}

if (!stop && parameter.schema) {
if (!stop && parameter.schema !== undefined) {
stop = visitSchema(parameter.schema, [...path, '/schema'], states)
}
if (!stop && parameter.examples) {
Expand All @@ -321,6 +363,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitExample = (example: Example | ReferenceType, path: JSONPointer[], states: State[]) => {
if (!isObject(example)) {
return false
}

if (seen.has(example)) {
return false
}
Expand All @@ -342,6 +388,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
path: JSONPointer[],
states: State[]
) => {
if (!isObject(requestBody)) {
return false
}

if (seen.has(requestBody)) {
return false
}
Expand All @@ -367,6 +417,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitHeader = (header: Header<ReferenceType> | ReferenceType, path: JSONPointer[], states: State[]) => {
if (!isObject(header)) {
return false
}

if (seen.has(header)) {
return false
}
Expand All @@ -384,7 +438,7 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
return false
}

if (!stop && header.schema) {
if (!stop && header.schema !== undefined) {
stop = visitSchema(header.schema, [...path, '/schema'], states)
}
if (!stop && header.examples) {
Expand All @@ -402,6 +456,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
path: JSONPointer[],
states: State[]
) => {
if (!isObject(securityScheme)) {
return false
}

if (seen.has(securityScheme)) {
return false
}
Expand All @@ -419,6 +477,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitLink = (link: Link | ReferenceType, path: JSONPointer[], states: State[]) => {
if (!isObject(link)) {
return false
}

if (seen.has(link)) {
return false
}
Expand All @@ -436,6 +498,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitCallback = (callback: Callback<ReferenceType> | ReferenceType, path: JSONPointer[], states: State[]) => {
if (!isObject(callback)) {
return false
}

if (seen.has(callback)) {
return false
}
Expand Down Expand Up @@ -463,6 +529,10 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitComponents = (components: Components<ReferenceType>, path: JSONPointer[], states: State[]) => {
if (!isObject(components)) {
return false
}

let stop = false
if (!stop && components.schemas) {
stop = visitMap(components.schemas, [...path, '/schemas'], states, visitSchema)
Expand Down Expand Up @@ -498,20 +568,28 @@ export function visitOpenAPIObjects<ReferenceType extends Reference | never, Sta
}

const visitMediaType = (mediaType: MediaType<ReferenceType>, path: JSONPointer[], states: State[]) => {
if (!isObject(mediaType)) {
return false
}

let stop = false
if (!stop && mediaType.encoding) {
stop = visitMap(mediaType.encoding, [...path, '/encoding'], states, visitEncoding)
}
if (!stop && mediaType.examples) {
stop = visitMap(mediaType.examples, [...path, '/examples'], states, visitExample)
}
if (!stop && mediaType.schema) {
if (!stop && mediaType.schema !== undefined) {
stop = visitSchema(mediaType.schema, [...path, '/schema'], states)
}
return stop
}

const visitEncoding = (encoding: Encoding<ReferenceType>, path: JSONPointer[], states: State[]) => {
if (!isObject(encoding)) {
return false
}

let stop = false
if (!stop && encoding.headers) {
stop = visitMap(encoding.headers, [...path, '/headers'], states, visitHeader)
Expand Down

0 comments on commit 5027177

Please sign in to comment.