Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fa639a8
feat(middleware): add hostauthorization config
mutantkeyboard Apr 7, 2026
7c7b410
feat(middleware): add hostauthorization handler
mutantkeyboard Apr 7, 2026
a5e97ec
test(middleware): add hostauthorization config, normalizeHost, and ma…
mutantkeyboard Apr 7, 2026
22114d7
test(middleware): add hostauthorization integration tests
mutantkeyboard Apr 7, 2026
b33e427
test(middleware): add hostauthorization benchmarks
mutantkeyboard Apr 7, 2026
3ecfc3d
fix(middleware): panic on no-arg hostauthorization config
mutantkeyboard Apr 7, 2026
a96d0e8
docs(middleware): add hostauthorization usage documentation
mutantkeyboard Apr 8, 2026
4b45ec9
chore(middleware): add hostauthorization to README, fix lint issues
mutantkeyboard Apr 8, 2026
698e01b
Zero allocation fixes + utils
mutantkeyboard Apr 9, 2026
6dfb453
feat(middleware): add hostauthorization config
mutantkeyboard Apr 7, 2026
e0d7971
feat(middleware): add hostauthorization handler
mutantkeyboard Apr 7, 2026
44f5299
test(middleware): add hostauthorization config, normalizeHost, and ma…
mutantkeyboard Apr 7, 2026
b6c3ca1
test(middleware): add hostauthorization integration tests
mutantkeyboard Apr 7, 2026
eb0219f
test(middleware): add hostauthorization benchmarks
mutantkeyboard Apr 7, 2026
3c75222
fix(middleware): panic on no-arg hostauthorization config
mutantkeyboard Apr 7, 2026
ecaff03
Merge branch 'main' into host_auth_middleware
mutantkeyboard Apr 9, 2026
cf7f9ce
Test fixes
mutantkeyboard Apr 9, 2026
30a0dee
Merge branch 'main' into host_auth_middleware
ReneWerner87 Apr 11, 2026
b3cf3ed
Merge branch 'main' into host_auth_middleware
ReneWerner87 Apr 11, 2026
b542c02
Fix tests with fasthttp
mutantkeyboard Apr 13, 2026
5f7a435
Fix codecov
mutantkeyboard Apr 13, 2026
04bc8e1
Merge branch 'main' into host_auth_middleware
mutantkeyboard Apr 13, 2026
39e32f0
Fix Go 1.26 test failure and improve test hygiene in the hostauthoriz…
mutantkeyboard Apr 13, 2026
803c5c8
Merge branch 'main' into host_auth_middleware
gaby Apr 15, 2026
2b21039
Fix normalizeHosts
mutantkeyboard Apr 15, 2026
32c78a6
CIDR fixes
mutantkeyboard Apr 15, 2026
3334763
Merge branch 'main' into host_auth_middleware
gaby Apr 24, 2026
ba418c4
Merge branch 'main' into host_auth_middleware
mutantkeyboard Apr 30, 2026
32e51a6
Fixed tests and edge cases, better handlers and updated docs
mutantkeyboard Apr 30, 2026
bfa353e
Fix MD formatting issue
mutantkeyboard Apr 30, 2026
5f32ae4
Fix golangci-lint
mutantkeyboard May 5, 2026
fc757a5
Merge branch 'main' into host_auth_middleware
ReneWerner87 May 7, 2026
649cfbf
Final fixes as proposed by @ReneWerner87
mutantkeyboard May 7, 2026
bfbfdbd
Fix for SplitSeq
mutantkeyboard May 7, 2026
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
1 change: 1 addition & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,7 @@ Here is a list of middleware that are included within the Fiber framework.
| [favicon](https://github.com/gofiber/fiber/tree/main/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. |
| [healthcheck](https://github.com/gofiber/fiber/tree/main/middleware/healthcheck) | Liveness and Readiness probes for Fiber. |
| [helmet](https://github.com/gofiber/fiber/tree/main/middleware/helmet) | Helps secure your apps by setting various HTTP headers. |
| [hostauthorization](https://github.com/gofiber/fiber/tree/main/middleware/hostauthorization) | Validates the Host header against a configurable allowlist, protecting against DNS rebinding attacks. |
| [idempotency](https://github.com/gofiber/fiber/tree/main/middleware/idempotency) | Allows for fault-tolerant APIs where duplicate requests do not erroneously cause the same action performed multiple times on the server-side. |
| [keyauth](https://github.com/gofiber/fiber/tree/main/middleware/keyauth) | Adds support for key based authentication. |
| [limiter](https://github.com/gofiber/fiber/tree/main/middleware/limiter) | Adds Rate-limiting support to Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. |
Expand Down
203 changes: 203 additions & 0 deletions docs/middleware/hostauthorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
id: hostauthorization
---

# Host Authorization

Host authorization middleware for [Fiber](https://github.com/gofiber/fiber) that validates the incoming `Host` header against a configurable allowlist. Protects against [DNS rebinding attacks](https://en.wikipedia.org/wiki/DNS_rebinding) where an attacker-controlled domain resolves to the application's internal IP, causing browsers to send requests with a malicious Host header.

## Signatures

```go
func New(config ...Config) fiber.Handler
```

## Examples

Import the middleware package:

```go
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/hostauthorization"
)
```

Once your Fiber app is initialized, choose one of the following approaches:

### Basic Usage

```go
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHosts: []string{"api.myapp.com"},
}))

app.Get("/users", func(c fiber.Ctx) error {
return c.JSON(getUsers())
})

// Host: api.myapp.com → 200 OK
// Host: evil.com → 403 Forbidden
```

### Subdomain Wildcards

A leading dot matches any subdomain but **not** the bare domain itself:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This not matching the bare domain, is a weird behavior.


```go
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHosts: []string{".myapp.com"},
}))

// Host: api.myapp.com → 200 OK
// Host: www.myapp.com → 200 OK
// Host: myapp.com → 403 Forbidden
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a confusing behavior. Requiring 2 allow rules

```

To allow both the bare domain and all subdomains, include both:

```go
AllowedHosts: []string{"myapp.com", ".myapp.com"},
```

### CIDR Ranges

Useful for services accessed directly by IP (e.g. internal tooling) where the `Host` header will be a raw IP address. This matches the **Host header value** against a CIDR range — it does not filter by client IP address:

```go
app.Use(hostauthorization.New(hostauthorization.Config{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of confusing with https://docs.gofiber.io/whats_new#trusted-proxies

I rather see the same TrustedProxyConfig here, to avoid duplicated code.

AllowedHosts: []string{
"internal.myapp.com",
"10.0.0.0/8", // Host header IPs in this range are allowed
"127.0.0.1", // Host header must be exactly this IP
},
}))

// Host: internal.myapp.com → 200 OK
// Host: 10.0.50.3 → 200 OK (Host header IP is in 10.0.0.0/8)
// Host: 169.254.169.254 → 403 Forbidden (Host header IP not in allowlist)
```

### Skipping Health Checks

Use `Next` to bypass host validation for specific paths:

```go
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHosts: []string{"myapp.com", ".myapp.com"},
Next: func(c fiber.Ctx) bool {
return c.Path() == "/healthz"
},
}))

// Host: evil.com GET /healthz → 200 OK (skipped)
// Host: evil.com GET /users → 403 Forbidden
```

### Dynamic Validation

Use `AllowedHostsFunc` for hosts that can't be known at startup:

```go
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHostsFunc: func(host string) bool {
// Look up tenant domains from database, cache, etc.
return isRegisteredTenant(host)
},
}))
```

`AllowedHostsFunc` is only called when static `AllowedHosts` don't match, so you can combine both:

```go
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHosts: []string{"myapp.com", ".myapp.com"},
AllowedHostsFunc: func(host string) bool {
return isRegisteredCustomDomain(host)
},
}))
```

### Custom Error Response

```go
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHosts: []string{"myapp.com"},
ErrorHandler: func(c fiber.Ctx, err error) error {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "unauthorized host",
})
},
}))
```

### Combined with Domain() Router

`hostauthorization` acts as a security gate; [`Domain()`](https://docs.gofiber.io) handles routing:

```go
// Security layer — reject anything not from our hosts
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHosts: []string{"myapp.com", ".myapp.com"},
Next: func(c fiber.Ctx) bool {
return c.Path() == "/healthz"
},
}))

// Routing layer — direct allowed hosts to the right handlers
app.Domain("api.myapp.com").Get("/users", listUsers)
app.Domain(":tenant.myapp.com").Get("/dashboard", tenantDashboard)
app.Get("/healthz", healthCheck)
```

## Config

| Property | Type | Description | Default |
|:-----------------|:------------------------------|:--------------------------------------------------------------------------------------------------|:--------|
| Next | `func(fiber.Ctx) bool` | Defines a function to skip this middleware when returned true. | `nil` |
| AllowedHosts | `[]string` | List of permitted hosts. Supports exact match, subdomain wildcard (`.example.com`), and CIDR. | `nil` |
| AllowedHostsFunc | `func(string) bool` | Dynamic validator called when static AllowedHosts don't match. Receives the normalized hostname. | `nil` |
| ErrorHandler | `fiber.ErrorHandler` | Called when a request is rejected. Receives `ErrForbiddenHost` as the error. | 403 |

Either `AllowedHosts` or `AllowedHostsFunc` (or both) must be provided. The middleware panics at startup if neither is set.

## Default Config

```go
var ConfigDefault = Config{}
```

There is no useful default — you must provide at least `AllowedHosts` or `AllowedHostsFunc`.

## Host Matching

The middleware matches hosts in this order:

1. **Exact match** — case-insensitive, port and trailing dot stripped
2. **Subdomain wildcard** — `".myapp.com"` matches `api.myapp.com` but not `myapp.com`
3. **CIDR range** — host is parsed as IP and checked against the network
4. **AllowedHostsFunc** — called only if no static rule matched

The first match wins. If nothing matches, `ErrorHandler` is called.

## Host Normalization

Before matching, the incoming host is normalized:

- Port is stripped (via `c.Hostname()`)
- Trailing dot removed (`example.com.` → `example.com`)
- IPv6 brackets removed (`[::1]` → `::1`)
- Lowercased

`AllowedHosts` entries are also lowercased at initialization.

## Proxy Support

The middleware uses Fiber's `c.Hostname()`, which respects `X-Forwarded-Host` when [`TrustProxy`](https://docs.gofiber.io/api/fiber#config) is enabled. When `TrustProxy` is disabled (the default), `X-Forwarded-Host` is ignored and the raw `Host` header is used.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a sentence here noting HTTP/2 behavior: clients send :authority instead of Host. fasthttp transparently maps it, so the middleware works the same in both protocols, but operators reading this section currently might assume HTTP/1.1-only.


## RFC Compliance

- **RFC 9110 Section 7.2** — Host and port are separate components; port is stripped before matching
- **RFC 9110 Section 17.1** — Origin servers should reject misdirected requests
- **RFC 9112 Section 3.2** — Requests with missing Host headers should be rejected
- Returns **403 Forbidden** (not 400) because the request is syntactically valid but semantically unauthorized
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth considering 421 Misdirected Request as the default instead of 403. RFC 9110 §15.5.20 defines it as:

The request was directed at a server that is unable or unwilling to produce an authoritative response for the target URI.

Which fits the "wrong host for this server" semantics better than 403 ("understood, but refused due to permissions"). Cloudflare, Fastly and other CDNs use 421 for exactly this case.

Either response is defensible, but 403 is currently presented here as the only correct option, which I don't think it is. At minimum, 421 deserves a mention as an alternative.

63 changes: 63 additions & 0 deletions middleware/hostauthorization/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package hostauthorization

import (
"errors"

"github.com/gofiber/fiber/v3"
)

// ErrForbiddenHost is returned when the Host header does not match any allowed host.
var ErrForbiddenHost = errors.New("hostauthorization: forbidden host")

// Config defines the config for the host authorization middleware.
type Config struct {
// Next defines a function to skip this middleware when returned true.
// Use this to exclude health check endpoints or other paths from host validation.
//
// Optional. Default: nil
Next func(c fiber.Ctx) bool

// AllowedHostsFunc is a dynamic validator called when static AllowedHosts
// don't match. Receives the hostname (port stripped, lowercased).
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated
// Return true to allow.
//
// Optional. Default: nil
AllowedHostsFunc func(host string) bool

// ErrorHandler is called when a request is rejected.
// Receives ErrForbiddenHost as the error.
//
// Optional. Default: returns 403 Forbidden with "Forbidden" body.
ErrorHandler fiber.ErrorHandler

// AllowedHosts is the list of permitted host values.
// Supports three match types:
// - Exact: "api.myapp.com"
// - Subdomain: ".myapp.com" (leading dot matches any subdomain, NOT the bare domain)
// - CIDR: "10.0.0.0/8" (matches hosts that are IPs in the range)
//
// Required if AllowedHostsFunc is nil.
AllowedHosts []string
}
Comment thread
mutantkeyboard marked this conversation as resolved.

// ConfigDefault is the default config.
var ConfigDefault = Config{}

func configDefault(config ...Config) Config {
cfg := ConfigDefault
if len(config) > 0 {
cfg = config[0]
}

if len(cfg.AllowedHosts) == 0 && cfg.AllowedHostsFunc == nil {
panic("hostauthorization: AllowedHosts or AllowedHostsFunc is required")
}

if cfg.ErrorHandler == nil {
cfg.ErrorHandler = func(c fiber.Ctx, _ error) error {
return c.Status(fiber.StatusForbidden).SendString("Forbidden")
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated
}
}

return cfg
}
Comment thread
mutantkeyboard marked this conversation as resolved.
122 changes: 122 additions & 0 deletions middleware/hostauthorization/hostauthorization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package hostauthorization

import (
"net"
"strings"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/utils/v2"
utilsstrings "github.com/gofiber/utils/v2/strings"
)

// parsedHosts holds the pre-parsed host matching structures.
type parsedHosts struct {
exact map[string]bool
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated
wildcardSuffixes []string
cidrNets []*net.IPNet
}

// parseAllowedHosts categorizes AllowedHosts into exact, wildcard, and CIDR groups.
// Panics on invalid CIDR entries.
func parseAllowedHosts(hosts []string) parsedHosts {
Comment thread
mutantkeyboard marked this conversation as resolved.
parsed := parsedHosts{
exact: make(map[string]bool, len(hosts)),
}

for _, h := range hosts {
h = utils.TrimSpace(h)
if h == "" {
continue
}
h = normalizeHost(h)
if h == "" {
continue
}

switch {
case strings.Contains(h, "/"):
Comment thread
mutantkeyboard marked this conversation as resolved.
Outdated
// CIDR range
_, cidr, err := net.ParseCIDR(h)
if err != nil {
panic("hostauthorization: invalid CIDR: " + h)
}
parsed.cidrNets = append(parsed.cidrNets, cidr)

case strings.HasPrefix(h, "."):
// Subdomain wildcard — store with leading dot to avoid allocation in hot path
parsed.wildcardSuffixes = append(parsed.wildcardSuffixes, h)

default:
// Exact match
parsed.exact[h] = true
}
}

return parsed
}

// normalizeHost normalizes a hostname (already port-stripped by c.Hostname()).
// Strips trailing dot, IPv6 brackets, and lowercases.
func normalizeHost(host string) string {
// Strip IPv6 brackets
host = strings.TrimPrefix(host, "[")
host = strings.TrimSuffix(host, "]")

// Strip trailing dot (FQDN normalization)
host = strings.TrimSuffix(host, ".")

return utilsstrings.ToLower(host)
}
Comment thread
mutantkeyboard marked this conversation as resolved.
Outdated

// matchHost checks if the given host matches any of the parsed allowed hosts.
func matchHost(host string, parsed parsedHosts, allowedHostsFunc func(string) bool) bool {
// Exact match
if parsed.exact[host] {
return true
}

// Subdomain wildcard: ".myapp.com" matches "api.myapp.com" but NOT "myapp.com"
for _, suffix := range parsed.wildcardSuffixes {
if strings.HasSuffix(host, suffix) {
return true
}
}

// CIDR match: parse host as IP and check against CIDR ranges
if len(parsed.cidrNets) > 0 {
if ip := net.ParseIP(host); ip != nil {
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated
for _, cidr := range parsed.cidrNets {
if cidr.Contains(ip) {
return true
}
}
}
}

// Dynamic validator fallback
if allowedHostsFunc != nil {
Comment thread
mutantkeyboard marked this conversation as resolved.
Outdated
return allowedHostsFunc(host)
}

return false
}

// New creates a new host authorization middleware handler.
func New(config ...Config) fiber.Handler {
cfg := configDefault(config...)
parsed := parseAllowedHosts(cfg.AllowedHosts)

return func(c fiber.Ctx) error {
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}

host := normalizeHost(c.Hostname())

if matchHost(host, parsed, cfg.AllowedHostsFunc) {
return c.Next()
}

return cfg.ErrorHandler(c, ErrForbiddenHost)
}
}
Loading
Loading