Skip to content

Commit

Permalink
Added extra validation for payloads when decoding schemas #17
Browse files Browse the repository at this point in the history
Signed-off-by: Dave Shanley <dave@quobix.com>
  • Loading branch information
daveshanley committed Aug 7, 2023
1 parent 7c8c7c7 commit 43e0d56
Show file tree
Hide file tree
Showing 6 changed files with 539 additions and 329 deletions.
43 changes: 43 additions & 0 deletions requests/validate_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -854,3 +854,46 @@ paths:
assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)

}

func TestValidateBody_InvalidSchema_BadDecode(t *testing.T) {
spec := `openapi: 3.1.0
paths:
/burgers/createBurger:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schema_validation/TestBody'
components:
schema_validation:
TestBody:
type: object
properties:
name:
type: string
patties:
type: integer
maximum: 3
minimum: 1
vegetarian:
type: boolean
required: [name, patties, vegetarian] `

doc, _ := libopenapi.NewDocument([]byte(spec))

m, _ := doc.BuildV3Model()
v := NewRequestBodyValidator(&m.Model)

request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
bytes.NewBuffer([]byte("{\"bad\": \"json\",}")))
request.Header.Set("Content-Type", "application/json")

valid, errors := v.ValidateRequestBody(request)

assert.False(t, valid)
assert.Len(t, errors, 1)
assert.Len(t, errors[0].SchemaValidationErrors, 1)
assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason)

}
288 changes: 157 additions & 131 deletions requests/validate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,142 +4,168 @@
package requests

import (
"bytes"
"encoding/json"
"fmt"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi-validator/schema_validation"
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/santhosh-tekuri/jsonschema/v5"
"gopkg.in/yaml.v3"
"io"
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
"bytes"
"encoding/json"
"fmt"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi-validator/schema_validation"
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/santhosh-tekuri/jsonschema/v5"
"gopkg.in/yaml.v3"
"io"
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
)

var instanceLocationRegex = regexp.MustCompile(`^/(\d+)`)

// ValidateRequestSchema will validate an http.Request pointer against a schema.
// If validation fails, it will return a list of validation errors as the second return value.
func ValidateRequestSchema(
request *http.Request,
schema *base.Schema,
renderedSchema,
jsonSchema []byte) (bool, []*errors.ValidationError) {

var validationErrors []*errors.ValidationError

requestBody, _ := io.ReadAll(request.Body)

// close the request body, so it can be re-read later by another player in the chain
_ = request.Body.Close()
request.Body = io.NopCloser(bytes.NewBuffer(requestBody))

var decodedObj interface{}
_ = json.Unmarshal(requestBody, &decodedObj)

// no request body? failed to decode anything? nothing to do here.
if requestBody == nil || decodedObj == nil {
return true, nil
}

compiler := jsonschema.NewCompiler()
_ = compiler.AddResource("requestBody.json", strings.NewReader(string(jsonSchema)))
jsch, _ := compiler.Compile("requestBody.json")

// 4. validate the object against the schema
scErrs := jsch.Validate(decodedObj)
if scErrs != nil {
jk := scErrs.(*jsonschema.ValidationError)

// flatten the validationErrors
schFlatErrs := jk.BasicOutput().Errors
var schemaValidationErrors []*errors.SchemaValidationFailure
for q := range schFlatErrs {
er := schFlatErrs[q]
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
continue // ignore this error, it's useless tbh, utter noise.
}
if er.Error != "" {

// re-encode the schema.
var renderedNode yaml.Node
_ = yaml.Unmarshal(renderedSchema, &renderedNode)

// locate the violated property in the schema
located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation)

// extract the element specified by the instance
val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation)
var referenceObject string

if len(val) > 0 {
referenceIndex, _ := strconv.Atoi(val[1])
if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice {
found := decodedObj.([]any)[referenceIndex]
recoded, _ := json.MarshalIndent(found, "", " ")
referenceObject = string(recoded)
}
}
if referenceObject == "" {
referenceObject = string(requestBody)
}

violation := &errors.SchemaValidationFailure{
Reason: er.Error,
Location: er.KeywordLocation,
ReferenceSchema: string(renderedSchema),
ReferenceObject: referenceObject,
OriginalError: jk,
}
// if we have a location within the schema, add it to the error
if located != nil {

line := located.Line
// if the located node is a map or an array, then the actual human interpretable
// line on which the violation occurred is the line of the key, not the value.
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
if line > 0 {
line--
}
}

// location of the violation within the rendered schema.
violation.Line = line
violation.Column = located.Column
}
schemaValidationErrors = append(schemaValidationErrors, violation)
}
}

line := 1
col := 0
if schema.GoLow().Type.KeyNode != nil {
line = schema.GoLow().Type.KeyNode.Line
col = schema.GoLow().Type.KeyNode.Column
}

