From ff1378e9624ae15a034947a9c354c3bb7828ae86 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Thu, 21 May 2026 01:29:19 -0700 Subject: [PATCH 1/5] fix(parameters): avoid panic on double-slash request paths Fixes #274 --- parameters/path_parameters.go | 9 +++++++++ parameters/path_parameters_test.go | 32 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index c5fd7ac..9c3dd66 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -60,6 +60,12 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p continue } + // guard against length mismatch (e.g. request path containing + // a double slash producing extra empty segments). + if x >= len(submittedSegments) { + continue + } + var rgx *regexp.Regexp if v.options.RegexCache != nil { @@ -83,6 +89,9 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p } matches := rgx.FindStringSubmatch(submittedSegments[x]) + if matches == nil { + continue + } matches = matches[1:] // Check if it is well-formed. diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index 5972439..19ede87 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -2348,3 +2348,35 @@ paths: assert.EqualValues(t, 1, cache.storeCount, "No new stores on cache hit") assert.EqualValues(t, 1, cache.hitCount, "Second OData lookup should hit cache") } + +func TestValidatePathParamsWithPathItem_DoubleSlashDoesNotPanic(t *testing.T) { + // Regression test for #274: ValidatePathParamsWithPathItem panics for + // request paths containing a leading double slash (e.g. //test/path/x), + // because path segments and submitted segments differ in length. + spec := `openapi: 3.1.0 +paths: + /test/path/{param}: + get: + operationId: testParam + parameters: + - in: path + name: param + required: true + schema: + type: string + responses: + "200": + description: ok` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + v := NewParameterValidator(&m.Model) + + req, _ := http.NewRequest(http.MethodGet, "https://example.com//test/path/fubar", nil) + pathItem := m.Model.Paths.PathItems.GetOrZero("/test/path/{param}") + require.NotNil(t, pathItem) + + assert.NotPanics(t, func() { + _, _ = v.ValidatePathParamsWithPathItem(req, pathItem, "/test/path/{param}") + }) +} From f90e3946cbed35bc4757c9211458155be553b09e Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Thu, 21 May 2026 01:38:43 -0700 Subject: [PATCH 2/5] test: cover length-mismatch guard --- parameters/path_parameters_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index 19ede87..8f3d47b 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -2379,4 +2379,11 @@ paths: assert.NotPanics(t, func() { _, _ = v.ValidatePathParamsWithPathItem(req, pathItem, "/test/path/{param}") }) + + // Also cover the case where the submitted path has fewer segments than + // the spec path, which would otherwise index submittedSegments out of bounds. + shortReq, _ := http.NewRequest(http.MethodGet, "https://example.com/test/path", nil) + assert.NotPanics(t, func() { + _, _ = v.ValidatePathParamsWithPathItem(shortReq, pathItem, "/test/path/{param}") + }) } From cce08bfa5f30d842ef6741b4c1fbee16e334324e Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sat, 23 May 2026 21:52:27 -0700 Subject: [PATCH 3/5] fix(parameters): align path segments by dropping empty entries --- parameters/path_parameters.go | 42 +++++++++++++++++++++--------- parameters/path_parameters_test.go | 20 ++++++++------ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index 9c3dd66..635a1a4 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -42,9 +42,11 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p HowToFix: errors.HowToFixPath, }} } - // split the path into segments - submittedSegments := strings.Split(paths.StripRequestPath(request, v.document), helpers.Slash) - pathSegments := strings.Split(pathValue, helpers.Slash) + // split the path into segments, dropping empty segments so that a request + // path containing a double slash (e.g. //test/path) does not shift the + // index alignment between submitted and template segments. + submittedSegments := nonEmptyPathSegments(paths.StripRequestPath(request, v.document)) + pathSegments := nonEmptyPathSegments(pathValue) // get the operation method for error reporting operation := strings.ToLower(request.Method) @@ -54,18 +56,17 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p var validationErrors []*errors.ValidationError for _, p := range params { if p.In == helpers.Path { + // a mismatch in segment counts means the submitted path cannot be + // aligned with the template (e.g. an extra empty segment from a + // double slash); reject it rather than silently mis-validating. + if len(submittedSegments) != len(pathSegments) { + validationErrors = append(validationErrors, + errors.PathParameterMissing(p, pathValue, request.URL.Path)) + continue + } + // var paramTemplate string for x := range pathSegments { - if pathSegments[x] == "" { // skip empty segments - continue - } - - // guard against length mismatch (e.g. request path containing - // a double slash producing extra empty segments). - if x >= len(submittedSegments) { - continue - } - var rgx *regexp.Regexp if v.options.RegexCache != nil { @@ -389,6 +390,21 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p return true, nil } +// nonEmptyPathSegments splits a path on "/" and drops empty segments, so that +// leading, trailing, or repeated slashes do not introduce empty entries that +// would shift index alignment between submitted and template segments. +func nonEmptyPathSegments(path string) []string { + raw := strings.Split(path, helpers.Slash) + segments := make([]string, 0, len(raw)) + for _, segment := range raw { + if segment == "" { + continue + } + segments = append(segments, segment) + } + return segments +} + func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string, pathValue string, renderedSchema string) (string, float64, []*errors.ValidationError) { if isLabel && p.Style == helpers.LabelStyle { paramValueParsed, err := strconv.ParseFloat(paramValue[1:], 64) diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index 8f3d47b..45d7102 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -2376,14 +2376,18 @@ paths: pathItem := m.Model.Paths.PathItems.GetOrZero("/test/path/{param}") require.NotNil(t, pathItem) - assert.NotPanics(t, func() { - _, _ = v.ValidatePathParamsWithPathItem(req, pathItem, "/test/path/{param}") - }) + // the leading double slash collapses to an empty segment that must be + // dropped, so the remaining segments still align and 'fubar' is validated + // against {param} instead of being silently accepted against the wrong + // segment. + valid, errs := v.ValidatePathParamsWithPathItem(req, pathItem, "/test/path/{param}") + assert.True(t, valid) + assert.Empty(t, errs) - // Also cover the case where the submitted path has fewer segments than - // the spec path, which would otherwise index submittedSegments out of bounds. + // When the submitted path has fewer segments than the spec path it can no + // longer be aligned, so it must be rejected rather than mis-validated. shortReq, _ := http.NewRequest(http.MethodGet, "https://example.com/test/path", nil) - assert.NotPanics(t, func() { - _, _ = v.ValidatePathParamsWithPathItem(shortReq, pathItem, "/test/path/{param}") - }) + valid, errs = v.ValidatePathParamsWithPathItem(shortReq, pathItem, "/test/path/{param}") + assert.False(t, valid) + assert.Len(t, errs, 1) } From 3c36636070ed440f6071a7d9ccad20fe7611dcb0 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sun, 24 May 2026 21:33:28 -0700 Subject: [PATCH 4/5] test: cover nil-match guard for constrained path template segments Signed-off-by: Sai Asish Y --- parameters/path_parameters_test.go | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index 45d7102..e766797 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -2391,3 +2391,37 @@ paths: assert.False(t, valid) assert.Len(t, errs, 1) } + +func TestValidatePathParamsWithPathItem_RegexNoMatchContinues(t *testing.T) { + // When a submitted path segment does not match the regex for a + // constrained path template segment (e.g. {id:[0-9]+} vs "abc"), the + // nil-match guard must skip the segment without panicking. + spec := `openapi: 3.1.0 +paths: + /items/{id:[0-9]+}: + get: + operationId: getItem + parameters: + - in: path + name: id + required: true + schema: + type: integer + responses: + "200": + description: ok` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + v := NewParameterValidator(&m.Model) + + // "abc" does not match ^([0-9]+)$; the nil-match guard should skip the + // segment cleanly rather than panicking on a nil FindStringSubmatch result. + req, _ := http.NewRequest(http.MethodGet, "https://example.com/items/abc", nil) + pathItem := m.Model.Paths.PathItems.GetOrZero("/items/{id:[0-9]+}") + require.NotNil(t, pathItem) + + assert.NotPanics(t, func() { + v.ValidatePathParamsWithPathItem(req, pathItem, "/items/{id:[0-9]+}") + }) +} From d9a349ec44eb7be897b2f342877ac671dd2fa0fa Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 26 May 2026 14:56:54 -0700 Subject: [PATCH 5/5] fix(parameters): return error on regex no-match for constrained template segments Signed-off-by: Sai Asish Y --- parameters/path_parameters.go | 4 +++- parameters/path_parameters_test.go | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index 635a1a4..2b162c6 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -91,7 +91,9 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p matches := rgx.FindStringSubmatch(submittedSegments[x]) if matches == nil { - continue + validationErrors = append(validationErrors, + errors.PathParameterMissing(p, pathValue, request.URL.Path)) + break } matches = matches[1:] diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index e766797..e1d5fde 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -2395,7 +2395,7 @@ paths: func TestValidatePathParamsWithPathItem_RegexNoMatchContinues(t *testing.T) { // When a submitted path segment does not match the regex for a // constrained path template segment (e.g. {id:[0-9]+} vs "abc"), the - // nil-match guard must skip the segment without panicking. + // guard must not panic and must return a validation error. spec := `openapi: 3.1.0 paths: /items/{id:[0-9]+}: @@ -2415,13 +2415,12 @@ paths: m, _ := doc.BuildV3Model() v := NewParameterValidator(&m.Model) - // "abc" does not match ^([0-9]+)$; the nil-match guard should skip the - // segment cleanly rather than panicking on a nil FindStringSubmatch result. + // "abc" does not match ^([0-9]+)$; expect invalid + at least one error. req, _ := http.NewRequest(http.MethodGet, "https://example.com/items/abc", nil) pathItem := m.Model.Paths.PathItems.GetOrZero("/items/{id:[0-9]+}") require.NotNil(t, pathItem) - assert.NotPanics(t, func() { - v.ValidatePathParamsWithPathItem(req, pathItem, "/items/{id:[0-9]+}") - }) + valid, errs := v.ValidatePathParamsWithPathItem(req, pathItem, "/items/{id:[0-9]+}") + assert.False(t, valid) + assert.NotEmpty(t, errs) }