Skip to content

🔥 feat: Host auth middleware #4199

Open
mutantkeyboard wants to merge 27 commits intogofiber:mainfrom
mutantkeyboard:host_auth_middleware
Open

🔥 feat: Host auth middleware #4199
mutantkeyboard wants to merge 27 commits intogofiber:mainfrom
mutantkeyboard:host_auth_middleware

Conversation

@mutantkeyboard
Copy link
Copy Markdown
Contributor

@mutantkeyboard mutantkeyboard commented Apr 8, 2026

Description

Add a new hostauthorization middleware that validates the incoming Host header against a configurable allowlist, rejecting unauthorized hosts with 403 Forbidden. This protects against DNS rebinding attacks
where an attacker-controlled domain resolves to the application's internal IP, causing browsers to send requests with a malicious Host header.

The middleware supports exact host matching, subdomain wildcards (.myapp.com), and CIDR ranges (10.0.0.0/8), with an optional dynamic validator function for runtime host resolution. It complements the Domain()
router (#4100) — Domain() handles host-based routing while hostauthorization acts as a security gate.

Fixes #4148

Changes introduced

  • Benchmarks: 3 benchmarks included (exact match, CIDR, mixed rules). All ~3,200 ns/op with 25 allocs on Apple M5.
  • Documentation Update: Added docs/middleware/hostauthorization.md with full usage guide, config reference, matching semantics, normalization rules, proxy support, and RFC compliance notes.
  • Changelog/What's New: New hostauthorization middleware — DNS rebinding protection via Host header validation with exact match, subdomain wildcard, and CIDR support.
  • Migration Guide: N/A — new middleware, no breaking changes.
  • API Alignment with Express: Modeled after host-validation (closest Express ecosystem equivalent). AllowedHosts/AllowedHostsFunc mirrors the AllowOrigins/AllowOriginsFunc pattern from the existing cors
    middleware. Next, ErrorHandler follow established Fiber conventions.
  • API Longevity: Minimal API surface (4 config fields). String-based allowlists and callback escape hatches — same patterns Rails has used since Rails 6 (2019) without breaking changes. No external
    dependencies, no state, no caching.
  • Examples: Usage examples covering basic protection, subdomain wildcards, CIDR ranges, health check exclusion, dynamic validation, custom error responses, and combined usage with Domain() router.

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)
  • Enhancement (improvement to existing features and functionality)
  • Documentation update (changes to documentation)
  • Performance improvement (non-breaking change which improves efficiency)
  • Code consistency (non-breaking change which improves code reliability and robustness)

Checklist

Before you submit your pull request, please make sure you meet these requirements:

  • Followed the inspiration of the Express.js framework for new functionalities, making them similar in usage.
  • Conducted a self-review of the code and provided comments for complex or critical parts.
  • Updated the documentation in the /docs/ directory for Fiber's documentation.
  • Added or updated unit tests to validate the effectiveness of the changes or new features.
  • Ensured that new and existing unit tests pass locally with the changes.
  • Verified that any new dependencies are essential and have been agreed upon by the maintainers/community.
  • Aimed for optimal performance with minimal allocations in the new code.
  • Provided benchmarks for the new code to analyze and improve upon.

Commit formatting

Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: CONTRIBUTING.md

@mutantkeyboard mutantkeyboard requested a review from a team as a code owner April 8, 2026 09:04
@welcome
Copy link
Copy Markdown

welcome Bot commented Apr 8, 2026

Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new hostauthorization middleware: implementation, configuration, documentation, README index entry, and comprehensive tests/benchmarks that enforce Host header allowlisting (exact, leading-dot wildcard, CIDR, dynamic validator) and rejection via a configurable error handler.

Changes

Cohort / File(s) Summary
Docs / README
/.github/README.md, docs/middleware/hostauthorization.md
Added README index row and a full middleware docs page describing func New(config ...Config) fiber.Handler, config fields, matching semantics (exact, leading-dot wildcard, CIDR), host normalization, proxy behavior, examples, and error handling.
Config
middleware/hostauthorization/config.go
New Config type (Next, AllowedHosts, AllowedHostsFunc, ErrorHandler), exported sentinel ErrForbiddenHost, ConfigDefault, and runtime validation/default injection (panics if neither AllowedHosts nor AllowedHostsFunc set).
Implementation
middleware/hostauthorization/hostauthorization.go
Added New(config ...Config) fiber.Handler that pre-parses AllowedHosts into exact names, leading-dot wildcard suffixes, and CIDR ranges (invalid CIDRs panic). Runtime: Next skip, hostname normalization (strip port/IPv6 brackets/trailing dot, lowercase), exact/wildcard/CIDR matching, fallback to AllowedHostsFunc, deny via ErrorHandler with ErrForbiddenHost.
Tests & Benchmarks
middleware/hostauthorization/hostauthorization_test.go
Extensive unit, integration, and benchmark tests covering config validation and panics, host normalization, exact/wildcard/CIDR/IP matches (v4/v6), dynamic allow-fn, Next bypass, TrustProxy + X-Forwarded-Host behavior, custom error handler, and performance microbenchmarks and end-to-end benchmarks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client as Client
  participant App as Fiber App
  participant HA as HostAuthorization
  participant Store as AllowedHostsStore
  participant Func as AllowedHostsFunc
  participant Err as ErrorHandler

  Client->>App: HTTP request (Host header)
  App->>HA: invoke middleware
  HA->>HA: normalize Host (strip port/brackets/trailing dot, lowercase)
  HA->>Store: check exact / wildcard / CIDR matches
  alt matched
    Store-->>HA: allowed
    HA->>App: c.Next() -> downstream handler
  else not matched
    HA->>Func: if configured, call AllowedHostsFunc(host)
    alt func returns true
      Func-->>HA: allowed
      HA->>App: c.Next() -> downstream handler
    else
      HA->>Err: call ErrorHandler(c, ErrForbiddenHost)
      Err-->>Client: 403 Forbidden (or custom response)
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • ReneWerner87
  • sixcolors
  • efectn

Poem

🐇 I hop through headers, trim brackets and dots,
I check wildcards, CIDRs, and exact little spots,
If hosts are true I let them pass on through,
If naughty hosts knock, I say “403, shoo!” 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title uses an emoji and vague phrasing that doesn't clearly convey the specific feature being added. Consider a more descriptive title like 'Add hostauthorization middleware for DNS rebinding protection' that conveys the feature without relying on emoji.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description comprehensively covers all required template sections including problem statement, changes introduced, type of change, and a complete checklist with detailed explanations.
Linked Issues check ✅ Passed The pull request fully addresses all requirements from issue #4148: DNS rebinding protection via Host header validation, exact/wildcard/CIDR matching, port stripping, X-Forwarded-Host support, 403 response codes, and stable minimal API.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the hostauthorization middleware feature: configuration, implementation, documentation, tests, and README entry. No extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ReneWerner87 ReneWerner87 added this to v3 Apr 8, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Apr 8, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.21%. Comparing base (80c5c88) to head (3334763).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4199      +/-   ##
==========================================
+ Coverage   91.15%   91.21%   +0.06%     
==========================================
  Files         123      125       +2     
  Lines       12076    12137      +61     
==========================================
+ Hits        11008    11071      +63     
+ Misses        669      668       -1     
+ Partials      399      398       -1     
Flag Coverage Δ
unittests 91.21% <100.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new hostauthorization middleware for Fiber v3, designed to protect against DNS rebinding attacks by validating the Host header against an allowlist of exact domains, subdomain wildcards, and CIDR ranges. The implementation includes comprehensive documentation and tests. Feedback focuses on making the configuration initialization more idiomatic for the Fiber framework and optimizing the performance of subdomain matching by pre-storing wildcard suffixes with their leading dots to avoid allocations during request processing.

