diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 48ee25a..aa870c9 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -470,6 +470,31 @@ func IncorrectCookieParamArrayNumber( } } +func QueryParameterCannotBeDecoded(param *v3.Parameter, val string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + specLine, specCol := paramSchemaTypeLineCol(param) + + return &ValidationError{ + ValidationType: helpers.ParameterValidation, + ValidationSubType: helpers.ParameterValidationQuery, + Message: fmt.Sprintf("Query parameter '%s' cannot be decoded", param.Name), + Reason: fmt.Sprintf("The query parameter '%s' is defined as an object, "+ + "however the value '%s' cannot be decoded as an object", param.Name, val), + SpecLine: specLine, + SpecCol: specCol, + ParameterName: param.Name, + Context: sch, + HowToFix: HowToFixInvalidEncoding, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Query value '%s' cannot be decoded as object", val), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, + } +} + func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 5b24d6d..b775ffb 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -197,9 +197,27 @@ doneLooking: } } + if encodedObj == nil { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) + break skipValues + } + objVal, objExists := encodedObj[params[p].Name] + if !objExists || objVal == nil { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) + break skipValues + } + objMap, mapOk := objVal.(map[string]interface{}) + if !mapOk { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) + break skipValues + } + numErrors := len(validationErrors) validationErrors = append(validationErrors, - ValidateParameterSchema(sch, encodedObj[params[p].Name].(map[string]interface{}), + ValidateParameterSchema(sch, objMap, ef, "Query parameter", "The query parameter", diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 1bc9ca7..f6289e2 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" + liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/paths" ) @@ -4140,3 +4141,83 @@ paths: assert.True(t, valid, "issue #91: item_count=10 with type: string should not fail with 'expected string, but got number'") assert.Empty(t, errors) } + +func TestQueryParamObjectMissingKey_NoPanic(t *testing.T) { + // A content-wrapped object parameter with a non-JSON content type cannot + // be decoded into a map. The validator must not panic AND must report a + // validation error (the parameter is present but unsatisfiable). + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + text/plain: + schema: + type: object + properties: + color: + type: string + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?filter=color,blue", nil) + + var valid bool + var valErrors []*liberrors.ValidationError + assert.NotPanics(t, func() { + valid, valErrors = v.ValidateQueryParams(request) + }) + + assert.False(t, valid, "parameter present but not decodable as object should be invalid") + require.Len(t, valErrors, 1) + assert.Equal(t, "Query parameter 'filter' cannot be decoded", valErrors[0].Message) +} + +func TestQueryParamObjectContentJSON_ValidObject(t *testing.T) { + // A content-wrapped JSON parameter with a valid JSON object should validate + // successfully against the schema. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + application/json: + schema: + type: object + properties: + color: + type: string + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, `https://example.com/test?filter={"color":"blue"}`, nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.True(t, valid, "valid JSON object should pass validation") + assert.Empty(t, valErrors) +}