Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON schema validation for http request body #1046

Merged
merged 1 commit into from Jul 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -163,6 +163,7 @@
"java-invoke-local": "0.0.6",
"js-string-escape": "^1.0.1",
"jsonpath-plus": "^3.0.0",
"jsonschema": "^1.2.6",
"jsonwebtoken": "^8.5.1",
"jszip": "^3.2.2",
"luxon": "^1.22.0",
Expand Down
40 changes: 33 additions & 7 deletions src/events/http/HttpServer.js
Expand Up @@ -15,6 +15,7 @@ import {
VelocityContext,
} from './lambda-events/index.js'
import parseResources from './parseResources.js'
import payloadSchemaValidator from './payloadSchemaValidator.js'
import debugLog from '../../debugLog.js'
import serverlessLog, { logRoutes } from '../../serverlessLog.js'
import {
Expand Down Expand Up @@ -466,6 +467,12 @@ export default class HttpServer {
? requestTemplates[contentType]
: ''

const schema =
typeof endpoint?.request?.schema !== 'undefined' &&
integration === 'AWS'
? endpoint.request.schema[contentType]
: ''

// https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing
// so we have to do it ourselves
const contentTypesThatRequirePayloadParsing = [
Expand Down Expand Up @@ -493,6 +500,16 @@ export default class HttpServer {
debugLog('requestTemplate:', requestTemplate)
debugLog('payload:', request.payload)

/* REQUEST PAYLOAD SCHEMA VALIDATION */
if (schema) {
debugLog('schema:', request.schema)
try {
payloadSchemaValidator.validate(schema, request.payload)
} catch (err) {
return this._reply400(response, err.message, err)
}
}

/* REQUEST TEMPLATE PROCESSING (event population) */

let event = {}
Expand All @@ -509,7 +526,7 @@ export default class HttpServer {
requestPath,
).create()
} catch (err) {
return this._reply500(
return this._reply502(
response,
`Error while parsing template "${contentType}" for ${functionKey}`,
err,
Expand Down Expand Up @@ -566,7 +583,7 @@ export default class HttpServer {
// it here and reply in the same way that we would have above when
// we lazy-load the non-IPC handler function.
if (this.#options.useChildProcesses && err.ipcException) {
return this._reply500(
return this._reply502(
response,
`Error while loading ${functionKey}`,
err,
Expand Down Expand Up @@ -763,7 +780,7 @@ export default class HttpServer {
} else if (typeof result === 'string') {
response.source = JSON.stringify(result)
} else if (result && result.body && typeof result.body !== 'string') {
return this._reply500(
return this._reply502(
response,
'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object',
{},
Expand Down Expand Up @@ -835,7 +852,7 @@ export default class HttpServer {
response.variety = 'buffer'
} else {
if (result && result.body && typeof result.body !== 'string') {
return this._reply500(
return this._reply502(
response,
'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object',
{},
Expand Down Expand Up @@ -872,15 +889,14 @@ export default class HttpServer {
})
}

// Bad news
_reply500(response, message, error) {
_replyError(statusCode, response, message, error) {
serverlessLog(message)

console.error(error)

response.header('Content-Type', 'application/json')

response.statusCode = 502 // APIG replies 502 by default on failures;
response.statusCode = statusCode
response.source = {
errorMessage: message,
errorType: error.constructor.name,
Expand All @@ -892,6 +908,16 @@ export default class HttpServer {
return response
}

// Bad news
_reply502(response, message, error) {
// APIG replies 502 by default on failures;
return this._replyError(502, response, message, error)
}

_reply400(response, message, error) {
return this._replyError(400, response, message, error)
}

createResourceRoutes() {
const resourceRoutesOptions = this.#options.resourceRoutes

Expand Down
15 changes: 15 additions & 0 deletions src/events/http/payloadSchemaValidator.js
@@ -0,0 +1,15 @@
'use strict'

const { validate: validateJsonSchema } = require('jsonschema')

exports.validate = function validate(model, body) {
const result = validateJsonSchema(body, model)

if (result.errors.length > 0) {
throw new Error(
`Request body validation failed: ${result.errors
.map((e) => e.message)
.join(', ')}`,
)
}
}
7 changes: 7 additions & 0 deletions tests/integration/handler/handler.js
Expand Up @@ -209,3 +209,10 @@ exports.TestResourceVariable = (event, context, callback) => {
body: stringify(event.resource),
})
}

exports.TestPayloadSchemaValidation = (event, context, callback) => {
callback(null, {
statusCode: 200,
body: stringify(event.body),
})
}
48 changes: 48 additions & 0 deletions tests/integration/handler/handlerPayload.test.js
Expand Up @@ -322,3 +322,51 @@ describe('handler payload tests with prepend off', () => {
})
})
})

describe('handler payload scehma validation tests', () => {
// init
beforeAll(() =>
setup({
servicePath: resolve(__dirname),
args: ['--noPrependStageInUrl'],
}),
)

// cleanup
afterAll(() => teardown())
;[
{
description: 'test with valid payload',
expectedBody: `{"foo":"bar"}`,
path: '/test-payload-schema-validator',
body: {
foo: 'bar',
},
status: 200,
},

{
description: 'test with invalid payload',
path: '/test-payload-schema-validator',
body: {},
status: 400,
},
].forEach(({ description, expectedBody, path, body, status }) => {
test(description, async () => {
const url = joinUrl(TEST_BASE_URL, path)

const response = await fetch(url, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
})

expect(response.status).toEqual(status)

if (expectedBody) {
const json = await response.json()
expect(json.body).toEqual(expectedBody)
}
})
})
})
18 changes: 18 additions & 0 deletions tests/integration/handler/serverless.yml
Expand Up @@ -162,3 +162,21 @@ functions:
method: get
path: /{id}/test-resource-variable-handler
handler: handler.TestResourceVariable

TestPayloadSchemaValidation:
events:
- http:
method: post
path: /test-payload-schema-validator
integration: lambda
request:
schema:
application/json:
$schema: http://json-schema.org/draft-07/schema
type: object
required:
- foo
properties:
foo:
type: string
handler: handler.TestPayloadSchemaValidation