@marcel91200 / caddy Mirror · read-only / commits / 789f60e3
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 07:15 UTC

caddyhttp: fix url_pattern authorization bypass via encoded-slash traversal (#7941)

* caddyhttp: match url_pattern against decoded, cleaned path The url_pattern matcher evaluated the raw, percent-encoded request URI while path-consuming handlers resolve the decoded, cleaned r.URL.Path. An encoded-slash payload such as "..%2f" stayed a single opaque segment for the WHATWG URLPattern parser, so "/public/..%2fadmin/secret" matched "/public/*" while handlers decoded it to "/admin/secret", bypassing any route-level access control built with url_pattern. Match the same path model handlers resolve: decode the path, normalize and clean it (mirroring the path matcher and #4407), then re-encode through url.URL to a canonical escaped form before running the pattern. The go-urlpattern library is spec-correct; the fix is in the integration. * build(deps): bump github.com/dunglas/go-urlpattern to v1.0.0 Moves off the pseudo-version to the first tagged release.

Kévin Dunglas committed Aug 15, 2026 at 18:38 UTC 789f60e337b82cea70e87dca2704c862185290c9
4 files changed +86 -4
go.mod
+1 -1
@@ -12,7 +12,7 @@ require (
12 github.com/caddyserver/certmagic v0.25.4
13 github.com/caddyserver/zerossl v0.1.5
14 github.com/cloudflare/circl v1.6.4
15 - github.com/dunglas/go-urlpattern v0.0.0-20260421141449-cbab7cf1e16d
15 + github.com/dunglas/go-urlpattern v1.0.0
16 github.com/dustin/go-humanize v1.0.1
17 github.com/go-chi/chi/v5 v5.3.1
18 github.com/google/cel-go v0.29.2
go.sum
+2 -2
@@ -138,8 +138,8 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
138 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
139 github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
140 github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
141 -github.com/dunglas/go-urlpattern v0.0.0-20260421141449-cbab7cf1e16d h1:Mw8vvAx2b5bjIrQIJaVTO25vJY5DMBUO/hJGNLIl+6g=
142 -github.com/dunglas/go-urlpattern v0.0.0-20260421141449-cbab7cf1e16d/go.mod h1:9qyjDljBPOWyWCGz7vo3Ek7cdnoG/DVk0Ucle7gWVS8=
141 +github.com/dunglas/go-urlpattern v1.0.0 h1:AmMwIDXWJFezeE1v38rxvQRGvvzTlityFuifLLVfoQQ=
142 +github.com/dunglas/go-urlpattern v1.0.0/go.mod h1:9qyjDljBPOWyWCGz7vo3Ek7cdnoG/DVk0Ucle7gWVS8=
143 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
144 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
145 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
modules/caddyhttp/urlpatternmatcher.go
+27 -1
@@ -3,6 +3,7 @@ package caddyhttp
3 import (
4 "fmt"
5 "net/http"
6 + "net/url"
7 "strings"
8
9 "github.com/dunglas/go-urlpattern"
@@ -39,6 +40,7 @@ type MatchURLPattern struct {
40 IgnoreCase bool `json:"ignore_case,omitempty"`
41
42 compiledPattern *urlpattern.URLPattern
43 + mergeSlashes bool
44 }
45
46 // CaddyModule returns the Caddy module information.
@@ -69,6 +71,19 @@ func (m *MatchURLPattern) Provision(_ caddy.Context) error {
71
72 m.compiledPattern = p
73
74 + // Like the path matcher, collapse empty path segments in the request
75 + // unless the pattern's path itself relies on them.
76 + patternPath := m.Pattern
77 + if idx := strings.Index(patternPath, "://"); idx != -1 {
78 + patternPath = patternPath[idx+3:]
79 + if slash := strings.IndexByte(patternPath, '/'); slash != -1 {
80 + patternPath = patternPath[slash:]
81 + } else {
82 + patternPath = ""
83 + }
84 + }
85 + m.mergeSlashes = !strings.Contains(patternPath, "//")
86 +
87 return nil
88 }
89
@@ -82,6 +97,8 @@ func (m *MatchURLPattern) Match(r *http.Request) bool {
97 // MatchWithError returns true if the request matches the URL pattern. The
98 // request's origin (scheme://host) is the base against which the path is
99 // resolved, so an absolute pattern or base_url can match on scheme and host.
100 +// The path is matched in its decoded, cleaned form so that the matcher sees
101 +// the same path that handlers resolve.
102 //
103 // On a match, captured groups are exposed as placeholders scoped by URL
104 // component, mirroring the URLPattern result object: a named group :id in the
@@ -93,7 +110,16 @@ func (m *MatchURLPattern) MatchWithError(r *http.Request) (bool, error) {
110 scheme = "https"
111 }
112
96 - result := m.compiledPattern.Exec(r.URL.RequestURI(), scheme+"://"+r.Host)
113 + // Match the same path model that handlers resolve: the decoded path,
114 + // normalized and cleaned (see #4407). Re-encoding through url.URL yields
115 + // a canonical escaped form, so encoded traversal payloads such as
116 + // "..%2f" cannot diverge from the path handlers actually use.
117 + uri := url.URL{
118 + Path: CleanPath(normalizeWindowsPath(r.URL.Path), m.mergeSlashes),
119 + RawQuery: r.URL.RawQuery,
120 + }
121 +
122 + result := m.compiledPattern.Exec(uri.RequestURI(), scheme+"://"+r.Host)
123 if result == nil {
124 return false, nil
125 }
modules/caddyhttp/urlpatternmatcher_test.go
+56
@@ -112,6 +112,62 @@ func TestURLPatternMatcher(t *testing.T) {
112 input: "/search",
113 expect: false,
114 },
115 + {
116 + name: "encoded slash traversal is rejected",
117 + match: MatchURLPattern{Pattern: "/public/*"},
118 + host: "example.com",
119 + input: "/public/..%2fadmin/secret.txt",
120 + expect: false,
121 + },
122 + {
123 + name: "uppercase encoded slash traversal is rejected",
124 + match: MatchURLPattern{Pattern: "/public/*"},
125 + host: "example.com",
126 + input: "/public/..%2Fadmin/secret.txt",
127 + expect: false,
128 + },
129 + {
130 + name: "encoded dot traversal is rejected",
131 + match: MatchURLPattern{Pattern: "/public/*"},
132 + host: "example.com",
133 + input: "/public/%2e%2e/admin/secret.txt",
134 + expect: false,
135 + },
136 + {
137 + name: "raw dot traversal is rejected",
138 + match: MatchURLPattern{Pattern: "/public/*"},
139 + host: "example.com",
140 + input: "/public/../admin/secret.txt",
141 + expect: false,
142 + },
143 + {
144 + name: "cleaned traversal still matches its real target",
145 + match: MatchURLPattern{Pattern: "/admin/*"},
146 + host: "example.com",
147 + input: "/public/..%2fadmin/secret.txt",
148 + expect: true,
149 + },
150 + {
151 + name: "doubled slashes are collapsed before matching",
152 + match: MatchURLPattern{Pattern: "/public/*"},
153 + host: "example.com",
154 + input: "/public//nested//file",
155 + expect: true,
156 + },
157 + {
158 + name: "pattern with empty segment preserves request slashes",
159 + match: MatchURLPattern{Pattern: "/a//b"},
160 + host: "example.com",
161 + input: "/a//b",
162 + expect: true,
163 + },
164 + {
165 + name: "encoded characters match their decoded form",
166 + match: MatchURLPattern{Pattern: "/caf%C3%A9"},
167 + host: "example.com",
168 + input: "/caf%C3%A9",
169 + expect: true,
170 + },
171 {
172 name: "invalid pattern fails provisioning",
173 match: MatchURLPattern{Pattern: "https://[invalid"},