From 81dbdeb502a6b136c104f580bcc54b7ed4563436 Mon Sep 17 00:00:00 2001 From: Jim Kalafut Date: Tue, 12 Aug 2025 14:07:23 -0700 Subject: [PATCH] Add option to decode path parameters before validation --- oapi_validate.go | 18 +++++++++++++ oapi_validate_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++ test_spec.yaml | 22 +++++++++++----- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/oapi_validate.go b/oapi_validate.go index f2ef893..fc9b369 100644 --- a/oapi_validate.go +++ b/oapi_validate.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "os" "strings" @@ -60,6 +61,9 @@ type Options struct { ParamDecoder openapi3filter.ContentParameterDecoder UserData interface{} MultiErrorHandler MultiErrorHandler + + // DecodePathParams enables decoding/unescaping of path parameters + DecodePathParams bool } // OapiRequestValidatorWithOptions creates a validator from a swagger object, with validation options @@ -98,6 +102,20 @@ func ValidateRequestFromContext(c *fiber.Ctx, router routers.Router, options *Op route, pathParams, err := router.FindRoute(r) + // FindRoute returns pathParams as percent-encoded values. This can make validation + // of length, character set, and other aspects more difficult if non-ASCII input is + // expected. These may be optionally decoded, but the default is to leave them as-is + // for backward compatibility. + if options.DecodePathParams { + for k, v := range pathParams { + p, err := url.PathUnescape(v) + if err != nil { + return err + } + pathParams[k] = p + } + } + // We failed to find a matching route for the request. if err != nil { switch e := err.(type) { diff --git a/oapi_validate_test.go b/oapi_validate_test.go index e19a893..f090c9c 100644 --- a/oapi_validate_test.go +++ b/oapi_validate_test.go @@ -432,3 +432,62 @@ func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) called = false } } + +func TestOapiRequestValidatorEscapedPathHandling(t *testing.T) { + swagger, err := openapi3.NewLoader().LoadFromData(testSchema) + require.NoError(t, err, "Error initializing swagger") + + for _, decodePathParams := range []bool{false, true} { + app := fiber.New() + + // Set up an authenticator to check authenticated function. It will allow + // access to "someScope", but disallow others. + options := Options{ + Options: openapi3filter.Options{ + ExcludeRequestBody: false, + ExcludeResponseBody: false, + IncludeResponseStatus: true, + MultiError: true, + }, + MultiErrorHandler: func(me openapi3.MultiError) error { + return fmt.Errorf("Bad stuff - %s", me.Error()) + }, + DecodePathParams: decodePathParams, + } + + // register middleware + app.Use(OapiRequestValidatorWithOptions(swagger, &options)) + + app.Get("/escaped_param/:id", func(c *fiber.Ctx) error { + return nil + }) + + // The spec has a max length of 12 and should reject the 👎 character. The escaped + // version of unicode characters are much longer and are used to test the validator setting. + tests := []struct { + param string + validIfDecodeFalse bool + validIfDecodeTrue bool + }{ + {"12_long_isok", true, true}, + {"🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂", false, true}, + {"🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂", false, false}, + {"👎", true, false}, + {"this_is_too_long", false, false}, + } + + for i, test := range tests { + res := doGet(t, app, fmt.Sprintf("https://deepmap.ai/escaped_param/%s", test.param)) + valid := res.StatusCode == http.StatusOK + + body, _ := io.ReadAll(res.Body) + defer res.Body.Close() + + if decodePathParams { + assert.Equal(t, test.validIfDecodeTrue, valid, "Test case %d failed: %s", i+1, string(body)) + } else { + assert.Equal(t, test.validIfDecodeFalse, valid, "Test case %d failed: %s", i+1, string(body)) + } + } + } +} diff --git a/test_spec.yaml b/test_spec.yaml index 6e0a241..f2801d5 100644 --- a/test_spec.yaml +++ b/test_spec.yaml @@ -16,7 +16,7 @@ paths: minimum: 10 maximum: 100 responses: - '200': + "200": description: success content: application/json: @@ -29,7 +29,7 @@ paths: post: operationId: createResource responses: - '204': + "204": description: No content requestBody: required: true @@ -46,7 +46,7 @@ paths: - BearerAuth: - someScope responses: - '204': + "204": description: no content /protected_resource2: get: @@ -55,7 +55,7 @@ paths: - BearerAuth: - otherScope responses: - '204': + "204": description: no content /protected_resource_401: get: @@ -64,7 +64,7 @@ paths: - BearerAuth: - unauthorized responses: - '401': + "401": description: no content /multiparamresource: get: @@ -85,7 +85,7 @@ paths: minimum: 10 maximum: 100 responses: - '200': + "200": description: success content: application/json: @@ -95,6 +95,16 @@ paths: type: string id: type: integer + /escaped_param/{id}: + get: + parameters: + - name: id + in: path + required: true + schema: + type: string + maxLength: 12 + pattern: "^[^👎]+$" components: securitySchemes: BearerAuth: