Skip to content

Commit 9946f64

Browse files
Claudegaby
andcommitted
🔒 security: harden OpenAPI middleware - fix path generation, add nil checks, improve docs
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/359aa061-8041-46a8-b6c8-46ef09f85838 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
1 parent 38a32b9 commit 9946f64

File tree

4 files changed

+169
-5
lines changed

4 files changed

+169
-5
lines changed

app.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@ func (app *App) Description(desc string) Router {
825825
// Consumes assigns a request media type to the most recently added route.
826826
func (app *App) Consumes(typ string) Router {
827827
if typ != "" {
828+
typ = strings.TrimSpace(typ)
828829
if _, _, err := mime.ParseMediaType(typ); err != nil || !strings.Contains(typ, "/") {
829830
panic("invalid media type: " + typ)
830831
}
@@ -838,6 +839,7 @@ func (app *App) Consumes(typ string) Router {
838839
// Produces assigns a response media type to the most recently added route.
839840
func (app *App) Produces(typ string) Router {
840841
if typ != "" {
842+
typ = strings.TrimSpace(typ)
841843
if _, _, err := mime.ParseMediaType(typ); err != nil || !strings.Contains(typ, "/") {
842844
panic("invalid media type: " + typ)
843845
}

docs/middleware/openapi.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,15 @@ import (
2626
After you initiate your Fiber app, you can use the following possibilities:
2727

2828
```go
29-
// Initialize default config. Register the middleware *after* all routes
30-
// so that the spec includes every handler.
29+
// Initialize default config.
30+
//
31+
// The middleware inspects the app's routes and generates the OpenAPI spec
32+
// the first time a matching request (for example, GET /openapi.json) is served.
33+
// That spec is then cached for the lifetime of the process, so any routes
34+
// registered after the first OpenAPI request will not appear in the spec.
35+
//
36+
// To avoid surprises, register the middleware *after* all routes have been
37+
// added and before you start serving traffic.
3138
app.Use(openapi.New())
3239

3340
// Or extend your config for customization

middleware/openapi/openapi.go

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,11 @@ func generateSpec(app *fiber.App, cfg *Config) openAPISpec {
144144
continue
145145
}
146146

147-
path := r.Path
147+
path := convertToOpenAPIPath(r.Path, r.Params)
148148
params := make([]parameter, 0, len(r.Params))
149149
paramIndex := make(map[string]int, len(r.Params))
150150
if len(r.Params) > 0 {
151151
for _, p := range r.Params {
152-
path = strings.Replace(path, ":"+p, "{"+p+"}", 1)
153152
param := parameter{
154153
Name: p,
155154
In: "path",
@@ -309,7 +308,7 @@ func mergeConfigParameters(params []parameter, index map[string]int, extras []Pa
309308
}
310309

311310
func appendOrReplaceParameter(params []parameter, index map[string]int, p *parameter) []parameter {
312-
if p.Name == "" || p.In == "" {
311+
if p == nil || p.Name == "" || p.In == "" {
313312
return params
314313
}
315314
key := p.In + ":" + p.Name
@@ -518,3 +517,88 @@ func defaultResponseForMethod(method, mediaType string) (string, response) {
518517
}
519518
return status, resp
520519
}
520+
521+
// convertToOpenAPIPath converts Fiber route path patterns to OpenAPI path templates.
522+
// It handles parameter constraints (:id<int>), wildcards (*), plus params (+), and optional markers (?).
523+
// Examples:
524+
// - /users/:id<int> -> /users/{id}
525+
// - /files/* -> /files/{wildcard}
526+
// - /items/:id? -> /items/{id}
527+
// - /posts/:slug+ -> /posts/{slug}
528+
func convertToOpenAPIPath(fiberPath string, params []string) string {
529+
if len(params) == 0 && !strings.ContainsAny(fiberPath, ":*+") {
530+
return fiberPath
531+
}
532+
533+
// Build a map of parameter names for quick lookup
534+
paramSet := make(map[string]struct{}, len(params))
535+
for _, p := range params {
536+
paramSet[p] = struct{}{}
537+
}
538+
539+
var result strings.Builder
540+
result.Grow(len(fiberPath))
541+
i := 0
542+
length := len(fiberPath)
543+
544+
for i < length {
545+
ch := fiberPath[i]
546+
547+
switch ch {
548+
case ':':
549+
// Named parameter - extract name until we hit a constraint, optional marker, or delimiter
550+
i++
551+
paramStart := i
552+
for i < length {
553+
c := fiberPath[i]
554+
if c == '<' || c == '?' || c == '/' || c == '-' || c == '.' {
555+
break
556+
}
557+
i++
558+
}
559+
paramName := fiberPath[paramStart:i]
560+
561+
// Skip constraints like <int>, <regex(...)>, etc.
562+
if i < length && fiberPath[i] == '<' {
563+
depth := 1
564+
i++ // skip '<'
565+
for i < length && depth > 0 {
566+
switch fiberPath[i] {
567+
case '<':
568+
depth++
569+
case '>':
570+
depth--
571+
default:
572+
// Other characters inside constraints are ignored
573+
}
574+
i++
575+
}
576+
}
577+
578+
// Skip optional marker '?'
579+
if i < length && fiberPath[i] == '?' {
580+
i++
581+
}
582+
583+
// Write OpenAPI parameter placeholder
584+
if paramName != "" {
585+
_ = result.WriteByte('{') //nolint:errcheck // strings.Builder.WriteByte never returns an error
586+
_, _ = result.WriteString(paramName) //nolint:errcheck // strings.Builder.WriteString never returns an error
587+
_ = result.WriteByte('}') //nolint:errcheck // strings.Builder.WriteByte never returns an error
588+
}
589+
590+
case '*', '+':
591+
// Wildcard or plus param - use a generic name
592+
// In Fiber, * and + are greedy params that match everything
593+
// We represent them as {wildcard} or the named param if it exists
594+
_, _ = result.WriteString("{wildcard}") //nolint:errcheck // strings.Builder.WriteString never returns an error
595+
i++
596+
597+
default:
598+
_ = result.WriteByte(ch) //nolint:errcheck // strings.Builder.WriteByte never returns an error
599+
i++
600+
}
601+
}
602+
603+
return result.String()
604+
}

middleware/openapi/openapi_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,3 +750,74 @@ func Test_OpenAPI_AutoHeadExcluded(t *testing.T) {
750750
require.Contains(t, ops, "get")
751751
require.NotContains(t, ops, "head", "auto-generated HEAD route should be excluded")
752752
}
753+
754+
func Test_ConvertToOpenAPIPath(t *testing.T) {
755+
tests := []struct {
756+
name string
757+
fiberPath string
758+
expectPath string
759+
params []string
760+
}{
761+
{
762+
name: "simple path no params",
763+
fiberPath: "/users",
764+
params: nil,
765+
expectPath: "/users",
766+
},
767+
{
768+
name: "parameter with constraint",
769+
fiberPath: "/users/:id<int>",
770+
params: []string{"id"},
771+
expectPath: "/users/{id}",
772+
},
773+
{
774+
name: "parameter with regex constraint",
775+
fiberPath: "/posts/:slug<regex([a-z]+)>",
776+
params: []string{"slug"},
777+
expectPath: "/posts/{slug}",
778+
},
779+
{
780+
name: "optional parameter",
781+
fiberPath: "/items/:id?",
782+
params: []string{"id"},
783+
expectPath: "/items/{id}",
784+
},
785+
{
786+
name: "wildcard param",
787+
fiberPath: "/files/*",
788+
params: []string{"*"},
789+
expectPath: "/files/{wildcard}",
790+
},
791+
{
792+
name: "plus param",
793+
fiberPath: "/docs/+",
794+
params: []string{"+"},
795+
expectPath: "/docs/{wildcard}",
796+
},
797+
{
798+
name: "multiple params with constraints",
799+
fiberPath: "/api/:version<int>/:resource/:id<int>",
800+
params: []string{"version", "resource", "id"},
801+
expectPath: "/api/{version}/{resource}/{id}",
802+
},
803+
{
804+
name: "param with dot delimiter",
805+
fiberPath: "/files/:name.:ext",
806+
params: []string{"name", "ext"},
807+
expectPath: "/files/{name}.{ext}",
808+
},
809+
{
810+
name: "param with dash delimiter",
811+
fiberPath: "/users/:firstName-:lastName",
812+
params: []string{"firstName", "lastName"},
813+
expectPath: "/users/{firstName}-{lastName}",
814+
},
815+
}
816+
817+
for _, tt := range tests {
818+
t.Run(tt.name, func(t *testing.T) {
819+
result := convertToOpenAPIPath(tt.fiberPath, tt.params)
820+
require.Equal(t, tt.expectPath, result)
821+
})
822+
}
823+
}

0 commit comments

Comments
 (0)