Skip to content

Commit 43e0d56

Browse files
committed
Added extra validation for payloads when decoding schemas #17
Signed-off-by: Dave Shanley <[email protected]>
1 parent 7c8c7c7 commit 43e0d56

File tree

6 files changed

+539
-329
lines changed

6 files changed

+539
-329
lines changed

requests/validate_body_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,3 +854,46 @@ paths:
854854
assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message)
855855

856856
}
857+
858+
func TestValidateBody_InvalidSchema_BadDecode(t *testing.T) {
859+
spec := `openapi: 3.1.0
860+
paths:
861+
/burgers/createBurger:
862+
post:
863+
requestBody:
864+
content:
865+
application/json:
866+
schema:
867+
$ref: '#/components/schema_validation/TestBody'
868+
components:
869+
schema_validation:
870+
TestBody:
871+
type: object
872+
properties:
873+
name:
874+
type: string
875+
patties:
876+
type: integer
877+
maximum: 3
878+
minimum: 1
879+
vegetarian:
880+
type: boolean
881+
required: [name, patties, vegetarian] `
882+
883+
doc, _ := libopenapi.NewDocument([]byte(spec))
884+
885+
m, _ := doc.BuildV3Model()
886+
v := NewRequestBodyValidator(&m.Model)
887+
888+
request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
889+
bytes.NewBuffer([]byte("{\"bad\": \"json\",}")))
890+
request.Header.Set("Content-Type", "application/json")
891+
892+
valid, errors := v.ValidateRequestBody(request)
893+
894+
assert.False(t, valid)
895+
assert.Len(t, errors, 1)
896+
assert.Len(t, errors[0].SchemaValidationErrors, 1)
897+
assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason)
898+
899+
}

requests/validate_request.go

Lines changed: 157 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -4,142 +4,168 @@
44
package requests
55

66
import (
7-
"bytes"
8-
"encoding/json"
9-
"fmt"
10-
"github.com/pb33f/libopenapi-validator/errors"
11-
"github.com/pb33f/libopenapi-validator/helpers"
12-
"github.com/pb33f/libopenapi-validator/schema_validation"
13-
"github.com/pb33f/libopenapi/datamodel/high/base"
14-
"github.com/santhosh-tekuri/jsonschema/v5"
15-
"gopkg.in/yaml.v3"
16-
"io"
17-
"net/http"
18-
"reflect"
19-
"regexp"
20-
"strconv"
21-
"strings"
7+
"bytes"
8+
"encoding/json"
9+
"fmt"
10+
"github.com/pb33f/libopenapi-validator/errors"
11+
"github.com/pb33f/libopenapi-validator/helpers"
12+
"github.com/pb33f/libopenapi-validator/schema_validation"
13+
"github.com/pb33f/libopenapi/datamodel/high/base"
14+
"github.com/santhosh-tekuri/jsonschema/v5"
15+
"gopkg.in/yaml.v3"
16+
"io"
17+
"net/http"
18+
"reflect"
19+
"regexp"
20+
"strconv"
21+
"strings"
2222
)
2323

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