Comment thread middleware/hostauthorization/config.go
Comment thread middleware/hostauthorization/hostauthorization.go Outdated
Comment thread middleware/hostauthorization/hostauthorization.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/middleware/hostauthorization.md`:
- Around line 63-79: The CIDR Ranges doc misleadingly describes source-IP
filtering; update the wording to clarify that hostauthorization checks the
request Host header against AllowedHosts (not the client's IP). Edit the
examples and comments around hostauthorization.New and hostauthorization.Config
AllowedHosts entries to say things like "allowed Host header IP range" or "Host
header value" instead of "internal network" or "respond to requests from known
IP ranges" so readers understand this matches Host header strings/CIDR-like IPs
in the Host header only.

In `@middleware/hostauthorization/config.go`:
- Around line 13-41: Add the missing Exclude hook to the public Config by
declaring a field Exclude func(c fiber.Ctx) bool (optional, default nil) next to
Next and ErrorHandler in the Config struct; update the middleware code path that
currently checks Next to also call Exclude (both should be evaluated before any
host validation/AllowedHosts/AllowedHostsFunc logic) and treat a true return
from Exclude as a signal to skip host validation and not invoke ErrorHandler.
Ensure the field name is exactly Exclude and that callers can provide it the
same way as Next/ErrorHandler.

In `@middleware/hostauthorization/hostauthorization.go`:
- Around line 24-45: Normalize each configured host string using normalizeHost()
before any checks and storage: call normalizeHost(h) (after trimming) and skip
if it becomes empty; for CIDR parsing still use the normalized value for
net.ParseCIDR and append the result to parsed.cidrNets; for wildcard entries
(case strings.HasPrefix(h, ".")) store the suffix without the leading dot from
the normalized value into parsed.wildcardSuffixes; for exact matches insert the
normalized host into parsed.exact. Ensure all references use the normalized host
variable so comparisons match request hosts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: abe37dce-637a-44c8-80d2-c96bfa96bba2

📥 Commits

Reviewing files that changed from the base of the PR and between 631e77d and 4b45ec9.

📒 Files selected for processing (5)
  • .github/README.md
  • docs/middleware/hostauthorization.md
  • middleware/hostauthorization/config.go
  • middleware/hostauthorization/hostauthorization.go
  • middleware/hostauthorization/hostauthorization_test.go

Comment thread docs/middleware/hostauthorization.md
Comment thread middleware/hostauthorization/config.go
Comment thread middleware/hostauthorization/hostauthorization.go
@ReneWerner87
Copy link
Copy Markdown
Member

Thanks for the contribution @mutantkeyboard , DNS rebinding protection is a valid concern and I appreciate the thorough work on docs and tests.

Before we move forward, I want to raise a design question: should this be a standalone middleware or an extension of the existing Domain() router?

Domain() already matches requests by host pattern at the routing level. Adding an allowlist/deny semantic there (e.g. app.SetAllowedHosts(...)) would:

  • Avoid the overhead of an extra handler in the middleware chain on every request
  • Make the relationship between host-based routing and host-based security explicit
  • Keep host logic in one place instead of two separate systems

If we do keep it as a standalone middleware, a few things need addressing:

Performance

  • ~3,200 ns/op with 25 allocations is too high for a middleware that runs on every request. The core of this is a map lookup + a few string comparisons, that should be near-zero alloc. The wildcard suffixes should be stored with the leading dot at init time (as already flagged in reviews), and normalizeHost() should be applied to config values at init, not just at request time.
    You can use the utils package or existing functions from the other middlewares

API / Consistency

  • Missing Exclude field , our other middlewares expose this hook, and this one should too for consistency.
  • normalizeHost() not applied to configured hosts during init - this can lead to subtle mismatches (e.g. MyApp.com in config vs myapp.com at runtime).

Documentation

  • The CIDR section reads like source-IP filtering ("respond to requests from known IP ranges"). This needs to clearly state it matches the Host header value, not the client IP. As written, someone could deploy this thinking they have IP-based access control, which
    would be a security misunderstanding.

Scope question

  • CIDR matching on the Host header is a fairly niche use case; in practice, Host headers rarely contain raw IPs outside of internal tooling. Is the complexity worth it, or would exact match + subdomain wildcards cover 99% of real-world usage?

I'd like to hear your thoughts on the Domain() integration angle before we iterate further on the middleware approach. Either way, the normalization logic and test coverage you've built here is solid and would carry over.

@mutantkeyboard
Copy link
Copy Markdown
Contributor Author

Hey @ReneWerner87, thank you for the review.

Regarding Domain() integration

I'm genuinely curious what did you have in mind there. My original inspiration for this was brannondorsey/host-validation, which is explicitly a standalone
middleware for DNS rebinding protection on Express. The pattern just felt like a natural fit here too — Domain() is opt-in per route and calls c.Next() on non-match, so it doesn't have global reject
semantics. CIDR and leading-dot wildcards would also need special-casing since parseDomainPattern doesn't support either today. That said, if you see a clean way to integrate it without blurring routing and
access control, I'm open to it.

Performance

The 3,200 ns/op and 25 allocations are from app.Test()

  • that overhead belongs to Fiber's request pipeline, not this middleware per se. The actual per-request cost is a map lookup, a couple of string ops, and one real alloc: the "."+suffix concat in the wildcard path. Fixing that (storing the leading dot at init) plus switching to utilsstrings.ToLower in normalizeHost gets the common path to near-zero alloc.
    I'm more than happy to add a lower-level benchmark that isolates just the matching logic so the numbers are less misleading.

normalizeHost at init

This one is actually a good catch - config entries with trailing dots won't match runtime-normalized hosts. Easy fix.

CIDR docs

This is actually a fair point. I can fix the wording to make clear it's matching the Host header value, not the client IP.

But regarding the overall architecture, I can integrate it into the existing Domain() or leave it as a separate middlware. It's really up to you and @gaby

WDYT?

@ReneWerner87
Copy link
Copy Markdown
Member

ok

Go ahead with the leading-dot fix at init + normalizeHost on config entries, that alone should clean up the hot path nicely. And yes, please add an isolated benchmark for just the matching logic so we have real numbers for the middleware cost without app.Test() noise.
Fix the CIDR docs wording as discussed.
On CIDR scope: keep it for now. It's not a lot of extra code and internal tooling with IP-based hosts is a real enough use case. We can always revisit if it causes maintenance headaches down the line.

pls also try the utils functions for the better performance

@gaby gaby changed the title 🔥 Feature: Host auth middleware 🔥 feat: Host auth middleware Apr 11, 2026
@gaby
Copy link
Copy Markdown
Member

gaby commented Apr 11, 2026

@mutantkeyboard Test failures are now related to the new middleware.

@mutantkeyboard
Copy link
Copy Markdown
Contributor Author

mutantkeyboard commented Apr 13, 2026

@gaby - I will fix that today

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
middleware/hostauthorization/hostauthorization_test.go (1)

275-279: ⚠️ Potential issue | 🟡 Minor

Tighten the empty-host error branch assertion.

On Lines 276-277, returning on any error can make this test pass for unrelated failures. Assert the expected Host-related failure before returning.

Suggested fix
 	resp, err := app.Test(req)
 	if err != nil {
+		require.ErrorContains(t, err, "Host")
 		return
 	}
 	require.Equal(t, fiber.StatusForbidden, resp.StatusCode)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@middleware/hostauthorization/hostauthorization_test.go` around lines 275 -
