Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions oapi_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
59 changes: 59 additions & 0 deletions oapi_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
}
22 changes: 16 additions & 6 deletions test_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ paths:
minimum: 10
maximum: 100
responses:
'200':
"200":
description: success
content:
application/json:
Expand All @@ -29,7 +29,7 @@ paths:
post:
operationId: createResource
responses:
'204':
"204":
description: No content
requestBody:
required: true
Expand All @@ -46,7 +46,7 @@ paths:
- BearerAuth:
- someScope
responses:
'204':
"204":
description: no content
/protected_resource2:
get:
Expand All @@ -55,7 +55,7 @@ paths:
- BearerAuth:
- otherScope
responses:
'204':
"204":
description: no content
/protected_resource_401:
get:
Expand All @@ -64,7 +64,7 @@ paths:
- BearerAuth:
- unauthorized
responses:
'401':
"401":
description: no content
/multiparamresource:
get:
Expand All @@ -85,7 +85,7 @@ paths:
minimum: 10
maximum: 100
responses:
'200':
"200":
description: success
content:
application/json:
Expand All @@ -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:
Expand Down