From 6d1d90bf39ca4f775d2e045f0cbc45af8bc9ae0b Mon Sep 17 00:00:00 2001 From: Laurence Date: Mon, 2 Feb 2026 09:55:13 +0000 Subject: [PATCH] appsec: handle unparseable URLs gracefully Add permissive ParseURL function that falls back to storing the raw value in URL.Opaque when Go's url.Parse fails. This fixes CONNECT requests with authority-form URIs like 116.202.157.104:80. Fixes #4152 --- pkg/appsec/request.go | 5 +--- pkg/exprhelpers/waf.go | 16 ++++++++++++ pkg/exprhelpers/waf_test.go | 52 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/pkg/appsec/request.go b/pkg/appsec/request.go index a300954f76d..b76d9211e47 100644 --- a/pkg/appsec/request.go +++ b/pkg/appsec/request.go @@ -374,10 +374,7 @@ func NewParsedRequestFromRequest(r *http.Request, logger *log.Entry) (ParsedRequ r.Header.Del("User-Agent") } - parsedURL, err := url.Parse(clientURI) - if err != nil { - return ParsedRequest{}, fmt.Errorf("unable to parse url '%s': %s", clientURI, err) - } + parsedURL := exprhelpers.ParseURL(clientURI) originalHTTPRequest.URL = parsedURL diff --git a/pkg/exprhelpers/waf.go b/pkg/exprhelpers/waf.go index 5293876447a..b869388589b 100644 --- a/pkg/exprhelpers/waf.go +++ b/pkg/exprhelpers/waf.go @@ -43,6 +43,22 @@ func ParseQueryIntoValues(m url.Values, query string) { } } +// ParseURL is a permissive URL parser that handles edge cases like +// authority-form URIs (host:port) used in CONNECT requests. +// It never returns an error - malformed URLs are parsed best-effort. +func ParseURL(rawURL string) *url.URL { + // Try standard parsing first + if u, err := url.Parse(rawURL); err == nil { + return u + } + + // Fallback: store raw value in Opaque to preserve original request-target + // This handles cases like CONNECT authority-form (host:port) + return &url.URL{ + Opaque: rawURL, + } +} + func hexDigitToByte(digit byte) (byte, bool) { switch { case digit >= '0' && digit <= '9': diff --git a/pkg/exprhelpers/waf_test.go b/pkg/exprhelpers/waf_test.go index be8584c22ab..c1e947f2e5b 100644 --- a/pkg/exprhelpers/waf_test.go +++ b/pkg/exprhelpers/waf_test.go @@ -217,6 +217,58 @@ func TestParseQuery(t *testing.T) { } } +func TestParseURL(t *testing.T) { + tests := []struct { + name string + rawURL string + expectedPath string + expectedHost string + expectedOpaque string + }{ + { + name: "Normal path", + rawURL: "/path/to/resource", + expectedPath: "/path/to/resource", + }, + { + name: "Path with query", + rawURL: "/path/to/resource?query=value", + expectedPath: "/path/to/resource", + }, + { + name: "Full URL with scheme", + rawURL: "http://example.com/path", + expectedPath: "/path", + expectedHost: "example.com", + }, + { + name: "Authority-form (CONNECT request)", + rawURL: "116.202.157.104:80", + expectedOpaque: "116.202.157.104:80", + }, + { + name: "Authority-form with hostname (parsed as scheme:opaque)", + rawURL: "example.com:443", + expectedOpaque: "443", // Go parses this as scheme=example.com, opaque=443 + }, + { + name: "Empty string", + rawURL: "", + expectedPath: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res := ParseURL(test.rawURL) + require.NotNil(t, res) + require.Equal(t, test.expectedPath, res.Path) + require.Equal(t, test.expectedHost, res.Host) + require.Equal(t, test.expectedOpaque, res.Opaque) + }) + } +} + func TestExtractQueryParam(t *testing.T) { tests := []struct { name string