// add the error to the list
validationErrors = append(validationErrors, &errors.ValidationError{
ValidationType: helpers.RequestBodyValidation,
ValidationSubType: helpers.Schema,
Message: fmt.Sprintf("%s request body for '%s' failed to validate schema",
request.Method, request.URL.Path),
Reason: "The request body is defined as an object. " +
"However, it does not meet the schema requirements of the specification",
SpecLine: line,
SpecCol: col,
SchemaValidationErrors: schemaValidationErrors,
HowToFix: errors.HowToFixInvalidSchema,
Context: string(renderedSchema), // attach the rendered schema to the error
})
}
if len(validationErrors) > 0 {
return false, validationErrors
}
return true, nil
request *http.Request,
schema *base.Schema,
renderedSchema,
jsonSchema []byte) (bool, []*errors.ValidationError) {

var validationErrors []*errors.ValidationError

requestBody, _ := io.ReadAll(request.Body)

// close the request body, so it can be re-read later by another player in the chain
_ = request.Body.Close()
request.Body = io.NopCloser(bytes.NewBuffer(requestBody))

var decodedObj interface{}

if len(requestBody) > 0 {
err := json.Unmarshal(requestBody, &decodedObj)

if err != nil {
// cannot decode the request body, so it's not valid
violation := &errors.SchemaValidationFailure{
Reason: err.Error(),
Location: "unavailable",
ReferenceSchema: string(renderedSchema),
ReferenceObject: string(requestBody),
}
validationErrors = append(validationErrors, &errors.ValidationError{
ValidationType: helpers.RequestBodyValidation,
ValidationSubType: helpers.Schema,
Message: fmt.Sprintf("%s request body for '%s' failed to validate schema",
request.Method, request.URL.Path),
Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()),
SpecLine: 1,
SpecCol: 0,
SchemaValidationErrors: []*errors.SchemaValidationFailure{violation},
HowToFix: errors.HowToFixInvalidSchema,
Context: string(renderedSchema), // attach the rendered schema to the error
})
return false, validationErrors
}
}

// no request body? failed to decode anything? nothing to do here.
if requestBody == nil || decodedObj == nil {
return true, nil
}

compiler := jsonschema.NewCompiler()
_ = compiler.AddResource("requestBody.json", strings.NewReader(string(jsonSchema)))
jsch, _ := compiler.Compile("requestBody.json")

// 4. validate the object against the schema
scErrs := jsch.Validate(decodedObj)
if scErrs != nil {
jk := scErrs.(*jsonschema.ValidationError)

// flatten the validationErrors
schFlatErrs := jk.BasicOutput().Errors
var schemaValidationErrors []*errors.SchemaValidationFailure
for q := range schFlatErrs {
er := schFlatErrs[q]
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
continue // ignore this error, it's useless tbh, utter noise.
}
if er.Error != "" {

// re-encode the schema.
var renderedNode yaml.Node
_ = yaml.Unmarshal(renderedSchema, &renderedNode)

// locate the violated property in the schema
located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation)

// extract the element specified by the instance
val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation)
var referenceObject string

if len(val) > 0 {
referenceIndex, _ := strconv.Atoi(val[1])
if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice {
found := decodedObj.([]any)[referenceIndex]
recoded, _ := json.MarshalIndent(found, "", " ")
referenceObject = string(recoded)
}
}
if referenceObject == "" {
referenceObject = string(requestBody)
}

violation := &errors.SchemaValidationFailure{
Reason: er.Error,
Location: er.KeywordLocation,
ReferenceSchema: string(renderedSchema),
ReferenceObject: referenceObject,
OriginalError: jk,
}
// if we have a location within the schema, add it to the error
if located != nil {

line := located.Line
// if the located node is a map or an array, then the actual human interpretable
// line on which the violation occurred is the line of the key, not the value.
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
if line > 0 {
line--
}
}

// location of the violation within the rendered schema.
violation.Line = line
violation.Column = located.Column
}
schemaValidationErrors = append(schemaValidationErrors, violation)
}
}

line := 1
col := 0
if schema.GoLow().Type.KeyNode != nil {
line = schema.GoLow().Type.KeyNode.Line
col = schema.GoLow().Type.KeyNode.Column
}

// add the error to the list
validationErrors = append(validationErrors, &errors.ValidationError{
ValidationType: helpers.RequestBodyValidation,
ValidationSubType: helpers.Schema,
Message: fmt.Sprintf("%s request body for '%s' failed to validate schema",
request.Method, request.URL.Path),
Reason: "The request body is defined as an object. " +
"However, it does not meet the schema requirements of the specification",
SpecLine: line,
SpecCol: col,
SchemaValidationErrors: schemaValidationErrors,
HowToFix: errors.HowToFixInvalidSchema,
Context: string(renderedSchema), // attach the rendered schema to the error
})
}
if len(validationErrors) > 0 {
return false, validationErrors
}
return true, nil
}
Loading

0 comments on commit 43e0d56

Please sign in to comment.