Skip to content

Commit 8a66010

Browse files
authored
openapi3filter: fix crash when given arrays of objects as query parameters (#664)
1 parent cd217fe commit 8a66010

File tree

2 files changed

+206
-1
lines changed

2 files changed

+206
-1
lines changed

openapi3filter/issue625_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package openapi3filter_test
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/getkin/kin-openapi/openapi3"
11+
"github.com/getkin/kin-openapi/openapi3filter"
12+
"github.com/getkin/kin-openapi/routers/gorillamux"
13+
)
14+
15+
func TestIssue625(t *testing.T) {
16+
17+
anyOfArraySpec := `
18+
openapi: 3.0.0
19+
info:
20+
version: 1.0.0
21+
title: Sample API
22+
paths:
23+
/items:
24+
get:
25+
description: Returns a list of stuff
26+
parameters:
27+
- description: test object
28+
explode: false
29+
in: query
30+
name: test
31+
required: false
32+
schema:
33+
type: array
34+
items:
35+
anyOf:
36+
- type: integer
37+
- type: boolean
38+
responses:
39+
'200':
40+
description: Successful response
41+
`[1:]
42+
43+
oneOfArraySpec := strings.ReplaceAll(anyOfArraySpec, "anyOf", "oneOf")
44+
45+
allOfArraySpec := strings.ReplaceAll(strings.ReplaceAll(anyOfArraySpec, "anyOf", "allOf"),
46+
"type: boolean", "type: number")
47+
48+
tests := []struct {
49+
name string
50+
spec string
51+
req string
52+
errStr string
53+
}{
54+
{
55+
name: "success anyof object array",
56+
spec: anyOfArraySpec,
57+
req: "/items?test=3,7",
58+
},
59+
{
60+
name: "failed anyof object array",
61+
spec: anyOfArraySpec,
62+
req: "/items?test=s1,s2",
63+
errStr: `parameter "test" in query has an error: path 0: value s1: an invalid boolean: invalid syntax`,
64+
},
65+
66+
{
67+
name: "success allof object array",
68+
spec: allOfArraySpec,
69+
req: `/items?test=1,3`,
70+
},
71+
{
72+
name: "failed allof object array",
73+
spec: allOfArraySpec,
74+
req: `/items?test=1.2,3.1`,
75+
errStr: `parameter "test" in query has an error: Value must be an integer`,
76+
},
77+
{
78+
name: "success oneof object array",
79+
spec: oneOfArraySpec,
80+
req: `/items?test=true,3`,
81+
},
82+
{
83+
name: "faled oneof object array",
84+
spec: oneOfArraySpec,
85+
req: `/items?test="val1","val2"`,
86+
errStr: `parameter "test" in query has an error: item 0: decoding oneOf failed: 0 schemas matched`,
87+
},
88+
}
89+
90+
for _, testcase := range tests {
91+
t.Run(testcase.name, func(t *testing.T) {
92+
loader := openapi3.NewLoader()
93+
ctx := loader.Context
94+
95+
doc, err := loader.LoadFromData([]byte(testcase.spec))
96+
require.NoError(t, err)
97+
98+
err = doc.Validate(ctx)
99+
require.NoError(t, err)
100+
101+
router, err := gorillamux.NewRouter(doc)
102+
require.NoError(t, err)
103+
httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil)
104+
require.NoError(t, err)
105+
106+
route, pathParams, err := router.FindRoute(httpReq)
107+
require.NoError(t, err)
108+
109+
requestValidationInput := &openapi3filter.RequestValidationInput{
110+
Request: httpReq,
111+
PathParams: pathParams,
112+
Route: route,
113+
}
114+
err = openapi3filter.ValidateRequest(ctx, requestValidationInput)
115+
if testcase.errStr == "" {
116+
require.NoError(t, err)
117+
} else {
118+
require.Contains(t, err.Error(), testcase.errStr)
119+
}
120+
},
121+
)
122+
}
123+
}

openapi3filter/req_resp_decoder.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,10 +526,92 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM
526526
}
527527
values = strings.Split(values[0], delim)
528528
}
529-
val, err := parseArray(values, schema)
529+
val, err := d.parseArray(values, sm, schema)
530530
return val, ok, err
531531
}
532532

533+
// parseArray returns an array that contains items from a raw array.
534+
// Every item is parsed as a primitive value.
535+
// The function returns an error when an error happened while parse array's items.
536+
func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]interface{}, error) {
537+
var value []interface{}
538+
539+
for i, v := range raw {
540+
item, err := d.parseValue(v, schemaRef.Value.Items)
541+
if err != nil {
542+
if v, ok := err.(*ParseError); ok {
543+
return nil, &ParseError{path: []interface{}{i}, Cause: v}
544+
}
545+
return nil, fmt.Errorf("item %d: %w", i, err)
546+
}
547+
548+
// If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive
549+
// values and some are nil values.
550+
if item == nil {
551+
return nil, nil
552+
}
553+
value = append(value, item)
554+
}
555+
return value, nil
556+
}
557+
558+
func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (interface{}, error) {
559+
if len(schema.Value.AllOf) > 0 {
560+
var value interface{}
561+
var err error
562+
for _, sr := range schema.Value.AllOf {
563+
value, err = d.parseValue(v, sr)
564+
if value == nil || err != nil {
565+
break
566+
}
567+
}
568+
return value, err
569+
}
570+
571+
if len(schema.Value.AnyOf) > 0 {
572+
var value interface{}
573+
var err error
574+
for _, sr := range schema.Value.AnyOf {
575+
value, err = d.parseValue(v, sr)
576+
if err == nil {
577+
return value, nil
578+
}
579+
}
580+
581+
return nil, err
582+
}
583+
584+
if len(schema.Value.OneOf) > 0 {
585+
isMatched := 0
586+
var value interface{}
587+
var err error
588+
for _, sr := range schema.Value.OneOf {
589+
result, err := d.parseValue(v, sr)
590+
if err == nil {
591+
value = result
592+
isMatched++
593+
}
594+
}
595+
if isMatched == 1 {
596+
return value, nil
597+
} else if isMatched > 1 {
598+
return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
599+
} else if isMatched == 0 {
600+
return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
601+
}
602+
603+
return nil, err
604+
}
605+
606+
if schema.Value.Not != nil {
607+
// TODO(decode not): handle decoding "not" JSON Schema
608+
return nil, errors.New("not implemented: decoding 'not'")
609+
}
610+
611+
return parsePrimitive(v, schema)
612+
613+
}
614+
533615
func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) {
534616
var propsFn func(url.Values) (map[string]string, error)
535617
switch sm.Style {

0 commit comments

Comments
 (0)