Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
90 changes: 84 additions & 6 deletions middleware/hostauthorization/hostauthorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,19 @@ func validateHostLength(host string) {
func normalizeHost(host string) string {
// Fast path for plain hostnames — avoids net.SplitHostPort's error allocation.
if host != "" && host[0] != '[' && strings.IndexByte(host, ':') < 0 {
host = strings.TrimSuffix(host, ".")
host = utils.TrimRight(host, '.')
host = utilsstrings.ToLower(host)
return toPunycode(host)
}

if h, _, err := net.SplitHostPort(host); err == nil {
host = h
} else {
host = strings.TrimPrefix(host, "[")
host = strings.TrimSuffix(host, "]")
host = utils.TrimLeft(host, '[')
host = utils.TrimRight(host, ']')
}

host = strings.TrimSuffix(host, ".")
host = utils.TrimRight(host, '.')
host = utilsstrings.ToLower(host)
return toPunycode(host)
}
Expand All @@ -115,6 +115,84 @@ func toPunycode(host string) string {
return host
}

func parseNormalizedAuthority(authority string) (string, bool) {
authority = utils.TrimSpace(authority)
if authority == "" {
return "", false
}

host := authority
if authority[0] == '[' {
idx := -1
for i := 1; i < len(authority); i++ {
switch authority[i] {
case '@', '[':
return "", false
case ']':
idx = i
i = len(authority)
}
}
if idx <= 1 {
return "", false
}

host = authority[1:idx]
rest := authority[idx+1:]
if rest != "" {
if rest[0] != ':' {
return "", false
}
if !isValidPort(rest[1:]) {
return "", false
}
}
Comment thread
ReneWerner87 marked this conversation as resolved.
} else {
colonIdx := -1
for i := 0; i < len(authority); i++ {
switch authority[i] {
case '@', '[', ']':
return "", false
case ':':
if colonIdx != -1 {
return "", false
}
colonIdx = i
}
}

if colonIdx != -1 {
host = authority[:colonIdx]
if !isValidPort(authority[colonIdx+1:]) {
return "", false
}
}
}

host = normalizeHost(host)
if host == "" {
return "", false
}

return host, true
}
Comment thread
gaby marked this conversation as resolved.

func isValidPort(raw string) bool {
if raw == "" || len(raw) > 5 {
return false
}

var port int
for i := 0; i < len(raw); i++ {
if raw[i] < '0' || raw[i] > '9' {
return false
}
port = port*10 + int(raw[i]-'0')
}

return port <= 65535
}
Comment thread
gaby marked this conversation as resolved.

func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 0x80 {
Expand Down Expand Up @@ -154,8 +232,8 @@ func New(config ...Config) fiber.Handler {
return c.Next()
}

host := normalizeHost(c.Hostname())
if host == "" {
host, ok := parseNormalizedAuthority(c.Host())
if !ok {
return cfg.ErrorHandler(c, ErrForbiddenHost)
}

Expand Down
60 changes: 60 additions & 0 deletions middleware/hostauthorization/hostauthorization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,39 @@ func Test_NormalizeHost(t *testing.T) {
}
}

func Test_ParseNormalizedAuthority(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
expected string
expectOK bool
}{
{name: "plain host", input: "example.com", expected: "example.com", expectOK: true},
{name: "host with valid port", input: "example.com:8080", expected: "example.com", expectOK: true},
{name: "ipv6 with port", input: "[::1]:443", expected: "::1", expectOK: true},
{name: "ipv6 without port", input: "[::1]", expected: "::1", expectOK: true},
{name: "empty port", input: "example.com:", expectOK: false},
{name: "signed port", input: "example.com:+80", expectOK: false},
{name: "port out of range", input: "example.com:65536", expectOK: false},
{name: "userinfo style authority", input: "allowed.com:443@attacker.example", expectOK: false},
{name: "invalid port syntax", input: "allowed.com:http", expectOK: false},
{name: "bare ipv6", input: "::1", expectOK: false},
{name: "malformed bracket", input: "[::1", expectOK: false},
{name: "extra data after bracket", input: "[::1]extra", expectOK: false},
}
Comment thread
gaby marked this conversation as resolved.

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
host, ok := parseNormalizedAuthority(tt.input)
require.Equal(t, tt.expectOK, ok)
require.Equal(t, tt.expected, host)
})
}
}

func Test_ParseAllowedHosts_SkipsBlankEntries(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -645,6 +678,33 @@ func Test_HostAuthorization_XForwardedHost_TrustProxy_Rejected(t *testing.T) {
require.Equal(t, fiber.StatusForbidden, resp.StatusCode)
}

func Test_HostAuthorization_XForwardedHost_TrustProxy_RejectsMalformedAuthority(t *testing.T) {
t.Parallel()

app := fiber.New(fiber.Config{
TrustProxy: true,
TrustProxyConfig: fiber.TrustProxyConfig{
Proxies: []string{"0.0.0.0"},
},
})

app.Use(New(Config{
AllowedHosts: []string{"allowed.com"},
}))

app.Get("/", func(c fiber.Ctx) error {
return c.SendString("OK")
})

req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
req.Host = "proxy.internal"
req.Header.Set("X-Forwarded-Host", "allowed.com:443@attacker.example")

resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, fiber.StatusForbidden, resp.StatusCode)
}

func Test_HostAuthorization_XForwardedHost_NoTrustProxy(t *testing.T) {
t.Parallel()

Expand Down