Skip to content

http: validate Host header syntax per RFC 3986#7551

Open
pawannn wants to merge 2 commits intocaddyserver:masterfrom
pawannn:fix/invalid-host-header
Open

http: validate Host header syntax per RFC 3986#7551
pawannn wants to merge 2 commits intocaddyserver:masterfrom
pawannn:fix/invalid-host-header

Conversation

@pawannn
Copy link

@pawannn pawannn commented Mar 5, 2026

fixes #7459

Summary

Caddy accepts HTTP requests with invalid Host headers like [], [::1, and [123g::1] and returns 200 OK instead of 400 Bad Request. These values are not valid according to RFC 3986 §3.2.2. This PR adds a validHostHeader() function that checks the Host header and rejects bad values early with a 400 response.

Consider the following test case:

func TestServeHTTP_InvalidHostHeader(t *testing.T) {
    tests := []struct {
        name       string
        host       string
        wantStatus int
    }{
        {"valid host", "example.com", http.StatusOK},
        {"valid IPv6", "[::1]", http.StatusOK},
        {"valid with port", "example.com:80", http.StatusOK},
        {"empty IP-literal", "[]", http.StatusBadRequest},
        {"unclosed bracket", "[::1", http.StatusBadRequest},
        {"invalid IPv6", "[12345]", http.StatusBadRequest},
        {"invalid hex char", "[123g::1]", http.StatusBadRequest},
        {"double colon host", "example.com::80", http.StatusBadRequest},
        {"non-numeric port", "example.com:80a", http.StatusBadRequest},
        {"port out of range", "example.com:99999", http.StatusBadRequest},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            s := &Server{}
            s.primaryHandlerChain = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
                w.WriteHeader(http.StatusOK)
                return nil
            })
            req := httptest.NewRequest(http.MethodGet, "/", nil)
            req.Host = tt.host
            req.Proto = "HTTP/1.1"
            req.ProtoMajor = 1
            req.ProtoMinor = 1
            rr := httptest.NewRecorder()
            err := s.serveHTTP(rr, req)
            gotStatus := rr.Code
            if err != nil {
                if he, ok := err.(HandlerError); ok {
                    gotStatus = he.StatusCode
                }
            }
            if gotStatus != tt.wantStatus {
                t.Errorf("host %q: got status %d, want %d", tt.host, gotStatus, tt.wantStatus)
            }
        })
    }
}
Screenshot 2026-03-06 at 1 45 31 AM

Problem

Caddy only checks that the Host header is not empty for HTTP/1.1 requests. It does not check if the value is actually valid. Because of this, requests with clearly broken Host values are accepted and return 200 OK when they should return 400 Bad Request:

Host: []                      # empty IP-literal
Host: [::1                    # unclosed bracket
Host: [12345]                 # not a valid IPv6 address
Host: [123g::1]               # 'g' is not a valid hex character
Host: [1:2:3:4:5:6:7]        # too few groups
Host: [1:2:3:4:5:6:7:8:9]    # too many groups
Host: [v4.192.10.2.1]        # IPvFuture not permitted
Host: example.com::80         # double colon
Host: example.com:80a         # non-numeric port
Host: example.com:99999       # port out of valid range

Fix

Two helper functions are added to server.go:

  1. validHostHeader: checks that the Host header is well-formed. It ensures IP-literals like [::1] have proper brackets, contain a valid IPv6 address, and have a valid port if one is present.
  2. validPort: checks that a port is a plain number between 0 and 65535.

Both are called inside serveHTTP() right after the existing empty Host check. If the Host value is malformed, the request is rejected with 400 Bad Request, the same way Caddy already handles an empty Host today.

After Fix

Screenshot 2026-03-06 at 1 49 46 AM

Assistance Disclosure

I identified the bug and authored the fix myself, referencing the test cases documented in #7459. Claude (Anthropic) was used to help structure the PR description and suggest test case coverage. All code was reviewed and verified by me for correctness before submission.

Copilot AI review requested due to automatic review settings March 5, 2026 20:39
@CLAassistant
Copy link

CLAassistant commented Mar 5, 2026

CLA assistant check
All committers have signed the CLA.

Copy link

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 RFC 3986–aligned validation for the HTTP Host header so malformed host values are rejected early with 400 Bad Request instead of being served normally.

Changes:

  • Add validHostHeader() + validPort() helpers and call them from serveHTTP().
  • Reject malformed bracketed IP-literals and invalid ports with a 400 HandlerError.
  • Add a table-driven test covering a set of valid/invalid Host header examples.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
modules/caddyhttp/server.go Adds Host header syntax validation and helper functions to reject malformed values with 400.
modules/caddyhttp/server_test.go Adds a table-driven test ensuring invalid Host headers produce 400.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1207 to +1209
// validHostHeader returns true if the Host header value is syntactically
// valid per RFC 3986 §3.2.2. It rejects malformed IP-literals (e.g. [],
// [123g::1], unclosed brackets) and invalid port values.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The docstring claims RFC 3986 §3.2.2 syntactic validity in general, but the implementation only validates bracketed IP-literals and port syntax (it does not validate reg-name characters/percent-encoding rules). Suggest rewording the comment to reflect the narrower scope (e.g., “validates IP-literal bracket/IPv6 syntax and optional port”) or expanding validation to cover reg-name if that’s intended.

Suggested change
// validHostHeader returns true if the Host header value is syntactically
// valid per RFC 3986 §3.2.2. It rejects malformed IP-literals (e.g. [],
// [123g::1], unclosed brackets) and invalid port values.
// validHostHeader returns true if the Host header value is structurally
// acceptable for this server. It specifically validates bracketed IP-literals
// (IPv6-style hosts) and optional port syntax, rejecting malformed IP-literals
// (e.g. [], [123g::1], unclosed brackets) and invalid port values. It does not
// fully validate reg-name characters or percent-encoding as defined in RFC 3986 §3.2.2.

Copilot uses AI. Check for mistakes.
@mholt
Copy link
Member

mholt commented Mar 9, 2026

Thank you. As being discussed in the issue, we're advising to check with the Go team first since a patch probably belongs upstream.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Host Header Validation for IP-Literals?

4 participants