279, The test currently returns on any non-nil error from app.Test(req), which
can mask unrelated failures; modify the app.Test(req) error branch to assert the
error is the expected host-related failure (e.g., using require.ErrorContains(t,
err, "host" or "empty host") or an equivalent check against the Host validation
error) before returning, then keep the existing require.Equal(t,
fiber.StatusForbidden, resp.StatusCode) assertion for the response path;
reference the app.Test call and the err variable when adding the
failure-specific assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@middleware/hostauthorization/hostauthorization_test.go`:
- Around line 455-457: The test currently calls require.ErrorIs inside the
ErrorHandler closure; move that assertion out of the callback by making
ErrorHandler side-effect-free (return the custom status/body only). Capture the
error returned by app.Test or assign the error to an outer variable inside the
closure (e.g., var handlerErr error; in ErrorHandler set handlerErr = err) and
then after calling app.Test perform require.ErrorIs(t, handlerErr,
ErrForbiddenHost) and any response assertions; reference ErrorHandler,
require.ErrorIs, ErrForbiddenHost, and app.Test when making the change.

---

Duplicate comments:
In `@middleware/hostauthorization/hostauthorization_test.go`:
- Around line 275-279: The test currently returns on any non-nil error from
app.Test(req), which can mask unrelated failures; modify the app.Test(req) error
branch to assert the error is the expected host-related failure (e.g., using
require.ErrorContains(t, err, "host" or "empty host") or an equivalent check
against the Host validation error) before returning, then keep the existing
require.Equal(t, fiber.StatusForbidden, resp.StatusCode) assertion for the
response path; reference the app.Test call and the err variable when adding the
failure-specific assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2b53a8e5-b6e4-4ba9-a4e6-fa3bc158bd36

📥 Commits

Reviewing files that changed from the base of the PR and between b542c02 and 5f7a435.

📒 Files selected for processing (2)
  • middleware/hostauthorization/hostauthorization.go
  • middleware/hostauthorization/hostauthorization_test.go

Comment thread middleware/hostauthorization/hostauthorization_test.go
@mutantkeyboard
Copy link
Copy Markdown
Contributor Author

@gaby - this is now fixed. 💪💪

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Given the current implementation and the distinct purposes, I recommend keeping hostauthorization as a separate middleware rather than integrating it directly into the Domain() router or Ctx helpers.

Here's why:

  • Separation of Concerns: The hostauthorization middleware serves a security function (DNS rebinding protection), while Domain() is primarily for routing. Keeping these concerns separate leads to a cleaner, more modular architecture. As the PR description states, hostauthorization acts as a "security gate" and Domain() as a "routing layer."
  • Flexibility: A standalone middleware can be applied globally, to specific groups, or conditionally, offering greater flexibility to users regardless of their routing strategy. Tying it to Domain() might limit its applicability.
  • Domain() Limitations: As @mutantkeyboard pointed out in comment [🐞] Found 3 gosec issues: CWE-78 and CWE-242 #5, Domain() is opt-in per route and lacks the global reject semantics needed for a security middleware. Additionally, parseDomainPattern in Domain() does not currently support CIDR ranges or leading-dot wildcards, which are core features of this host authorization middleware. Integrating these would add complexity to the router that might not align with its primary function.
  • Current Review Direction: The recent discussions (e.g., comment Add deprecated alerts, linting, nosec, typo's #6 from @ReneWerner87) have focused on optimizing the standalone middleware's performance and refining its documentation, implicitly affirming the current design choice.

Therefore, maintaining it as a dedicated middleware seems to be the most robust and flexible approach for this security feature.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new hostauthorization middleware to validate the incoming Host header against an allowlist to mitigate DNS rebinding / Host-header attacks, with support for exact hosts, subdomain wildcards, CIDR ranges, and an optional dynamic validator.

Changes:

  • Introduces middleware/hostauthorization with config, parsing/normalization, and request-time host matching.
  • Adds a comprehensive test suite plus benchmarks for matching and end-to-end middleware behavior.
  • Adds end-user documentation and lists the middleware in .github/README.md.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
middleware/hostauthorization/hostauthorization.go Implements allowlist parsing + host matching and the middleware handler.
middleware/hostauthorization/config.go Defines middleware configuration, defaults, and exported error.
middleware/hostauthorization/hostauthorization_test.go Adds unit/integration tests and benchmarks for matching behavior and proxy semantics.
docs/middleware/hostauthorization.md Documents usage, matching/normalization rules, and proxy/RFC notes.
.github/README.md Adds the new middleware to the project’s middleware list.

Comment thread middleware/hostauthorization/hostauthorization.go Outdated
Comment thread middleware/hostauthorization/hostauthorization.go Outdated
Comment thread middleware/hostauthorization/hostauthorization.go Outdated
@mutantkeyboard mutantkeyboard requested a review from gaby April 20, 2026 09:01

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


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

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.

continue
}

if hostIP, cidr, err := net.ParseCIDR(h); err == nil {
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 all duplicated logic from TrustedProxyConfig handling. Maybe we should make that a public function that can be used from here

Copy link
Copy Markdown
Member

@ReneWerner87 ReneWerner87 left a comment

Choose a reason for hiding this comment

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

Going through the latest version of the branch (3334763), here is a follow-up on top of what's already been discussed. Most of the high-priority code-level findings are in inline comments. The points below are either architectural (don't fit a single line) or reinforcements of things @gaby and others already raised that I think deserve a more concrete recommendation.

IDN / Punycode

Browsers always send internationalised domains as Punycode in the Host header: münchen.example.com arrives as xn--mnchen-3ya.example.com. With the current implementation, configuring "münchen.example.com" in AllowedHosts will never match a real browser request.

There's already precedent in the codebase (req.go, Subdomains()):

if strings.Contains(host, "xn--") {
    if u, err := idna.Lookup.ToUnicode(host); err == nil {
        host = utilsstrings.ToLower(u)
    }
}

Suggest normalising both sides (config entries and request hostnames) consistently, ideally to ASCII via idna.Lookup.ToASCII since browsers consistently use Punycode and ASCII comparisons stay alloc-free. At minimum this should be a documented "Known limitations" entry.

TrustedProxyConfig consolidation (reinforcing @gaby)

The CIDR parse and contains-check logic is essentially identical to app.handleTrustedProxy. A shared helper would benefit both modules and keep behaviour consistent (the canonical-CIDR panic exists here but not in handleTrustedProxy). Worth doing, even if as a follow-up PR.

Subdomain wildcard semantics (reinforcing @gaby)

The leading-dot convention that excludes the apex is ergonomically weak and forces two entries for the most common case. Worth deciding now since changing it later is breaking. My preference would be *.myapp.com for subdomains-only and myapp.com for apex (familiar glob syntax), but implicit apex inclusion would also be defensible.

Test gaps

A few that would be worth adding alongside the inline points:

  • AllowedHostsFunc as fallback (with call-counter assertion, see L80 inline)

  • IPv6 + port in AllowedHosts config entry ("[::1]:8080"). IPv4 is covered, IPv6 isn't.

  • Concurrent requests with -race. The implementation is read-only after init but the contract should be locked in.

  • A scaling benchmark with many wildcards (e.g. 100 entries) so we know when the linear HasSuffix scan starts mattering.

  • A FuzzNormalizeHost target. Trivial to add and standard hygiene for any header parser:

    func FuzzNormalizeHost(f *testing.F) {
        f.Add("example.com")
        f.Add("[::1]:8080")
        f.Add(".myapp.com")
        f.Fuzz(func(t *testing.T, input string) {
            _ = normalizeHost(input)
        })
    }

Sentinel error stability

ErrForbiddenHost is exported, so users can do errors.Is(...). A single test asserting the exact Error() string locks in the textual contract for those who match on the message. Minor but useful.

Scope question

CIDR matching accounts for the bulk of the surface area in this PR (parsing, the non-canonical panic, documentation confusion with TrustedProxyConfig, and the duplicated logic). For an initial merge, dropping CIDR and shipping with exact + subdomain wildcards only would make this a tighter, more obviously-correct module. CIDR can land in a follow-up where it's properly consolidated with the trusted-proxy infrastructure.

Not a blocker, just a thought given how much of the review feedback (mine and @gaby's) orbits the CIDR feature.


Overall really nice work on the iterations so far. The split between matchHost micro-benchmarks and the full HTTP-pipeline benchmarks is a great addition for isolating actual middleware cost.


// parsedHosts holds the pre-parsed host matching structures.
type parsedHosts struct {
exact map[string]bool
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.

Suggest using map[string]struct{} instead of map[string]bool. The convention in the rest of the codebase (e.g. TrustProxyConfig.ips map[string]struct{} in app.go) uses zero-byte values for set semantics. make modernize will likely flag this too.

continue
}

if hostIP, cidr, err := net.ParseCIDR(h); err == nil {
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.

Silent CIDR typo footgun: a common mistake like "10.0.0/8" (missing octet) silently falls through to an exact-match entry that can never match. Test_ConfigInvalidCIDRTreatedAsExact documents this, but in production it's dangerous because the operator thinks their internal range is allowed when it isn't.

We currently panic on non-canonical CIDRs (e.g. 10.0.0.5/8) but accept syntactically broken ones silently. That asymmetry is confusing. Suggest mirroring app.handleTrustedProxy: if the string contains /, always treat it as a CIDR attempt and either panic or log.Warnf on parse failure so the misconfiguration surfaces.

func normalizeHost(host string) string {
// Strip port if present. net.SplitHostPort handles both "host:port" and
// "[::1]:port" forms, and strips IPv6 brackets as a side effect.
if h, _, err := net.SplitHostPort(host); err == nil {
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.

Hot-path allocation: c.Hostname() already strips the port (via parseAddr in helpers.go), so on real request hostnames net.SplitHostPort always hits the error path, and stdlib allocates a *AddrError for every error. That's one allocation per request.

Suggest a fast path:

if len(host) > 0 && host[0] != '[' && strings.IndexByte(host, ':') < 0 {
    host = strings.TrimSuffix(host, ".")
    return utilsstrings.ToLower(host)
}
// existing logic for the rare case

Worth a before/after benchmark with the existing Benchmark_matchHost_* to confirm the alloc drops to 0.

// matchHost checks if the given host matches any of the parsed allowed hosts.
func matchHost(host string, parsed parsedHosts, allowedHostsFunc func(string) bool) bool {
// Dynamic validator — checked first so it can override static rules
if allowedHostsFunc != nil && allowedHostsFunc(host) {
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 contradicts both the docs and the stated CORS pattern.

The middleware docs say:

AllowedHostsFunc is only called when static AllowedHosts don't match

And the PR description claims this mirrors the CORS AllowOriginsFunc pattern, where the func is explicitly a fallback (see middleware/cors/cors.go):

// Run AllowOriginsFunc if the logic for handling the value in 'AllowOrigins'
// does not result in allowOrigin being set.
if allowOrigin == "" && cfg.AllowOriginsFunc != nil && cfg.AllowOriginsFunc(originHeaderRaw) {

Performance-wise this matters too: a user-supplied AllowedHostsFunc is the most expensive lookup (the docs even suggest "Look up tenant domains from database"), so calling it on every request before the static map lookup is exactly the wrong default.

Suggest reordering to: exact → wildcard → CIDR → AllowedHostsFunc. That matches the documented behaviour and the CORS convention.

A counter-based test would prevent regressions:

called := 0
fn := func(host string) bool { called++; return false }
// request that matches a static AllowedHosts entry
require.Equal(t, 0, called, "AllowedHostsFunc should not be called when static hosts match")


// CIDR match: parse host as IP and check against CIDR ranges
if len(parsed.cidrNets) > 0 {
if ip := net.ParseIP(host); ip != nil {
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.

Hot-path allocation: net.ParseIP runs on every request as soon as any CIDR is configured, even for hostnames like api.myapp.com that obviously can't be IPs. ParseIP allocates internally.

A cheap pre-check (first byte must be a digit or :) would skip ParseIP for the typical mixed exact + CIDR config where most requests carry hostnames, not raw IPs:

if len(parsed.cidrNets) > 0 && len(host) > 0 {
    if c := host[0]; (c >= '0' && c <= '9') || c == ':' {
        if ip := net.ParseIP(host); ip != nil {
            // ...
        }
    }
}


if cfg.ErrorHandler == nil {
cfg.ErrorHandler = func(c fiber.Ctx, _ error) error {
return c.Status(fiber.StatusForbidden).SendString("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.

The hardcoded "Forbidden" string drifts from the rest of the framework. Other middleware uses utils.StatusMessage(fiber.StatusForbidden) or returns fiber.NewError(fiber.StatusForbidden) so the response body stays consistent with however Fiber surfaces status text elsewhere (and survives a future refactor without each middleware needing a touch). Minor, but trivially fixable.


// parseAllowedHosts categorizes AllowedHosts into exact, wildcard, and CIDR groups.
// Panics on invalid CIDR entries.
func parseAllowedHosts(hosts []string) parsedHosts {
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.

RFC 1035 length validation: parseDomainPattern in domain.go enforces total length ≤ 253 chars, per-label length ≤ 63 chars, and a max label count via maxDomainParts. parseAllowedHosts enforces none of these, so a misconfiguration can put unbounded strings into the matching loop on every request.

For consistency with the rest of the framework's hostname handling (and as a cheap defense against accidental DoS from a malformed config), suggest mirroring those bounds here.

// - fasthttp rejects the missing Host at the protocol level → app.Test returns an error
// - Go 1.26+ serializes a default Host value so the request reaches the middleware,
// which rejects it with 403 Forbidden → app.Test returns a response with no error
resp, err := app.Test(req)
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 early return on any non-nil error from app.Test (already flagged by CodeRabbit) makes the test pass for unrelated failures too.

Suggest tightening to assert the error is host-related before returning, e.g.

if err != nil {
    require.ErrorContains(t, err, "Host")
    return
}

Otherwise a future fasthttp change that errors out for a different reason would silently keep this test green.

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


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

@ReneWerner87
Copy link
Copy Markdown
Member

ReneWerner87 commented Apr 27, 2026

@mutantkeyboard pls check the last comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

📝 [Proposal]: Host authorization middleware — DNS rebinding protection via Host header validation

4 participants