Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
224 changes: 224 additions & 0 deletions docs/middleware/hostauthorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
---
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 `*.` prefix matches any subdomain but **not** the bare domain itself:

```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"},
```

### Internationalized Domain Names (IDN)

Browsers always transmit the `Host` header in ASCII (Punycode) form, so IDN entries in `AllowedHosts` are converted to Punycode at startup. You can configure entries in either form — they are equivalent:

```go
AllowedHosts: []string{"münchen.example.com"} // Unicode
AllowedHosts: []string{"xn--mnchen-3ya.example.com"} // Punycode (what the browser sends)
```

Both match an incoming request whose Host header is `xn--mnchen-3ya.example.com`.

### 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

The default response is **403 Forbidden**. **421 Misdirected Request** ([RFC 9110 §15.5.20](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.20)) is a semantically closer choice for "wrong host for this server" — CDNs like Cloudflare and Fastly use it for this case. Either is reasonable; pick one via `ErrorHandler`:

```go
// 403 with a JSON body
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",
})
},
}))

// 421 Misdirected Request — closer to the RFC-defined semantics
app.Use(hostauthorization.New(hostauthorization.Config{
AllowedHosts: []string{"myapp.com"},
ErrorHandler: func(c fiber.Ctx, _ error) error {
return c.SendStatus(fiber.StatusMisdirectedRequest) // 421
},
}))
```

### 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 and subdomain wildcard (`*.example.com`). | `nil` |
| AllowedHostsFunc | `func(string) bool` | Dynamic validator called only when no static AllowedHosts rule matches. Receives the normalized hostname: port stripped, trailing dot removed, IPv6 brackets removed, lowercased, IDN converted to Punycode. | `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, IDN labels in Punycode form
2. **Subdomain wildcard** — `"*.myapp.com"` matches `api.myapp.com` but not `myapp.com`
3. **AllowedHostsFunc** — called only if no static rule matched

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

## Host Normalization

Before matching, both incoming hosts and `AllowedHosts` entries are normalized at startup:

- Port is stripped (`example.com:8080` → `example.com`)
- Trailing dot removed (`example.com.` → `example.com`)
- IPv6 brackets removed (`[::1]` → `::1`)
- Lowercased
- IDN labels converted to ASCII/Punycode (`münchen.example.com` → `xn--mnchen-3ya.example.com`)
- RFC 1035 length limits enforced at startup: ≤253 chars total, ≤63 chars per label (panic on violation)

## Filtering by Client IP

This middleware filters by the `Host` *header*, not by the client's source IP. To restrict access by client IP, use Fiber's [`TrustProxy` / `TrustProxyConfig`](https://docs.gofiber.io/whats_new#trusted-proxies) configuration — those are the correct knobs for IP allowlisting and CIDR ranges of trusted proxies.

## 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.


fasthttp itself is HTTP/1.x only. HTTP/2 support requires an external library (e.g. `fasthttp2`) plugged in via `Server.NextProto`. Those libraries are responsible for mapping the HTTP/2 `:authority` pseudo-header to a Host value before the request reaches Fiber handlers, so the middleware should work transparently once H2 is wired up — but this is the H2 library's responsibility, not fasthttp's or this middleware's.

## 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
- **RFC 1035** — `AllowedHosts` entries are validated against the 253-char total / 63-char per-label limits
- 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.


:::note
**RFC 9110 §15.5.20** defines **421 Misdirected Request** as a semantically closer response for host mismatches ("the request was directed at a server unable or unwilling to produce an authoritative response for the target URI"). CDNs like Cloudflare and Fastly use 421 for this case. To use 421 instead of 403, set a custom `ErrorHandler`:

```go
ErrorHandler: func(c fiber.Ctx, err error) error {
return c.SendStatus(fiber.StatusMisdirectedRequest) // 421
},
```

:::
67 changes: 67 additions & 0 deletions middleware/hostauthorization/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 only when no static
// AllowedHosts rule matches. Receives the normalized hostname: port stripped,
// trailing dot removed, IPv6 brackets removed, lowercased.
// 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.
ErrorHandler fiber.ErrorHandler

// AllowedHosts is the list of permitted host values.
// Supports two match types:
// - Exact: "api.myapp.com"
// - Subdomain: "*.myapp.com" (matches any subdomain, NOT the bare domain — list both for apex+subdomains)
//
// Entries are normalized at startup: port stripped, trailing dot removed,
// lowercased, IDN labels converted to Punycode, RFC 1035 length limits enforced
// (≤253 total / ≤63 per-label).
//
// 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.SendStatus(fiber.StatusForbidden)
}
}

return cfg
}
Comment thread
mutantkeyboard marked this conversation as resolved.
Loading
Loading