2626
// ValidateRequestSchema will validate an http.Request pointer against a schema.
2727
// If validation fails, it will return a list of validation errors as the second return value.
2828
func ValidateRequestSchema(
29-
request *http.Request,
30-
schema *base.Schema,
31-
renderedSchema,
32-
jsonSchema []byte) (bool, []*errors.ValidationError) {
33-
34-
var validationErrors []*errors.ValidationError
35-
36-
requestBody, _ := io.ReadAll(request.Body)
37-
38-
// close the request body, so it can be re-read later by another player in the chain
39-
_ = request.Body.Close()
40-
request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
41-
42-
var decodedObj interface{}
43-
_ = json.Unmarshal(requestBody, &decodedObj)
44-
45-
// no request body? failed to decode anything? nothing to do here.
46-
if requestBody == nil || decodedObj == nil {
47-
return true, nil
48-
}
49-
50-
compiler := jsonschema.NewCompiler()
51-
_ = compiler.AddResource("requestBody.json", strings.NewReader(string(jsonSchema)))
52-
jsch, _ := compiler.Compile("requestBody.json")
53-
54-
// 4. validate the object against the schema
55-
scErrs := jsch.Validate(decodedObj)
56-
if scErrs != nil {
57-
jk := scErrs.(*jsonschema.ValidationError)
58-
59-
// flatten the validationErrors
60-
schFlatErrs := jk.BasicOutput().Errors
61-
var schemaValidationErrors []*errors.SchemaValidationFailure
62-
for q := range schFlatErrs {
63-
er := schFlatErrs[q]
64-
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
65-
continue // ignore this error, it's useless tbh, utter noise.
66-
}
67-
if er.Error != "" {
68-
69-
// re-encode the schema.
70-
var renderedNode yaml.Node
71-
_ = yaml.Unmarshal(renderedSchema, &renderedNode)
72-
73-
// locate the violated property in the schema
74-
located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation)
75-
76-
// extract the element specified by the instance
77-
val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation)
78-
var referenceObject string
79-
80-
if len(val) > 0 {
81-
referenceIndex, _ := strconv.Atoi(val[1])
82-
if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice {
83-
found := decodedObj.([]any)[referenceIndex]
84-
recoded, _ := json.MarshalIndent(found, "", " ")
85-
referenceObject = string(recoded)
86-
}
87-
}
88-
if referenceObject == "" {
89-
referenceObject = string(requestBody)
90-
}
91-
92-
violation := &errors.SchemaValidationFailure{
93-
Reason: er.Error,
94-
Location: er.KeywordLocation,
95-
ReferenceSchema: string(renderedSchema),
96-
ReferenceObject: referenceObject,
97-
OriginalError: jk,
98-
}
99-
// if we have a location within the schema, add it to the error
100-
if located != nil {
101-
102-
line := located.Line
103-
// if the located node is a map or an array, then the actual human interpretable
104-
// line on which the violation occurred is the line of the key, not the value.
105-
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
106-
if line > 0 {
107-
line--
108-
}
109-
}
110-
111-
// location of the violation within the rendered schema.
112-
violation.Line = line
113-
violation.Column = located.Column
114-
}
115-
schemaValidationErrors = append(schemaValidationErrors, violation)
116-
}
117-
}
118-
119-
line := 1
120-
col := 0
121-
if schema.GoLow().Type.KeyNode != nil {
122-
line = schema.GoLow().Type.KeyNode.Line
123-
col = schema.GoLow().Type.KeyNode.Column
124-
}
125-
126-
// add the error to the list
127-
validationErrors = append(validationErrors, &errors.ValidationError{
128-
ValidationType: helpers.RequestBodyValidation,
129-
ValidationSubType: helpers.Schema,
130-
Message: fmt.Sprintf("%s request body for '%s' failed to validate schema",
131-
request.Method, request.URL.Path),
132-
Reason: "The request body is defined as an object. " +
133-
"However, it does not meet the schema requirements of the specification",
134-
SpecLine: line,
135-
SpecCol: col,
136-
SchemaValidationErrors: schemaValidationErrors,
137-
HowToFix: errors.HowToFixInvalidSchema,
138-
Context: string(renderedSchema), // attach the rendered schema to the error
139-
})
140-
}
141-
if len(validationErrors) > 0 {
142-
return false, validationErrors
143-
}
144-
return true, nil
29+
request *http.Request,
30+
schema *base.Schema,
31+
renderedSchema,
32+
jsonSchema []byte) (bool, []*errors.ValidationError) {
33+
34+
var validationErrors []*errors.ValidationError
35+
36+
requestBody, _ := io.ReadAll(request.Body)
37+
38+
// close the request body, so it can be re-read later by another player in the chain
39+
_ = request.Body.Close()
40+
request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
41+
42+
var decodedObj interface{}
43+
44+
if len(requestBody) > 0 {
45+
err := json.Unmarshal(requestBody, &decodedObj)
46+
47+
if err != nil {
48+
// cannot decode the request body, so it's not valid
49+
violation := &errors.SchemaValidationFailure{
50+
Reason: err.Error(),
51+
Location: "unavailable",
52+
ReferenceSchema: string(renderedSchema),
53+
ReferenceObject: string(requestBody),
54+
}
55+
validationErrors = append(validationErrors, &errors.ValidationError{
56+
ValidationType: helpers.RequestBodyValidation,
57+
ValidationSubType: helpers.Schema,
58+
Message: fmt.Sprintf("%s request body for '%s' failed to validate schema",
59+
request.Method, request.URL.Path),
60+
Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()),
61+
SpecLine: 1,
62+
SpecCol: 0,
63+
SchemaValidationErrors: []*errors.SchemaValidationFailure{violation},
64+
HowToFix: errors.HowToFixInvalidSchema,
65+
Context: string(renderedSchema), // attach the rendered schema to the error
66+
})
67+
return false, validationErrors
68+
}
69+
}
70+
71+
// no request body? failed to decode anything? nothing to do here.
72+
if requestBody == nil || decodedObj == nil {
73+
return true, nil
74+
}
75+
76+
compiler := jsonschema.NewCompiler()
77+
_ = compiler.AddResource("requestBody.json", strings.NewReader(string(jsonSchema)))
78+
jsch, _ := compiler.Compile("requestBody.json")
79+
80+
// 4. validate the object against the schema
81+
scErrs := jsch.Validate(decodedObj)
82+
if scErrs != nil {
83+
jk := scErrs.(*jsonschema.ValidationError)
84+
85+
// flatten the validationErrors
86+
schFlatErrs := jk.BasicOutput().Errors
87+
var schemaValidationErrors []*errors.SchemaValidationFailure
88+
for q := range schFlatErrs {
89+
er := schFlatErrs[q]
90+
if er.KeywordLocation == "" || strings.HasPrefix(er.Error, "doesn't validate with") {
91+
continue // ignore this error, it's useless tbh, utter noise.
92+
}
93+
if er.Error != "" {
94+
95+
// re-encode the schema.
96+
var renderedNode yaml.Node
97+
_ = yaml.Unmarshal(renderedSchema, &renderedNode)
98+
99+
// locate the violated property in the schema
100+
located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation)
101+
102+
// extract the element specified by the instance
103+
val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation)
104+
var referenceObject string
105+
106+
if len(val) > 0 {
107+
referenceIndex, _ := strconv.Atoi(val[1])
108+
if reflect.ValueOf(decodedObj).Type().Kind() == reflect.Slice {
109+
found := decodedObj.([]any)[referenceIndex]
110+
recoded, _ := json.MarshalIndent(found, "", " ")
111+
referenceObject = string(recoded)
112+
}
113+
}
114+
if referenceObject == "" {
115+
referenceObject = string(requestBody)
116+
}
117+
118+
violation := &errors.SchemaValidationFailure{
119+
Reason: er.Error,
120+
Location: er.KeywordLocation,
121+
ReferenceSchema: string(renderedSchema),
122+
ReferenceObject: referenceObject,
123+
OriginalError: jk,
124+
}
125+
// if we have a location within the schema, add it to the error
126+
if located != nil {
127+
128+
line := located.Line
129+
// if the located node is a map or an array, then the actual human interpretable
130+
// line on which the violation occurred is the line of the key, not the value.
131+
if located.Kind == yaml.MappingNode || located.Kind == yaml.SequenceNode {
132+
if line > 0 {
133+
line--
134+
}
135+
}
136+
137+
// location of the violation within the rendered schema.
138+
violation.Line = line
139+
violation.Column = located.Column
140+
}
141+
schemaValidationErrors = append(schemaValidationErrors, violation)
142+
}
143+
}
144+
145+
line := 1
146+
col := 0
147+
if schema.GoLow().Type.KeyNode != nil {
148+
line = schema.GoLow().Type.KeyNode.Line
149+
col = schema.GoLow().Type.KeyNode.Column
150+
}
151+
152+
// add the error to the list
153+
validationErrors = append(validationErrors, &errors.ValidationError{
154+
ValidationType: helpers.RequestBodyValidation,
155+
ValidationSubType: helpers.Schema,
156+
Message: fmt.Sprintf("%s request body for '%s' failed to validate schema",
157+
request.Method, request.URL.Path),
158+
Reason: "The request body is defined as an object. " +
159+
"However, it does not meet the schema requirements of the specification",
160+
SpecLine: line,
161+
SpecCol: col,
162+
SchemaValidationErrors: schemaValidationErrors,
163+
HowToFix: errors.HowToFixInvalidSchema,
164+
Context: string(renderedSchema), // attach the rendered schema to the error
165+
})
166+
}
167+
if len(validationErrors) > 0 {
168+
return false, validationErrors
169+
}
170+
return true, nil
145171
}

0 commit comments

Comments
 (0)