Skip to content
Merged
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
66 changes: 64 additions & 2 deletions docs/modules/swagger.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ The Swagger module generates the following JSON:
```

### Config
The Swagger module offers simple configurations to customize the generated API documentation. Below are the default values:
The Swagger module offers configurations to customize the generated API documentation and control access. Below are the available configurations:

#### API Documentation Configuration
```yaml

info:
description: ""
version: 1.0.0
Expand All @@ -119,6 +119,68 @@ info:
- Terms: A URL to the terms of service for your API. Defaults to http://swagger.io/terms/.
- Email: The contact email for API support or inquiries.

#### Host-based Access Control
To protect your Swagger documentation from unauthorized access, you can configure host-based restrictions. This is particularly useful when your application is exposed to both internal and external networks.

```yaml
server:
swagger:
# Block access when hostname contains any of these substrings
blockedHostsContains:
- "api.example.com"
- "external.company.com"

# Optional: Only allow access when hostname contains any of these substrings
# If both allowedHostsContains and blockedHostsContains are set,
# blockedHostsContains takes precedence for better security
allowedHostsContains:
- "internal.company.com"
- "localhost"
- "127.0.0.1"
```

**Configuration Options:**
- `blockedHostsContains`: Array of strings. If the request hostname contains any of these substrings, access to Swagger will be denied with a 404 response. Takes precedence over allowlist for security.
- `allowedHostsContains`: Array of strings. If specified, only requests from hostnames containing these substrings will be allowed, unless they are also in the blocked list.

**Host Detection:**
The middleware checks both the `Host` header and `X-Forwarded-Host` header (common when behind reverse proxies). The `X-Forwarded-Host` header takes precedence if present.

**Example Use Cases:**

1. **Block External Gateway Access:**
```yaml
server:
swagger:
blockedHostsContains:
- "apigw.company.com"
- "public.api.company.com"
```

2. **Internal-Only Access:**
```yaml
server:
swagger:
allowedHostsContains:
- "internal"
- "corp"
- "localhost"
- "127.0.0.1"
```

3. **Development Environment:**
```yaml
server:
swagger:
allowedHostsContains:
- "localhost"
- "127.0.0.1"
- "dev.company.com"
- "staging.company.com"
```

**Security Note:** When access is denied, the middleware returns a 404 status code instead of 403 to avoid revealing the existence of Swagger endpoints to unauthorized users.




13 changes: 13 additions & 0 deletions example/server/resources/configs/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ server:
# to be able to use wildcard, we need to declare it between double quotes.
allowedOrigins: "*"
allowCredentials: false

# Swagger host-based access control
swagger:
# Block external access while allowing internal and development access
blockedHostsContains:
- "api.example.com"
- "external.company.com"
# Alternative: Use allowlist instead of blocklist
# allowedHostsContains:
# - "localhost"
# - "127.0.0.1"
# - "internal.company.com"
# - "dev.company.com"

info:
description: "this app is an example usage of Chaki."
Expand Down
68 changes: 64 additions & 4 deletions modules/swagger/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"strings"

"github.com/Trendyol/chaki/config"
"github.com/Trendyol/chaki/modules/server/common"
"github.com/Trendyol/chaki/modules/swagger/files"

Expand All @@ -12,9 +13,10 @@ import (
"github.com/gofiber/fiber/v2/middleware/redirect"
)

func fiberWrapper(docs Docs) common.FiberAppWrapper {
func fiberWrapper(docs Docs, cfg *config.Config) common.FiberAppWrapper {
return func(a *fiber.App) *fiber.App {
a.Use(
newHostAccessMiddleware(cfg),
newRedirectMiddleware(),
newMiddleware(docs),
)
Expand Down Expand Up @@ -42,14 +44,72 @@ func newMiddleware(docs Docs) fiber.Handler {

return func(c *fiber.Ctx) error {
if c.Path() == "/swagger/docs.json" || c.Path() == "/swagger/docs.json/" {
return c.JSON(docs.WithHost(c.Hostname()))
return c.JSON(docs.WithHost(getEffectiveHost(c)))
}

if strings.HasPrefix(c.Path(), prefix) {
c.Path(strings.TrimPrefix(c.Path(), prefix))
if after, ok := strings.CutPrefix(c.Path(), prefix); ok {
c.Path(after)
return fsmw(c)
}

return c.Next()
}
}

func newHostAccessMiddleware(cfg *config.Config) fiber.Handler {
serverCfg := cfg.Of("server")
if !serverCfg.Exists("swagger") {
return func(c *fiber.Ctx) error { return c.Next() }
}

swcfg := serverCfg.Of("swagger")
var blocked, allowed []string

if swcfg.Exists("blockedHostsContains") {
blocked = swcfg.GetStringSlice("blockedHostsContains")
}
if swcfg.Exists("allowedHostsContains") {
allowed = swcfg.GetStringSlice("allowedHostsContains")
}

return func(c *fiber.Ctx) error {
if !isSwaggerRequest(c.Path()) {
return c.Next()
}

host := getEffectiveHost(c)

if len(blocked) > 0 && containsAny(host, blocked) {
return c.SendStatus(fiber.StatusNotFound)
}

if len(allowed) > 0 && !containsAny(host, allowed) {
return c.SendStatus(fiber.StatusNotFound)
}

return c.Next()
}
}

func getEffectiveHost(c *fiber.Ctx) string {
if xfwd := c.Get("X-Forwarded-Host"); xfwd != "" {
return xfwd
}
return c.Hostname()
}

func isSwaggerRequest(path string) bool {
if path == "/" || path == "/swagger" || path == "/swagger.json" || path == "/swagger/v1/swagger.json" {
return true
}
return strings.HasPrefix(path, "/swagger/")
}

func containsAny(s string, subs []string) bool {
for _, sub := range subs {
if sub != "" && strings.Contains(s, sub) {
return true
}
}
return false
}
Loading
Loading