From d5d4560558901e459c2cf4441559cd9ed3a5b264 Mon Sep 17 00:00:00 2001 From: pkgagent Date: Thu, 6 Aug 2026 11:02:21 +0800 Subject: [PATCH 1/2] fix CVE-2026-27585, CVE-2026-27586, CVE-2026-27587, CVE-2026-27588, CVE-2026-27589, CVE-2026-27590 Update caddy from 2.7.5 to 2.10.2 (matching RHEL 10 / EPEL 10) and backport the six CVE fix patches: - CVE-2026-27585: file matcher path sanitization (backslash bypass) - CVE-2026-27586: mTLS client auth silent fail-open on bad CA file - CVE-2026-27587: HTTP path matcher percent-escape case-sensitivity bypass - CVE-2026-27588: HTTP host matcher case-sensitive fast path (>100 entries) - CVE-2026-27589: admin API /load cross-origin request enforcement - CVE-2026-27590: FastCGI split SCRIPT_NAME/PATH_INFO Unicode confusion Also: adapt 0001-Disable-commands patch to 2.10.2, drop obsolete celmatcher patch, regenerate bundled(golang(...)) provides, add gotestflags -short. Co-Authored-By: Claude Opus 4.7 --- ...e-commands-that-can-alter-the-binary.patch | 54 ++- ...-format-string-in-celmatcher.go-for-.patch | 49 -- caddy-2.10.2-CVE-2026-27585.patch | 47 ++ caddy-2.10.2-CVE-2026-27586.patch | 43 ++ caddy-2.10.2-CVE-2026-27587.patch | 64 +++ caddy-2.10.2-CVE-2026-27588.patch | 63 +++ caddy-2.10.2-CVE-2026-27589.patch | 60 +++ caddy-2.10.2-CVE-2026-27590.patch | 417 ++++++++++++++++++ caddy.spec | 244 +++++----- sources | 2 +- 10 files changed, 867 insertions(+), 176 deletions(-) delete mode 100644 0001-Fix-non-constant-format-string-in-celmatcher.go-for-.patch create mode 100644 caddy-2.10.2-CVE-2026-27585.patch create mode 100644 caddy-2.10.2-CVE-2026-27586.patch create mode 100644 caddy-2.10.2-CVE-2026-27587.patch create mode 100644 caddy-2.10.2-CVE-2026-27588.patch create mode 100644 caddy-2.10.2-CVE-2026-27589.patch create mode 100644 caddy-2.10.2-CVE-2026-27590.patch diff --git a/0001-Disable-commands-that-can-alter-the-binary.patch b/0001-Disable-commands-that-can-alter-the-binary.patch index 07f25a5..8211105 100644 --- a/0001-Disable-commands-that-can-alter-the-binary.patch +++ b/0001-Disable-commands-that-can-alter-the-binary.patch @@ -1,17 +1,18 @@ -From 4f771cd0f5f1a1a66bc142a98d3356296fa656a5 Mon Sep 17 00:00:00 2001 +From 41711beb6776923a84c06fad76964e5932333bd7 Mon Sep 17 00:00:00 2001 From: Carl George Date: Wed, 16 Feb 2022 11:45:03 -0600 Subject: [PATCH] Disable commands that can alter the binary --- - cmd/commands.go | 44 -------------------------------------------- - 1 file changed, 44 deletions(-) + cmd/commands.go | 45 -------------------------------------------- + cmd/commands_test.go | 8 ++++---- + 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/cmd/commands.go b/cmd/commands.go -index e5e1265e..972d24dc 100644 +index c9ea636b..791ddf6d 100644 --- a/cmd/commands.go +++ b/cmd/commands.go -@@ -394,50 +394,6 @@ is always printed to stdout. +@@ -404,51 +404,6 @@ is always printed to stdout. }, }) @@ -30,12 +31,13 @@ index e5e1265e..972d24dc 100644 - - RegisterCommand(Command{ - Name: "add-package", -- Usage: "", +- Usage: "", - Short: "Adds Caddy packages (EXPERIMENTAL)", - Long: ` -Downloads an updated Caddy binary with the specified packages (module/plugin) --added. Retains existing packages. Returns an error if the any of packages are --already included. EXPERIMENTAL: May be changed or removed. +-added, with an optional version specified (e.g., "package@version"). Retains +-existing packages. Returns an error if any of the specified packages are already +-included. EXPERIMENTAL: May be changed or removed. -`, - CobraFunc: func(cmd *cobra.Command) { - cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it") @@ -59,9 +61,33 @@ index e5e1265e..972d24dc 100644 - }, - }) - - RegisterCommand(Command{ - Name: "manpage", - Usage: "--directory ", --- -2.41.0 - + defaultFactory.Use(func(rootCmd *cobra.Command) { + manpageCommand := Command{ + Name: "manpage", +diff --git a/cmd/commands_test.go b/cmd/commands_test.go +index 085a9d78..f6d01fa3 100644 +--- a/cmd/commands_test.go ++++ b/cmd/commands_test.go +@@ -19,7 +19,7 @@ func TestCommandsAreAvailable(t *testing.T) { + // include the commands registered through calls to init in + // other packages + cmds := Commands() +- if len(cmds) != 17 { ++ if len(cmds) != 14 { + t.Errorf("expected 17 commands, got %d", len(cmds)) + } + +@@ -27,10 +27,10 @@ func TestCommandsAreAvailable(t *testing.T) { + slices.Sort(commandNames) + + expectedCommandNames := []string{ +- "adapt", "add-package", "build-info", "completion", ++ "adapt", "build-info", "completion", + "environ", "fmt", "list-modules", "manpage", +- "reload", "remove-package", "run", "start", +- "stop", "storage", "upgrade", "validate", "version", ++ "reload", "run", "start", ++ "stop", "storage", "validate", "version", + } + + if !reflect.DeepEqual(expectedCommandNames, commandNames) { diff --git a/0001-Fix-non-constant-format-string-in-celmatcher.go-for-.patch b/0001-Fix-non-constant-format-string-in-celmatcher.go-for-.patch deleted file mode 100644 index 7bb6d69..0000000 --- a/0001-Fix-non-constant-format-string-in-celmatcher.go-for-.patch +++ /dev/null @@ -1,49 +0,0 @@ -From a9c3161f0fed5d68d71c3a62631ab5ffbec794ae Mon Sep 17 00:00:00 2001 -From: doupengda -Date: Tue, 23 Sep 2025 17:12:01 +0800 -Subject: [PATCH] Fix non-constant format string in celmatcher.go for hardened - build - ---- - caddyconfig/caddyfile/dispenser.go | 2 +- - modules/caddyhttp/celmatcher.go | 4 ++-- - 2 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go -index 215a164..910e0ab 100644 ---- a/caddyconfig/caddyfile/dispenser.go -+++ b/caddyconfig/caddyfile/dispenser.go -@@ -411,7 +411,7 @@ func (d *Dispenser) EOFErr() error { - - // Err generates a custom parse-time error with a message of msg. - func (d *Dispenser) Err(msg string) error { -- return d.Errf(msg) -+ return d.Errf("%s",msg) - } - - // Errf is like Err, but for formatted error messages -diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go -index e997336..8729e24 100644 ---- a/modules/caddyhttp/celmatcher.go -+++ b/modules/caddyhttp/celmatcher.go -@@ -287,7 +287,7 @@ func (celTypeAdapter) NativeToValue(value any) ref.Val { - case time.Time: - return types.Timestamp{Time: v} - case error: -- types.NewErr(v.Error()) -+ types.NewErr("%s",v.Error()) - } - return types.DefaultTypeAdapter.NativeToValue(value) - } -@@ -446,7 +446,7 @@ func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions - return func(celReq, matcherData ref.Val) ref.Val { - matcher, err := fac(matcherData) - if err != nil { -- return types.NewErr(err.Error()) -+ return types.NewErr("%s",err.Error()) - } - httpReq := celReq.Value().(celHTTPRequest) - return types.Bool(matcher.Match(httpReq.Request)) --- -2.43.7 - diff --git a/caddy-2.10.2-CVE-2026-27585.patch b/caddy-2.10.2-CVE-2026-27585.patch new file mode 100644 index 0000000..0e0f4f6 --- /dev/null +++ b/caddy-2.10.2-CVE-2026-27585.patch @@ -0,0 +1,47 @@ +From 50e117440c1a0ab07d313bdf074e1c68c168823f Mon Sep 17 00:00:00 2001 +From: Matthew Holt +Date: Thu, 19 Feb 2026 13:17:19 -0700 +Subject: [PATCH] fileserver: Replace \ with \\ in file matcher paths + +Fixes: CVE-2026-27585 + +(cherry picked from commit a2825c5dd952769f139a16448dc1ca1be61b6058) +--- + modules/caddyhttp/fileserver/matcher.go | 1 + + modules/caddyhttp/fileserver/matcher_test.go | 6 ++++++ + "modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" | 0 + 3 files changed, 7 insertions(+) + create mode 100644 "modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" + +diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go +index fbcd36e0..9fe2774c 100644 +--- a/modules/caddyhttp/fileserver/matcher.go ++++ b/modules/caddyhttp/fileserver/matcher.go +@@ -720,6 +720,7 @@ var globSafeRepl = strings.NewReplacer( + "*", "\\*", + "[", "\\[", + "?", "\\?", ++ "\\", "\\\\", + ) + + const ( +diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go +index f0ec4b39..36483fcd 100644 +--- a/modules/caddyhttp/fileserver/matcher_test.go ++++ b/modules/caddyhttp/fileserver/matcher_test.go +@@ -115,6 +115,12 @@ func TestFileMatcher(t *testing.T) { + expectedType: "file", + matched: !isWindows, + }, ++ { ++ path: "/foodir/secr%5Cet.txt", ++ expectedPath: "/foodir/secr\\et.txt", ++ expectedType: "file", ++ matched: true, ++ }, + } { + m := &MatchFile{ + fsmap: &filesystems.FileSystemMap{}, +diff --git "a/modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" "b/modules/caddyhttp/fileserver/testdata/foodir/secr\\et.txt" +new file mode 100644 +index 00000000..e69de29b diff --git a/caddy-2.10.2-CVE-2026-27586.patch b/caddy-2.10.2-CVE-2026-27586.patch new file mode 100644 index 0000000..06e7850 --- /dev/null +++ b/caddy-2.10.2-CVE-2026-27586.patch @@ -0,0 +1,43 @@ +From 73a6287031f5ea6847f222aa9641ff5b72ba4200 Mon Sep 17 00:00:00 2001 +From: moscowchill <72578879+moscowchill@users.noreply.github.com> +Date: Thu, 12 Feb 2026 23:42:54 +0800 +Subject: [PATCH] caddytls: Return errors instead of nil in client auth + provisioning (#7464) + +Two error returns in ClientAuthentication.provision() were +returning nil instead of the actual error, silently swallowing +failures when converting PEM files to DER and when provisioning +the CA pool. This could cause mTLS client authentication to +silently fall back to the system trust store, accepting any +client certificate signed by a public CA instead of restricting +to the configured trust anchors. + +Fixes: CVE-2026-27586 + +(cherry picked from commit d42d39b4bc237c628f9a95363b28044cb7a7fe72) +--- + modules/caddytls/connpolicy.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go +index 724271a8..a6aedcc2 100644 +--- a/modules/caddytls/connpolicy.go ++++ b/modules/caddytls/connpolicy.go +@@ -794,7 +794,7 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { + for _, fpath := range clientauth.TrustedCACertPEMFiles { + ders, err := convertPEMFilesToDER(fpath) + if err != nil { +- return nil ++ return err + } + clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...) + } +@@ -807,7 +807,7 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { + } + err := caPool.Provision(ctx) + if err != nil { +- return nil ++ return err + } + clientauth.ca = caPool + } diff --git a/caddy-2.10.2-CVE-2026-27587.patch b/caddy-2.10.2-CVE-2026-27587.patch new file mode 100644 index 0000000..214a31d --- /dev/null +++ b/caddy-2.10.2-CVE-2026-27587.patch @@ -0,0 +1,64 @@ +From 41df2955b0f3222cc01a1f5d0b4dbc08ec39cedb Mon Sep 17 00:00:00 2001 +From: Matthew Holt +Date: Mon, 9 Feb 2026 09:43:07 -0700 +Subject: [PATCH] caddyhttp: Lowercase comparison when matching with escape + sequence + +Necessary as otherwise the early-bail in `until = +strings.IndexByte(remaining, nextCh) ... if until == -1` can cause a +case-insensitive mismatch + +Fixes: CVE-2026-27587 +Co-authored-by: Asim Viladi Oglu Manizada + +(cherry picked from commit bd374ca9d72e296c9361aee76924b6540f22f0c0) +(cherry picked from commit a1081194bfae4e0d8c227ec44aecb95eded55d1e) +--- + modules/caddyhttp/matchers.go | 5 +++-- + modules/caddyhttp/matchers_test.go | 10 ++++++++++ + 2 files changed, 13 insertions(+), 2 deletions(-) + +diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go +index 22976cfb..3e6c2932 100644 +--- a/modules/caddyhttp/matchers.go ++++ b/modules/caddyhttp/matchers.go +@@ -533,6 +533,7 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) { + } + + func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool { ++ escapedPath = strings.ToLower(escapedPath) + // We would just compare the pattern against r.URL.Path, + // but the pattern contains %, indicating that we should + // compare at least some part of the path in raw/escaped +@@ -632,8 +633,8 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b + // we can now treat rawpath globs (%*) as regular globs (*) + matchPath = strings.ReplaceAll(matchPath, "%*", "*") + +- // ignore error here because we can't handle it anyway= +- matches, _ := path.Match(matchPath, sb.String()) ++ // ignore error here because we can't handle it anyway ++ matches, _ := path.Match(matchPath, strings.ToLower(sb.String())) + return matches + } + +diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go +index 068207e8..c002a3e4 100644 +--- a/modules/caddyhttp/matchers_test.go ++++ b/modules/caddyhttp/matchers_test.go +@@ -412,6 +412,16 @@ func TestPathMatcher(t *testing.T) { + input: "/foo%2fbar/baz", + expect: true, + }, ++ { ++ match: MatchPath{"/admin%2fpanel"}, ++ input: "/ADMIN%2fpanel", ++ expect: true, ++ }, ++ { ++ match: MatchPath{"/admin%2fpa*el"}, ++ input: "/ADMIN%2fPaAzZLm123NEL", ++ expect: true, ++ }, + } { + err := tc.match.Provision(caddy.Context{}) + if err == nil && tc.provisionErr { diff --git a/caddy-2.10.2-CVE-2026-27588.patch b/caddy-2.10.2-CVE-2026-27588.patch new file mode 100644 index 0000000..9697690 --- /dev/null +++ b/caddy-2.10.2-CVE-2026-27588.patch @@ -0,0 +1,63 @@ +From c8f27e75fe97672dffafccab6d7b2b26f02ba9de Mon Sep 17 00:00:00 2001 +From: Matthew Holt +Date: Mon, 9 Feb 2026 14:18:55 -0700 +Subject: [PATCH] caddyhttp: Use case-insensitive comparison for large Host + lists +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Normalize exact hosts at provisioning and reqHost in the fast path so case-different Host variants can’t bypass host-gated routes. + +Fixes: CVE-2026-27588 +Co-authored-by: Asim Viladi Oglu Manizada + +(cherry picked from commit 1f43e8566b4c1d66a00a138188f0defc5adc6d75) +(cherry picked from commit eec32a0bb5a11651c6a7b04ce82dc50610f2b27e) +--- + modules/caddyhttp/matchers.go | 15 ++++++++++----- + 1 file changed, 10 insertions(+), 5 deletions(-) + +diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go +index 3e6c2932..27e5c5ae 100644 +--- a/modules/caddyhttp/matchers.go ++++ b/modules/caddyhttp/matchers.go +@@ -262,13 +262,17 @@ func (m MatchHost) Provision(_ caddy.Context) error { + if err != nil { + return fmt.Errorf("converting hostname '%s' to ASCII: %v", host, err) + } +- if asciiHost != host { +- m[i] = asciiHost +- } + normalizedHost := strings.ToLower(asciiHost) + if firstI, ok := seen[normalizedHost]; ok { + return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host) + } ++ // Normalize exact hosts for standardized comparison in large-list fastpath later on. ++ // Keep wildcards/placeholders untouched. ++ if m.fuzzy(asciiHost) { ++ m[i] = asciiHost ++ } else { ++ m[i] = normalizedHost ++ } + seen[normalizedHost] = i + } + +@@ -312,14 +316,15 @@ func (m MatchHost) MatchWithError(r *http.Request) (bool, error) { + } + + if m.large() { ++ reqHostLower := strings.ToLower(reqHost) + // fast path: locate exact match using binary search (about 100-1000x faster for large lists) + pos := sort.Search(len(m), func(i int) bool { + if m.fuzzy(m[i]) { + return false + } +- return m[i] >= reqHost ++ return m[i] >= reqHostLower + }) +- if pos < len(m) && m[pos] == reqHost { ++ if pos < len(m) && m[pos] == reqHostLower { + return true, nil + } + } diff --git a/caddy-2.10.2-CVE-2026-27589.patch b/caddy-2.10.2-CVE-2026-27589.patch new file mode 100644 index 0000000..8b89580 --- /dev/null +++ b/caddy-2.10.2-CVE-2026-27589.patch @@ -0,0 +1,60 @@ +From 61f8e68a4abad32958d2adf03ba733de23073c43 Mon Sep 17 00:00:00 2001 +From: Matthew Holt +Date: Thu, 5 Feb 2026 09:39:11 -0700 +Subject: [PATCH] admin: Reject requests with Sec-Fetch-Mode headers + +And buggy Origin: null headers. + +Resolves a low-risk security report by @1seal. + +Fixes: CVE-2026-27589 + +(cherry picked from commit 42ca010e9d8da35e91edd69e724d3736678b5620) +--- + admin.go | 27 ++++++++++++++++++++++++++- + 1 file changed, 26 insertions(+), 1 deletion(-) + +diff --git a/admin.go b/admin.go +index e6895c39..7dbfd478 100644 +--- a/admin.go ++++ b/admin.go +@@ -807,13 +807,38 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { + } + } + ++ // common mitigations in browser contexts + if strings.Contains(r.Header.Get("Upgrade"), "websocket") { + // I've never been able demonstrate a vulnerability myself, but apparently + // WebSocket connections originating from browsers aren't subject to CORS + // restrictions, so we'll just be on the safe side +- h.handleError(w, r, fmt.Errorf("websocket connections aren't allowed")) ++ h.handleError(w, r, APIError{ ++ HTTPStatus: http.StatusBadRequest, ++ Err: errors.New("websocket connections aren't allowed"), ++ Message: "WebSocket connections aren't allowed.", ++ }) + return + } ++ if strings.Contains(r.Header.Get("Sec-Fetch-Mode"), "no-cors") { ++ // turns out web pages can just disable the same-origin policy (!???!?) ++ // but at least browsers let us know that's the case, holy heck ++ h.handleError(w, r, APIError{ ++ HTTPStatus: http.StatusBadRequest, ++ Err: errors.New("client attempted to make request by disabling same-origin policy using no-cors mode"), ++ Message: "Disabling same-origin restrictions is not allowed.", ++ }) ++ return ++ } ++ if r.Header.Get("Origin") == "null" { ++ // bug in Firefox in certain cross-origin situations (yikes?) ++ // (not strictly a security vuln on its own, but it's red flaggy, ++ // since it seems to manifest in cross-origin contexts) ++ h.handleError(w, r, APIError{ ++ HTTPStatus: http.StatusBadRequest, ++ Err: errors.New("invalid origin 'null'"), ++ Message: "Buggy browser is sending null Origin header.", ++ }) ++ } + + if h.enforceHost { + // DNS rebinding mitigation diff --git a/caddy-2.10.2-CVE-2026-27590.patch b/caddy-2.10.2-CVE-2026-27590.patch new file mode 100644 index 0000000..6c342c9 --- /dev/null +++ b/caddy-2.10.2-CVE-2026-27590.patch @@ -0,0 +1,417 @@ +From c840bfaf4a5f0c16b2a5880a3b95f32d3e83aeba Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= +Date: Tue, 10 Feb 2026 19:52:36 +0100 +Subject: [PATCH] fix: FastCGI split SCRIPT_NAME/PATH_INFO confusion + +Fixes: CVE-2026-27590 + +(cherry picked from commit 7c28c0c07ac70a8960a166c7126150a408ba7464) +--- + .../caddyhttp/reverseproxy/fastcgi/fastcgi.go | 90 ++++++- + .../reverseproxy/fastcgi/fastcgi_test.go | 246 ++++++++++++++++++ + 2 files changed, 332 insertions(+), 4 deletions(-) + create mode 100644 modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go + +diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +index d451dd38..51d4ebc2 100644 +--- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go ++++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +@@ -16,6 +16,7 @@ package fastcgi + + import ( + "crypto/tls" ++ "errors" + "fmt" + "net" + "net/http" +@@ -23,9 +24,12 @@ import ( + "strconv" + "strings" + "time" ++ "unicode/utf8" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" ++ "golang.org/x/text/language" ++ "golang.org/x/text/search" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +@@ -33,7 +37,11 @@ import ( + "github.com/caddyserver/caddy/v2/modules/caddytls" + ) + +-var noopLogger = zap.NewNop() ++var ( ++ ErrInvalidSplitPath = errors.New("split path contains non-ASCII characters") ++ ++ noopLogger = zap.NewNop() ++) + + func init() { + caddy.RegisterModule(Transport{}) +@@ -50,6 +58,9 @@ type Transport struct { + // actual resource (CGI script) name, and the second piece will be set to + // PATH_INFO for the CGI script to use. + // ++ // Split paths can only contain ASCII characters. ++ // Comparison is case-insensitive. ++ // + // Future enhancements should be careful to avoid CVE-2019-11043, + // which can be mitigated with use of a try_files-like behavior + // that 404s if the fastcgi path info is not found. +@@ -109,6 +120,28 @@ func (t *Transport) Provision(ctx caddy.Context) error { + t.DialTimeout = caddy.Duration(3 * time.Second) + } + ++ var b strings.Builder ++ ++ for i, split := range t.SplitPath { ++ b.Grow(len(split)) ++ ++ for j := 0; j < len(split); j++ { ++ c := split[j] ++ if c >= utf8.RuneSelf { ++ return ErrInvalidSplitPath ++ } ++ ++ if 'A' <= c && c <= 'Z' { ++ b.WriteByte(c + 'a' - 'A') ++ } else { ++ b.WriteByte(c) ++ } ++ } ++ ++ t.SplitPath[i] = b.String() ++ b.Reset() ++ } ++ + return nil + } + +@@ -371,8 +404,15 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) { + return env, nil + } + ++var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase) ++ + // splitPos returns the index where path should + // be split based on t.SplitPath. ++// ++// example: if splitPath is [".php"] ++// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path") ++// ++// Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license) + func (t Transport) splitPos(path string) int { + // TODO: from v1... + // if httpserver.CaseSensitivePath { +@@ -382,12 +422,54 @@ func (t Transport) splitPos(path string) int { + return 0 + } + +- lowerPath := strings.ToLower(path) ++ pathLen := len(path) ++ ++ // We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in Provision(). + for _, split := range t.SplitPath { +- if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 { +- return idx + len(split) ++ splitLen := len(split) ++ ++ for i := 0; i < pathLen; i++ { ++ if path[i] >= utf8.RuneSelf { ++ if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { ++ return end ++ } ++ ++ break ++ } ++ ++ if i+splitLen > pathLen { ++ continue ++ } ++ ++ match := true ++ for j := 0; j < splitLen; j++ { ++ c := path[i+j] ++ ++ if c >= utf8.RuneSelf { ++ if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { ++ return end ++ } ++ ++ break ++ } ++ ++ if 'A' <= c && c <= 'Z' { ++ c += 'a' - 'A' ++ } ++ ++ if c != split[j] { ++ match = false ++ ++ break ++ } ++ } ++ ++ if match { ++ return i + splitLen ++ } + } + } ++ + return -1 + } + +diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go +new file mode 100644 +index 00000000..7097ff79 +--- /dev/null ++++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go +@@ -0,0 +1,246 @@ ++package fastcgi ++ ++import ( ++ "strings" ++ "testing" ++ ++ "github.com/stretchr/testify/assert" ++ "github.com/stretchr/testify/require" ++ ++ "github.com/caddyserver/caddy/v2" ++) ++ ++func TestProvisionSplitPath(t *testing.T) { ++ tests := []struct { ++ name string ++ splitPath []string ++ wantErr error ++ wantSplitPath []string ++ }{ ++ { ++ name: "valid lowercase split path", ++ splitPath: []string{".php"}, ++ wantErr: nil, ++ wantSplitPath: []string{".php"}, ++ }, ++ { ++ name: "valid uppercase split path normalized", ++ splitPath: []string{".PHP"}, ++ wantErr: nil, ++ wantSplitPath: []string{".php"}, ++ }, ++ { ++ name: "valid mixed case split path normalized", ++ splitPath: []string{".PhP", ".PHTML"}, ++ wantErr: nil, ++ wantSplitPath: []string{".php", ".phtml"}, ++ }, ++ { ++ name: "empty split path", ++ splitPath: []string{}, ++ wantErr: nil, ++ wantSplitPath: []string{}, ++ }, ++ { ++ name: "non-ASCII character in split path rejected", ++ splitPath: []string{".php", ".Ⱥphp"}, ++ wantErr: ErrInvalidSplitPath, ++ }, ++ { ++ name: "unicode character in split path rejected", ++ splitPath: []string{".phpⱥ"}, ++ wantErr: ErrInvalidSplitPath, ++ }, ++ } ++ ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ tr := Transport{SplitPath: tt.splitPath} ++ err := tr.Provision(caddy.Context{}) ++ ++ if tt.wantErr != nil { ++ require.ErrorIs(t, err, tt.wantErr) ++ ++ return ++ } ++ ++ require.NoError(t, err) ++ assert.Equal(t, tt.wantSplitPath, tr.SplitPath) ++ }) ++ } ++} ++ ++func TestSplitPos(t *testing.T) { ++ tests := []struct { ++ name string ++ path string ++ splitPath []string ++ wantPos int ++ }{ ++ { ++ name: "simple php extension", ++ path: "/path/to/script.php", ++ splitPath: []string{".php"}, ++ wantPos: 19, ++ }, ++ { ++ name: "php extension with path info", ++ path: "/path/to/script.php/some/path", ++ splitPath: []string{".php"}, ++ wantPos: 19, ++ }, ++ { ++ name: "case insensitive match", ++ path: "/path/to/script.PHP", ++ splitPath: []string{".php"}, ++ wantPos: 19, ++ }, ++ { ++ name: "mixed case match", ++ path: "/path/to/script.PhP/info", ++ splitPath: []string{".php"}, ++ wantPos: 19, ++ }, ++ { ++ name: "no match", ++ path: "/path/to/script.txt", ++ splitPath: []string{".php"}, ++ wantPos: -1, ++ }, ++ { ++ name: "empty split path", ++ path: "/path/to/script.php", ++ splitPath: []string{}, ++ wantPos: 0, ++ }, ++ { ++ name: "multiple split paths first match", ++ path: "/path/to/script.php", ++ splitPath: []string{".php", ".phtml"}, ++ wantPos: 19, ++ }, ++ { ++ name: "multiple split paths second match", ++ path: "/path/to/script.phtml", ++ splitPath: []string{".php", ".phtml"}, ++ wantPos: 21, ++ }, ++ // Unicode case-folding tests (security fix for GHSA-g966-83w7-6w38) ++ // U+023A (Ⱥ) lowercases to U+2C65 (ⱥ), which has different UTF-8 byte length ++ // Ⱥ: 2 bytes (C8 BA), ⱥ: 3 bytes (E2 B1 A5) ++ { ++ name: "unicode path with case-folding length expansion", ++ path: "/ȺȺȺȺshell.php", ++ splitPath: []string{".php"}, ++ wantPos: 18, // correct position in original string ++ }, ++ { ++ name: "unicode path with extension after expansion chars", ++ path: "/ȺȺȺȺshell.php/path/info", ++ splitPath: []string{".php"}, ++ wantPos: 18, ++ }, ++ { ++ name: "unicode in filename with multiple php occurrences", ++ path: "/ȺȺȺȺshell.php.txt.php", ++ splitPath: []string{".php"}, ++ wantPos: 18, // should match first .php, not be confused by byte offset shift ++ }, ++ { ++ name: "unicode case insensitive extension", ++ path: "/ȺȺȺȺshell.PHP", ++ splitPath: []string{".php"}, ++ wantPos: 18, ++ }, ++ { ++ name: "unicode in middle of path", ++ path: "/path/Ⱥtest/script.php", ++ splitPath: []string{".php"}, ++ wantPos: 23, // Ⱥ is 2 bytes, so path is 23 bytes total, .php ends at byte 23 ++ }, ++ { ++ name: "unicode only in directory not filename", ++ path: "/Ⱥ/script.php", ++ splitPath: []string{".php"}, ++ wantPos: 14, ++ }, ++ // Additional Unicode characters that expand when lowercased ++ // U+0130 (İ - Turkish capital I with dot) lowercases to U+0069 + U+0307 ++ { ++ name: "turkish capital I with dot", ++ path: "/İtest.php", ++ splitPath: []string{".php"}, ++ wantPos: 11, ++ }, ++ // Ensure standard ASCII still works correctly ++ { ++ name: "ascii only path with case variation", ++ path: "/PATH/TO/SCRIPT.PHP/INFO", ++ splitPath: []string{".php"}, ++ wantPos: 19, ++ }, ++ { ++ name: "path at root", ++ path: "/index.php", ++ splitPath: []string{".php"}, ++ wantPos: 10, ++ }, ++ { ++ name: "extension in middle of filename", ++ path: "/test.php.bak", ++ splitPath: []string{".php"}, ++ wantPos: 9, ++ }, ++ } ++ ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ gotPos := Transport{SplitPath: tt.splitPath}.splitPos(tt.path) ++ assert.Equal(t, tt.wantPos, gotPos, "splitPos(%q, %v)", tt.path, tt.splitPath) ++ ++ // Verify that the split produces valid substrings ++ if gotPos > 0 && gotPos <= len(tt.path) { ++ scriptName := tt.path[:gotPos] ++ pathInfo := tt.path[gotPos:] ++ ++ // The script name should end with one of the split extensions (case-insensitive) ++ hasValidEnding := false ++ for _, split := range tt.splitPath { ++ if strings.HasSuffix(strings.ToLower(scriptName), split) { ++ hasValidEnding = true ++ break ++ } ++ } ++ assert.True(t, hasValidEnding, "script name %q should end with one of %v", scriptName, tt.splitPath) ++ ++ // Original path should be reconstructable ++ assert.Equal(t, tt.path, scriptName+pathInfo, "path should be reconstructable from split parts") ++ } ++ }) ++ } ++} ++ ++// TestSplitPosUnicodeSecurityRegression specifically tests the vulnerability ++// described in GHSA-g966-83w7-6w38 where Unicode case-folding caused ++// incorrect SCRIPT_NAME/PATH_INFO splitting ++func TestSplitPosUnicodeSecurityRegression(t *testing.T) { ++ // U+023A: Ⱥ (UTF-8: C8 BA). Lowercase is ⱥ (UTF-8: E2 B1 A5), longer in bytes. ++ path := "/ȺȺȺȺshell.php.txt.php" ++ split := []string{".php"} ++ ++ pos := Transport{SplitPath: split}.splitPos(path) ++ ++ // The vulnerable code would return 22 (computed on lowercased string) ++ // The correct code should return 18 (position in original string) ++ expectedPos := strings.Index(path, ".php") + len(".php") ++ assert.Equal(t, expectedPos, pos, "split position should match first .php in original string") ++ assert.Equal(t, 18, pos, "split position should be 18, not 22") ++ ++ if pos > 0 && pos <= len(path) { ++ scriptName := path[:pos] ++ pathInfo := path[pos:] ++ ++ assert.Equal(t, "/ȺȺȺȺshell.php", scriptName, "script name should be the path up to first .php") ++ assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php") ++ } ++} diff --git a/caddy.spec b/caddy.spec index f843fb2..7cf082e 100644 --- a/caddy.spec +++ b/caddy.spec @@ -1,8 +1,9 @@ %global goipath github.com/caddyserver/caddy +%global gotestflags %{gocompilerflags} -short Name: caddy -Version: 2.7.5 -Release: 4%{?dist} +Version: 2.10.2 +Release: 1%{?dist} Summary: Web server with automatic HTTPS License: Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND MIT AND BSD-2-Clause-Views AND CC0-1.0 AND ISC AND MPL-2.0 URL: https://caddyserver.com @@ -21,149 +22,164 @@ Source30: poweredby-white.png Source31: poweredby-black.png Patch0: 0001-Disable-commands-that-can-alter-the-binary.patch -Patch1: 0001-Fix-non-constant-format-string-in-celmatcher.go-for-.patch - -BuildRequires: golang >= 1.20 - -Provides: bundled(golang(filippo.io/edwards25519)) = 1.0.0 +Patch1: caddy-2.10.2-CVE-2026-27585.patch +Patch2: caddy-2.10.2-CVE-2026-27586.patch +Patch3: caddy-2.10.2-CVE-2026-27587.patch +Patch4: caddy-2.10.2-CVE-2026-27588.patch +Patch5: caddy-2.10.2-CVE-2026-27589.patch +Patch6: caddy-2.10.2-CVE-2026-27590.patch + +BuildRequires: golang >= 1.25 + +Provides: bundled(golang(cel.dev/expr)) = 0.24.0 +Provides: bundled(golang(cloud.google.com/go/auth)) = 0.16.2 +Provides: bundled(golang(cloud.google.com/go/auth/oauth2adapt)) = 0.2.8 +Provides: bundled(golang(cloud.google.com/go/compute/metadata)) = 0.7.0 +Provides: bundled(golang(dario.cat/mergo)) = 1.0.1 +Provides: bundled(golang(filippo.io/edwards25519)) = 1.1.0 Provides: bundled(golang(github.com/AndreasBriese/bbloom)) = 46b345b -Provides: bundled(golang(github.com/BurntSushi/toml)) = 1.3.2 +Provides: bundled(golang(github.com/BurntSushi/toml)) = 1.5.0 +Provides: bundled(golang(github.com/KimMachineGun/automemlimit)) = 0.7.4 Provides: bundled(golang(github.com/Masterminds/goutils)) = 1.1.1 -Provides: bundled(golang(github.com/Masterminds/semver/v3)) = 3.2.0 -Provides: bundled(golang(github.com/Masterminds/sprig/v3)) = 3.2.3 +Provides: bundled(golang(github.com/Masterminds/semver/v3)) = 3.3.0 +Provides: bundled(golang(github.com/Masterminds/sprig/v3)) = 3.3.0 Provides: bundled(golang(github.com/Microsoft/go-winio)) = 0.6.0 -Provides: bundled(golang(github.com/alecthomas/chroma/v2)) = 2.9.1 -Provides: bundled(golang(github.com/antlr/antlr4/runtime/Go/antlr/v4)) = 8188dc5 +Provides: bundled(golang(github.com/alecthomas/chroma/v2)) = 2.20.0 +Provides: bundled(golang(github.com/antlr4-go/antlr/v4)) = 4.13.0 Provides: bundled(golang(github.com/aryann/difflib)) = ff5ff6d Provides: bundled(golang(github.com/beorn7/perks)) = 1.0.1 -Provides: bundled(golang(github.com/caddyserver/certmagic)) = 0.19.2 -Provides: bundled(golang(github.com/cenkalti/backoff/v4)) = 4.2.1 +Provides: bundled(golang(github.com/caddyserver/certmagic)) = 0.24.0 +Provides: bundled(golang(github.com/caddyserver/zerossl)) = 0.1.3 +Provides: bundled(golang(github.com/ccoveille/go-safecast)) = 1.6.1 +Provides: bundled(golang(github.com/cenkalti/backoff/v5)) = 5.0.2 Provides: bundled(golang(github.com/cespare/xxhash)) = 1.1.0 -Provides: bundled(golang(github.com/cespare/xxhash/v2)) = 2.2.0 +Provides: bundled(golang(github.com/cespare/xxhash/v2)) = 2.3.0 Provides: bundled(golang(github.com/chzyer/readline)) = 1.5.1 -Provides: bundled(golang(github.com/cpuguy83/go-md2man/v2)) = 2.0.2 +Provides: bundled(golang(github.com/cloudflare/circl)) = 1.6.1 +Provides: bundled(golang(github.com/coreos/go-oidc/v3)) = 3.14.1 +Provides: bundled(golang(github.com/cpuguy83/go-md2man/v2)) = 2.0.7 Provides: bundled(golang(github.com/davecgh/go-spew)) = 1.1.1 Provides: bundled(golang(github.com/dgraph-io/badger)) = 1.6.2 Provides: bundled(golang(github.com/dgraph-io/badger/v2)) = 2.2007.4 -Provides: bundled(golang(github.com/dgraph-io/ristretto)) = 0.1.0 +Provides: bundled(golang(github.com/dgraph-io/ristretto)) = 0.2.0 Provides: bundled(golang(github.com/dgryski/go-farm)) = a6ae236 -Provides: bundled(golang(github.com/dlclark/regexp2)) = 1.10.0 +Provides: bundled(golang(github.com/dlclark/regexp2)) = 1.11.5 Provides: bundled(golang(github.com/dustin/go-humanize)) = 1.0.1 -Provides: bundled(golang(github.com/felixge/httpsnoop)) = 1.0.3 -Provides: bundled(golang(github.com/fxamacker/cbor/v2)) = 2.5.0 -Provides: bundled(golang(github.com/go-chi/chi)) = 4.1.2+incompatible -Provides: bundled(golang(github.com/go-chi/chi/v5)) = 5.0.10 -Provides: bundled(golang(github.com/go-kit/kit)) = 0.10.0 -Provides: bundled(golang(github.com/go-logfmt/logfmt)) = 0.5.1 -Provides: bundled(golang(github.com/go-logr/logr)) = 1.2.4 +Provides: bundled(golang(github.com/felixge/httpsnoop)) = 1.0.4 +Provides: bundled(golang(github.com/francoispqt/gojay)) = 1.2.13 +Provides: bundled(golang(github.com/fxamacker/cbor/v2)) = 2.8.0 +Provides: bundled(golang(github.com/go-chi/chi/v5)) = 5.2.2 +Provides: bundled(golang(github.com/go-jose/go-jose/v3)) = 3.0.4 +Provides: bundled(golang(github.com/go-jose/go-jose/v4)) = 4.0.5 +Provides: bundled(golang(github.com/go-logr/logr)) = 1.4.3 Provides: bundled(golang(github.com/go-logr/stdr)) = 1.2.2 -Provides: bundled(golang(github.com/go-sql-driver/mysql)) = 1.7.1 -Provides: bundled(golang(github.com/go-task/slim-sprig)) = 52ccab3 -Provides: bundled(golang(github.com/golang/glog)) = 1.1.0 -Provides: bundled(golang(github.com/golang/protobuf)) = 1.5.3 +Provides: bundled(golang(github.com/go-sql-driver/mysql)) = 1.8.1 +Provides: bundled(golang(github.com/golang/protobuf)) = 1.5.4 Provides: bundled(golang(github.com/golang/snappy)) = 0.0.4 -Provides: bundled(golang(github.com/google/cel-go)) = 0.15.1 -Provides: bundled(golang(github.com/google/certificate-transparency-go)) = 1.1.6 -Provides: bundled(golang(github.com/google/go-tpm)) = 0.9.0 +Provides: bundled(golang(github.com/google/cel-go)) = 0.26.0 +Provides: bundled(golang(github.com/google/certificate-transparency-go)) = 74a5dd3 +Provides: bundled(golang(github.com/google/go-tpm)) = 0.9.5 Provides: bundled(golang(github.com/google/go-tspi)) = 0.3.0 -Provides: bundled(golang(github.com/google/pprof)) = 4bb14d4 -Provides: bundled(golang(github.com/google/uuid)) = 1.3.1 -Provides: bundled(golang(github.com/grpc-ecosystem/grpc-gateway/v2)) = 2.15.2 -Provides: bundled(golang(github.com/huandu/xstrings)) = 1.3.3 -Provides: bundled(golang(github.com/imdario/mergo)) = 0.3.12 +Provides: bundled(golang(github.com/google/s2a-go)) = 0.1.9 +Provides: bundled(golang(github.com/google/uuid)) = 1.6.0 +Provides: bundled(golang(github.com/googleapis/enterprise-certificate-proxy)) = 0.3.6 +Provides: bundled(golang(github.com/googleapis/gax-go/v2)) = 2.14.2 +Provides: bundled(golang(github.com/grpc-ecosystem/grpc-gateway/v2)) = 2.27.1 +Provides: bundled(golang(github.com/huandu/xstrings)) = 1.5.0 Provides: bundled(golang(github.com/inconshreveable/mousetrap)) = 1.1.0 -Provides: bundled(golang(github.com/jackc/chunkreader/v2)) = 2.0.1 -Provides: bundled(golang(github.com/jackc/pgconn)) = 1.14.0 -Provides: bundled(golang(github.com/jackc/pgio)) = 1.0.0 Provides: bundled(golang(github.com/jackc/pgpassfile)) = 1.0.0 -Provides: bundled(golang(github.com/jackc/pgproto3/v2)) = 2.3.2 Provides: bundled(golang(github.com/jackc/pgservicefile)) = 091c0ba -Provides: bundled(golang(github.com/jackc/pgtype)) = 1.14.0 -Provides: bundled(golang(github.com/jackc/pgx/v4)) = 4.18.0 -Provides: bundled(golang(github.com/klauspost/compress)) = 1.17.0 -Provides: bundled(golang(github.com/klauspost/cpuid/v2)) = 2.2.5 -Provides: bundled(golang(github.com/libdns/libdns)) = 0.2.1 +Provides: bundled(golang(github.com/jackc/pgx/v5)) = 5.6.0 +Provides: bundled(golang(github.com/jackc/puddle/v2)) = 2.2.1 +Provides: bundled(golang(github.com/klauspost/compress)) = 1.18.0 +Provides: bundled(golang(github.com/klauspost/cpuid/v2)) = 2.3.0 +Provides: bundled(golang(github.com/kylelemons/godebug)) = 1.1.0 +Provides: bundled(golang(github.com/libdns/libdns)) = 1.1.0 Provides: bundled(golang(github.com/manifoldco/promptui)) = 0.9.0 -Provides: bundled(golang(github.com/mastercactapus/proxyprotocol)) = 0.0.4 -Provides: bundled(golang(github.com/mattn/go-colorable)) = 0.1.8 -Provides: bundled(golang(github.com/mattn/go-isatty)) = 0.0.16 -Provides: bundled(golang(github.com/matttproud/golang_protobuf_extensions)) = 1.0.4 +Provides: bundled(golang(github.com/mattn/go-colorable)) = 0.1.13 +Provides: bundled(golang(github.com/mattn/go-isatty)) = 0.0.20 Provides: bundled(golang(github.com/mgutz/ansi)) = d51e80e -Provides: bundled(golang(github.com/mholt/acmez)) = 1.2.0 -Provides: bundled(golang(github.com/micromdm/scep/v2)) = 2.1.0 -Provides: bundled(golang(github.com/miekg/dns)) = 1.1.55 +Provides: bundled(golang(github.com/mholt/acmez/v3)) = 3.1.2 +Provides: bundled(golang(github.com/miekg/dns)) = 1.1.63 Provides: bundled(golang(github.com/mitchellh/copystructure)) = 1.2.0 Provides: bundled(golang(github.com/mitchellh/go-ps)) = 1.0.0 Provides: bundled(golang(github.com/mitchellh/reflectwalk)) = 1.0.2 -Provides: bundled(golang(github.com/onsi/ginkgo/v2)) = 2.9.5 +Provides: bundled(golang(github.com/munnerz/goautoneg)) = a7dc8b6 +Provides: bundled(golang(github.com/pbnjay/memory)) = 7b4eea6 +Provides: bundled(golang(github.com/pires/go-proxyproto)) = 0.8.1 Provides: bundled(golang(github.com/pkg/errors)) = 0.9.1 Provides: bundled(golang(github.com/pmezard/go-difflib)) = 1.0.0 -Provides: bundled(golang(github.com/prometheus/client_golang)) = 1.15.1 -Provides: bundled(golang(github.com/prometheus/client_model)) = 0.4.0 -Provides: bundled(golang(github.com/prometheus/common)) = 0.42.0 -Provides: bundled(golang(github.com/prometheus/procfs)) = 0.9.0 -Provides: bundled(golang(github.com/quic-go/qpack)) = 0.4.0 -Provides: bundled(golang(github.com/quic-go/qtls-go1-20)) = 0.3.4 -Provides: bundled(golang(github.com/quic-go/quic-go)) = 0.39.0 -Provides: bundled(golang(github.com/rs/xid)) = 1.5.0 +Provides: bundled(golang(github.com/prometheus/client_golang)) = 1.23.0 +Provides: bundled(golang(github.com/prometheus/client_model)) = 0.6.2 +Provides: bundled(golang(github.com/prometheus/common)) = 0.65.0 +Provides: bundled(golang(github.com/prometheus/procfs)) = 0.16.1 +Provides: bundled(golang(github.com/quic-go/qpack)) = 0.5.1 +Provides: bundled(golang(github.com/quic-go/quic-go)) = 0.54.0 +Provides: bundled(golang(github.com/rs/xid)) = 1.6.0 Provides: bundled(golang(github.com/russross/blackfriday/v2)) = 2.1.0 -Provides: bundled(golang(github.com/shopspring/decimal)) = 1.2.0 +Provides: bundled(golang(github.com/shopspring/decimal)) = 1.4.0 Provides: bundled(golang(github.com/shurcooL/sanitized_anchor_name)) = 1.0.0 Provides: bundled(golang(github.com/sirupsen/logrus)) = 1.9.3 -Provides: bundled(golang(github.com/slackhq/nebula)) = 1.6.1 -Provides: bundled(golang(github.com/smallstep/certificates)) = 0.25.0 -Provides: bundled(golang(github.com/smallstep/go-attestation)) = cf579e5 -Provides: bundled(golang(github.com/smallstep/nosql)) = 0.6.0 -Provides: bundled(golang(github.com/smallstep/truststore)) = 0.12.1 -Provides: bundled(golang(github.com/spf13/cast)) = 1.4.1 -Provides: bundled(golang(github.com/spf13/cobra)) = 1.7.0 -Provides: bundled(golang(github.com/spf13/pflag)) = 1.0.5 +Provides: bundled(golang(github.com/slackhq/nebula)) = 1.9.5 +Provides: bundled(golang(github.com/smallstep/certificates)) = 0.28.4 +Provides: bundled(golang(github.com/smallstep/cli-utils)) = 0.12.1 +Provides: bundled(golang(github.com/smallstep/go-attestation)) = 2306d5b +Provides: bundled(golang(github.com/smallstep/linkedca)) = 0.23.0 +Provides: bundled(golang(github.com/smallstep/nosql)) = 0.7.0 +Provides: bundled(golang(github.com/smallstep/pkcs7)) = 0.2.1 +Provides: bundled(golang(github.com/smallstep/scep)) = 8cf1ca4 +Provides: bundled(golang(github.com/smallstep/truststore)) = 0.13.0 +Provides: bundled(golang(github.com/spf13/cast)) = 1.7.0 +Provides: bundled(golang(github.com/spf13/cobra)) = 1.9.1 +Provides: bundled(golang(github.com/spf13/pflag)) = 1.0.7 Provides: bundled(golang(github.com/stoewer/go-strcase)) = 1.2.0 -Provides: bundled(golang(github.com/stretchr/testify)) = 1.8.4 -Provides: bundled(golang(github.com/tailscale/tscert)) = 28a91b6 -Provides: bundled(golang(github.com/urfave/cli)) = 1.22.14 +Provides: bundled(golang(github.com/stretchr/testify)) = 1.10.0 +Provides: bundled(golang(github.com/tailscale/tscert)) = d3f8340 +Provides: bundled(golang(github.com/urfave/cli)) = 1.22.17 Provides: bundled(golang(github.com/x448/float16)) = 0.8.4 -Provides: bundled(golang(github.com/yuin/goldmark)) = 1.5.6 +Provides: bundled(golang(github.com/yuin/goldmark)) = 1.7.13 Provides: bundled(golang(github.com/yuin/goldmark-highlighting/v2)) = 37449ab -Provides: bundled(golang(github.com/zeebo/blake3)) = 0.2.3 -Provides: bundled(golang(go.etcd.io/bbolt)) = 1.3.7 -Provides: bundled(golang(go.mozilla.org/pkcs7)) = 33d0574 -Provides: bundled(golang(go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp)) = 0.42.0 -Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/autoprop)) = 0.42.0 -Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/aws)) = 1.17.0 -Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/b3)) = 1.17.0 -Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/jaeger)) = 1.17.0 -Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/ot)) = 1.17.0 -Provides: bundled(golang(go.opentelemetry.io/otel)) = 1.16.0 -Provides: bundled(golang(go.opentelemetry.io/otel/exporters/otlp/internal/retry)) = 1.16.0 -Provides: bundled(golang(go.opentelemetry.io/otel/exporters/otlp/otlptrace)) = 1.16.0 -Provides: bundled(golang(go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc)) = 1.16.0 -Provides: bundled(golang(go.opentelemetry.io/otel/metric)) = 1.16.0 -Provides: bundled(golang(go.opentelemetry.io/otel/sdk)) = 1.16.0 -Provides: bundled(golang(go.opentelemetry.io/otel/trace)) = 1.16.0 -Provides: bundled(golang(go.opentelemetry.io/proto/otlp)) = 0.19.0 -Provides: bundled(golang(go.step.sm/cli-utils)) = 0.8.0 -Provides: bundled(golang(go.step.sm/crypto)) = 0.35.1 -Provides: bundled(golang(go.step.sm/linkedca)) = 0.20.1 -Provides: bundled(golang(go.uber.org/mock)) = 0.3.0 +Provides: bundled(golang(github.com/zeebo/blake3)) = 0.2.4 +Provides: bundled(golang(go.etcd.io/bbolt)) = 1.3.10 +Provides: bundled(golang(go.opentelemetry.io/auto/sdk)) = 1.1.0 +Provides: bundled(golang(go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp)) = 0.61.0 +Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/autoprop)) = 0.62.0 +Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/aws)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/b3)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/jaeger)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/contrib/propagators/ot)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/otel)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/otel/exporters/otlp/otlptrace)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/otel/metric)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/otel/sdk)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/otel/trace)) = 1.37.0 +Provides: bundled(golang(go.opentelemetry.io/proto/otlp)) = 1.7.0 +Provides: bundled(golang(go.step.sm/crypto)) = 0.67.0 +Provides: bundled(golang(go.uber.org/automaxprocs)) = 1.6.0 +Provides: bundled(golang(go.uber.org/mock)) = 0.5.2 Provides: bundled(golang(go.uber.org/multierr)) = 1.11.0 -Provides: bundled(golang(go.uber.org/zap)) = 1.25.0 -Provides: bundled(golang(golang.org/x/crypto)) = 0.14.0 -Provides: bundled(golang(golang.org/x/exp)) = 522b1b5 -Provides: bundled(golang(golang.org/x/mod)) = 0.11.0 -Provides: bundled(golang(golang.org/x/net)) = 0.17.0 -Provides: bundled(golang(golang.org/x/sync)) = 0.4.0 -Provides: bundled(golang(golang.org/x/sys)) = 0.13.0 -Provides: bundled(golang(golang.org/x/term)) = 0.13.0 -Provides: bundled(golang(golang.org/x/text)) = 0.13.0 -Provides: bundled(golang(golang.org/x/tools)) = 0.10.0 -Provides: bundled(golang(google.golang.org/genproto)) = 007df8e -Provides: bundled(golang(google.golang.org/genproto/googleapis/api)) = f966b18 -Provides: bundled(golang(google.golang.org/genproto/googleapis/rpc)) = e6e6cda -Provides: bundled(golang(google.golang.org/grpc)) = 1.58.2 -Provides: bundled(golang(google.golang.org/protobuf)) = 1.31.0 +Provides: bundled(golang(go.uber.org/zap)) = 1.27.0 +Provides: bundled(golang(go.uber.org/zap/exp)) = 0.3.0 +Provides: bundled(golang(golang.org/x/crypto)) = 0.40.0 +Provides: bundled(golang(golang.org/x/crypto/x509roots/fallback)) = 49bf5b8 +Provides: bundled(golang(golang.org/x/exp)) = 7e4ce0a +Provides: bundled(golang(golang.org/x/mod)) = 0.25.0 +Provides: bundled(golang(golang.org/x/net)) = 0.42.0 +Provides: bundled(golang(golang.org/x/oauth2)) = 0.30.0 +Provides: bundled(golang(golang.org/x/sync)) = 0.16.0 +Provides: bundled(golang(golang.org/x/sys)) = 0.34.0 +Provides: bundled(golang(golang.org/x/term)) = 0.33.0 +Provides: bundled(golang(golang.org/x/text)) = 0.27.0 +Provides: bundled(golang(golang.org/x/time)) = 0.12.0 +Provides: bundled(golang(golang.org/x/tools)) = 0.34.0 +Provides: bundled(golang(google.golang.org/api)) = 0.240.0 +Provides: bundled(golang(google.golang.org/genproto/googleapis/api)) = 513f239 +Provides: bundled(golang(google.golang.org/genproto/googleapis/rpc)) = 513f239 +Provides: bundled(golang(google.golang.org/grpc)) = 1.73.0 +Provides: bundled(golang(google.golang.org/grpc/cmd/protoc-gen-go-grpc)) = 1.5.1 +Provides: bundled(golang(google.golang.org/protobuf)) = 1.36.6 Provides: bundled(golang(gopkg.in/natefinch/lumberjack.v2)) = 2.2.1 -Provides: bundled(golang(gopkg.in/square/go-jose.v2)) = 2.6.0 Provides: bundled(golang(gopkg.in/yaml.v3)) = 3.0.1 Provides: bundled(golang(howett.net/plist)) = 1.0.0 @@ -294,6 +310,10 @@ fi %changelog +* Thu Aug 06 2026 PkgAgent Robot - 2.10.2-1 +- [Type] security +- [DESC] Update to 2.10.2 (fixes CVE-2026-27585, CVE-2026-27586, CVE-2026-27587, CVE-2026-27588, CVE-2026-27589, CVE-2026-27590) + * Tue Sep 23 2025 Huang Yang - 2.7.5-4 - [Type] bugfix - [DESC] Fix non-constant format string for hardened build. diff --git a/sources b/sources index 0b27989..30285dc 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (caddy-2.7.5-vendored.tar.gz) = 963c25c171d67e5f564615d3e86f6923fb349a3c5ee2d9a37d598e3f39afdc1f4323416a24acf5e889d20e9ee55d8f61bc44c34191877566a1b693162d419bd3 +SHA512 (caddy-2.10.2-vendored.tar.gz) = cf9865b325a94aaf74c19a74fdbeb5128d9ad854ff55d2002ae033459cbdbfcdb5e4e6254d33a2023c372437e823b11ace28f91f06370a14620c7f605f569ed6 -- Gitee From 757a0892e1779d03664d39d9a998817425624d47 Mon Sep 17 00:00:00 2001 From: pkgagent Date: Fri, 7 Aug 2026 10:07:50 +0800 Subject: [PATCH 2/2] also backport CVE-2026-30851, CVE-2026-30852 Add the remaining two CVE fixes from EPEL 10 (0008/0009) to complete the RHEL 10 / EPEL 10 alignment: - CVE-2026-30851: forward_auth copy_headers does not strip client-supplied identity headers (header injection / privilege escalation) - CVE-2026-30852: repl.ReplaceAll() double-expansion leaks {env.*}/{file.*} values from placeholder-sourced match values Spec now carries Patch7/Patch8; changelog updated to list all 8 CVEs. Co-Authored-By: Claude Opus 4.7 --- caddy-2.10.2-CVE-2026-30851.patch | 630 ++++++++++++++++++++++++++++++ caddy-2.10.2-CVE-2026-30852.patch | 93 +++++ caddy.spec | 4 +- 3 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 caddy-2.10.2-CVE-2026-30851.patch create mode 100644 caddy-2.10.2-CVE-2026-30852.patch diff --git a/caddy-2.10.2-CVE-2026-30851.patch b/caddy-2.10.2-CVE-2026-30851.patch new file mode 100644 index 0000000..6ccc093 --- /dev/null +++ b/caddy-2.10.2-CVE-2026-30851.patch @@ -0,0 +1,630 @@ +From 24dd7026ec1a6fec7ffe447f969e1d2dd11d5193 Mon Sep 17 00:00:00 2001 +From: newklei <105632119+NucleiAv@users.noreply.github.com> +Date: Tue, 3 Mar 2026 23:30:49 -0500 +Subject: [PATCH] forward_auth: `copy_headers` does not strip client-supplied + identity headers (#7545) + +When using copy_headers in a forward_auth block, client-supplied headers with +the same names were not being removed before being forwarded to the backend. + +This happens because PR #6608 added a MatchNot guard that skips the Set +operation when the auth service does not return a given header. That guard +prevents setting headers to empty strings, which is the correct behavior, +but it also means a client can send X-User-Id: admin in their request and +if the auth service validates the token without returning X-User-Id, Caddy +skips the Set and the client value passes through unchanged to the backend. + +The fix adds an unconditional delete route for each copy_headers entry, +placed just before the existing conditional set route. The delete always runs +regardless of what the auth service returns. The conditional set still only +runs when the auth service provides that header. + +The end result is: + - Client-supplied headers are always removed + - When the auth service returns the header, the backend gets that value + - When the auth service does not return the header, the backend sees nothing + +Existing behavior is unchanged for any deployment where the auth service +returns all of the configured copy_headers entries. + +Fixes: GHSA-7r4p-vjf4-gxv4 +Fixes: CVE-2026-30851 + +(cherry picked from commit 2dbcdefbbee68e7b4a31ac66361a0f4e3bcd2eea) +--- + .../forward_auth_authelia.caddyfiletest | 50 ++++- + ...ward_auth_copy_headers_strip.caddyfiletest | 146 +++++++++++++ + .../forward_auth_rename_headers.caddyfiletest | 62 +++++- + caddytest/integration/forwardauth_test.go | 206 ++++++++++++++++++ + .../reverseproxy/forwardauth/caddyfile.go | 18 ++ + 5 files changed, 480 insertions(+), 2 deletions(-) + create mode 100644 caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest + create mode 100644 caddytest/integration/forwardauth_test.go + +diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest +index 240bdc62..831d7d2f 100644 +--- a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest ++++ b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest +@@ -46,6 +46,18 @@ app.example.com { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "Remote-Email" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -73,6 +85,18 @@ app.example.com { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "Remote-Groups" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -100,6 +124,18 @@ app.example.com { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "Remote-Name" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -127,6 +163,18 @@ app.example.com { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "Remote-User" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -200,4 +248,4 @@ app.example.com { + } + } + } +-} +\ No newline at end of file ++} +diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest +new file mode 100644 +index 00000000..887bef0a +--- /dev/null ++++ b/caddytest/integration/caddyfile_adapt/forward_auth_copy_headers_strip.caddyfiletest +@@ -0,0 +1,146 @@ ++:8080 ++ ++forward_auth 127.0.0.1:9091 { ++ uri / ++ copy_headers X-User-Id X-User-Role ++} ++---------- ++{ ++ "apps": { ++ "http": { ++ "servers": { ++ "srv0": { ++ "listen": [ ++ ":8080" ++ ], ++ "routes": [ ++ { ++ "handle": [ ++ { ++ "handle_response": [ ++ { ++ "match": { ++ "status_code": [ ++ 2 ++ ] ++ }, ++ "routes": [ ++ { ++ "handle": [ ++ { ++ "handler": "vars" ++ } ++ ] ++ }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "X-User-Id" ++ ] ++ } ++ } ++ ] ++ }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "set": { ++ "X-User-Id": [ ++ "{http.reverse_proxy.header.X-User-Id}" ++ ] ++ } ++ } ++ } ++ ], ++ "match": [ ++ { ++ "not": [ ++ { ++ "vars": { ++ "{http.reverse_proxy.header.X-User-Id}": [ ++ "" ++ ] ++ } ++ } ++ ] ++ } ++ ] ++ }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "X-User-Role" ++ ] ++ } ++ } ++ ] ++ }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "set": { ++ "X-User-Role": [ ++ "{http.reverse_proxy.header.X-User-Role}" ++ ] ++ } ++ } ++ } ++ ], ++ "match": [ ++ { ++ "not": [ ++ { ++ "vars": { ++ "{http.reverse_proxy.header.X-User-Role}": [ ++ "" ++ ] ++ } ++ } ++ ] ++ } ++ ] ++ } ++ ] ++ } ++ ], ++ "handler": "reverse_proxy", ++ "headers": { ++ "request": { ++ "set": { ++ "X-Forwarded-Method": [ ++ "{http.request.method}" ++ ], ++ "X-Forwarded-Uri": [ ++ "{http.request.uri}" ++ ] ++ } ++ } ++ }, ++ "rewrite": { ++ "method": "GET", ++ "uri": "/" ++ }, ++ "upstreams": [ ++ { ++ "dial": "127.0.0.1:9091" ++ } ++ ] ++ } ++ ] ++ } ++ ] ++ } ++ } ++ } ++ } ++} +\ No newline at end of file +diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest +index c2be2ed4..5d61e5ff 100644 +--- a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest ++++ b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest +@@ -35,6 +35,18 @@ forward_auth localhost:9000 { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "1" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -62,6 +74,18 @@ forward_auth localhost:9000 { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "B" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -89,6 +113,18 @@ forward_auth localhost:9000 { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "3" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -116,6 +152,18 @@ forward_auth localhost:9000 { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "D" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -143,6 +191,18 @@ forward_auth localhost:9000 { + } + ] + }, ++ { ++ "handle": [ ++ { ++ "handler": "headers", ++ "request": { ++ "delete": [ ++ "5" ++ ] ++ } ++ } ++ ] ++ }, + { + "handle": [ + { +@@ -203,4 +263,4 @@ forward_auth localhost:9000 { + } + } + } +-} +\ No newline at end of file ++} +diff --git a/caddytest/integration/forwardauth_test.go b/caddytest/integration/forwardauth_test.go +new file mode 100644 +index 00000000..d0ecc2be +--- /dev/null ++++ b/caddytest/integration/forwardauth_test.go +@@ -0,0 +1,206 @@ ++// Copyright 2015 Matthew Holt and The Caddy Authors ++// ++// Licensed under the Apache License, Version 2.0 (the "License"); ++// you may not use this file except in compliance with the License. ++// You may obtain a copy of the License at ++// ++// http://www.apache.org/licenses/LICENSE-2.0 ++// ++// Unless required by applicable law or agreed to in writing, software ++// distributed under the License is distributed on an "AS IS" BASIS, ++// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++// See the License for the specific language governing permissions and ++// limitations under the License. ++ ++package integration ++ ++import ( ++ "fmt" ++ "net/http" ++ "net/http/httptest" ++ "strings" ++ "sync" ++ "testing" ++ ++ "github.com/caddyserver/caddy/v2/caddytest" ++) ++ ++// TestForwardAuthCopyHeadersStripsClientHeaders is a regression test for the ++// header injection vulnerability in forward_auth copy_headers. ++// ++// When the auth service returns 200 OK without one of the copy_headers headers, ++// the MatchNot guard skips the Set operation. Before this fix, the original ++// client-supplied header survived unchanged into the backend request, allowing ++// privilege escalation with only a valid (non-privileged) bearer token. After ++// the fix, an unconditional delete route runs first, so the backend always ++// sees an absent header rather than the attacker-supplied value. ++func TestForwardAuthCopyHeadersStripsClientHeaders(t *testing.T) { ++ // Mock auth service: accepts any Bearer token, returns 200 OK with NO ++ // identity headers. This is the stateless JWT validator pattern that ++ // triggers the vulnerability. ++ authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { ++ w.WriteHeader(http.StatusOK) ++ return ++ } ++ w.WriteHeader(http.StatusUnauthorized) ++ })) ++ defer authSrv.Close() ++ ++ // Mock backend: records the identity headers it receives. A real application ++ // would use X-User-Id / X-User-Role to make authorization decisions. ++ type received struct{ userID, userRole string } ++ var ( ++ mu sync.Mutex ++ last received ++ ) ++ backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ mu.Lock() ++ last = received{ ++ userID: r.Header.Get("X-User-Id"), ++ userRole: r.Header.Get("X-User-Role"), ++ } ++ mu.Unlock() ++ w.WriteHeader(http.StatusOK) ++ fmt.Fprint(w, "ok") ++ })) ++ defer backendSrv.Close() ++ ++ authAddr := strings.TrimPrefix(authSrv.URL, "http://") ++ backendAddr := strings.TrimPrefix(backendSrv.URL, "http://") ++ ++ tester := caddytest.NewTester(t) ++ tester.InitServer(fmt.Sprintf(` ++ { ++ skip_install_trust ++ admin localhost:2999 ++ http_port 9080 ++ https_port 9443 ++ grace_period 1ns ++ } ++ http://localhost:9080 { ++ forward_auth %s { ++ uri / ++ copy_headers X-User-Id X-User-Role ++ } ++ reverse_proxy %s ++ } ++ `, authAddr, backendAddr), "caddyfile") ++ ++ // Case 1: no token. Auth must still reject the request even when the client ++ // includes identity headers. This confirms the auth check is not bypassed. ++ req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) ++ req.Header.Set("X-User-Id", "injected") ++ req.Header.Set("X-User-Role", "injected") ++ resp := tester.AssertResponseCode(req, http.StatusUnauthorized) ++ resp.Body.Close() ++ ++ // Case 2: valid token, no injected headers. The backend should see absent ++ // identity headers (the auth service never returns them). ++ req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) ++ req.Header.Set("Authorization", "Bearer token123") ++ tester.AssertResponse(req, http.StatusOK, "ok") ++ mu.Lock() ++ gotID, gotRole := last.userID, last.userRole ++ mu.Unlock() ++ if gotID != "" { ++ t.Errorf("baseline: X-User-Id should be absent, got %q", gotID) ++ } ++ if gotRole != "" { ++ t.Errorf("baseline: X-User-Role should be absent, got %q", gotRole) ++ } ++ ++ // Case 3 (the security regression): valid token plus forged identity headers. ++ // The fix must strip those values so the backend never sees them. ++ req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) ++ req.Header.Set("Authorization", "Bearer token123") ++ req.Header.Set("X-User-Id", "admin") // forged ++ req.Header.Set("X-User-Role", "superadmin") // forged ++ tester.AssertResponse(req, http.StatusOK, "ok") ++ mu.Lock() ++ gotID, gotRole = last.userID, last.userRole ++ mu.Unlock() ++ if gotID != "" { ++ t.Errorf("injection: X-User-Id must be stripped, got %q", gotID) ++ } ++ if gotRole != "" { ++ t.Errorf("injection: X-User-Role must be stripped, got %q", gotRole) ++ } ++} ++ ++// TestForwardAuthCopyHeadersAuthResponseWins verifies that when the auth ++// service does include a copy_headers header in its response, that value ++// is forwarded to the backend and takes precedence over any client-supplied ++// value for the same header. ++func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) { ++ const wantUserID = "service-user-42" ++ const wantUserRole = "editor" ++ ++ // Auth service: accepts bearer token and sets identity headers. ++ authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { ++ w.Header().Set("X-User-Id", wantUserID) ++ w.Header().Set("X-User-Role", wantUserRole) ++ w.WriteHeader(http.StatusOK) ++ return ++ } ++ w.WriteHeader(http.StatusUnauthorized) ++ })) ++ defer authSrv.Close() ++ ++ type received struct{ userID, userRole string } ++ var ( ++ mu sync.Mutex ++ last received ++ ) ++ backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ mu.Lock() ++ last = received{ ++ userID: r.Header.Get("X-User-Id"), ++ userRole: r.Header.Get("X-User-Role"), ++ } ++ mu.Unlock() ++ w.WriteHeader(http.StatusOK) ++ fmt.Fprint(w, "ok") ++ })) ++ defer backendSrv.Close() ++ ++ authAddr := strings.TrimPrefix(authSrv.URL, "http://") ++ backendAddr := strings.TrimPrefix(backendSrv.URL, "http://") ++ ++ tester := caddytest.NewTester(t) ++ tester.InitServer(fmt.Sprintf(` ++ { ++ skip_install_trust ++ admin localhost:2999 ++ http_port 9080 ++ https_port 9443 ++ grace_period 1ns ++ } ++ http://localhost:9080 { ++ forward_auth %s { ++ uri / ++ copy_headers X-User-Id X-User-Role ++ } ++ reverse_proxy %s ++ } ++ `, authAddr, backendAddr), "caddyfile") ++ ++ // The client sends forged headers; the auth service overrides them with ++ // its own values. The backend must receive the auth service values. ++ req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) ++ req.Header.Set("Authorization", "Bearer token123") ++ req.Header.Set("X-User-Id", "forged-id") // must be overwritten ++ req.Header.Set("X-User-Role", "forged-role") // must be overwritten ++ tester.AssertResponse(req, http.StatusOK, "ok") ++ ++ mu.Lock() ++ gotID, gotRole := last.userID, last.userRole ++ mu.Unlock() ++ if gotID != wantUserID { ++ t.Errorf("X-User-Id: want %q, got %q", wantUserID, gotID) ++ } ++ if gotRole != wantUserRole { ++ t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole) ++ } ++} +diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go +index 347f6dfb..5d4393ac 100644 +--- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go ++++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go +@@ -208,6 +208,24 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) + for _, from := range sortedHeadersToCopy { + to := http.CanonicalHeaderKey(headersToCopy[from]) + placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from) ++ ++ // Always delete the client-supplied header before conditionally setting ++ // it from the auth response. Without this, a client that pre-supplies a ++ // header listed in copy_headers can inject arbitrary values when the auth ++ // service does not return that header: the MatchNot guard below would ++ // skip the Set entirely, leaving the original client-controlled value ++ // intact and forwarding it to the backend. ++ copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{ ++ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject( ++ &headers.Handler{ ++ Request: &headers.HeaderOps{ ++ Delete: []string{to}, ++ }, ++ }, ++ "handler", "headers", nil, ++ )}, ++ }) ++ + handler := &headers.Handler{ + Request: &headers.HeaderOps{ + Set: http.Header{ diff --git a/caddy-2.10.2-CVE-2026-30852.patch b/caddy-2.10.2-CVE-2026-30852.patch new file mode 100644 index 0000000..8cc75a3 --- /dev/null +++ b/caddy-2.10.2-CVE-2026-30852.patch @@ -0,0 +1,93 @@ +From 6a0c6e84b17754eb92cfb200a58dca8df228f212 Mon Sep 17 00:00:00 2001 +From: "Sam.An" <56215891+sammiee5311@users.noreply.github.com> +Date: Thu, 5 Mar 2026 01:08:39 +0900 +Subject: [PATCH] Only apply repl.ReplaceAll() on values from literal variable + names + +Only apply repl.ReplaceAll() on values from literal variable names +(e.g. map outputs), not on values resolved from placeholder keys +(e.g. {http.request.header.*}). The placeholder path already resolves +the value via repl.Get(), so a second expansion allows user-controlled +input containing {env.*} or {file.*} to be evaluated, leaking +environment variables and file contents. + +Add regression test to verify placeholder-sourced values are not +re-expanded. + +Fixes: CVE-2026-30852 + +(cherry picked from commit 7e83775e3adea8b8da72fea3b159207bd71000dd) +--- + modules/caddyhttp/matchers_test.go | 11 ++++++++++- + modules/caddyhttp/vars.go | 11 ++++++++++- + 2 files changed, 20 insertions(+), 2 deletions(-) + +diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go +index c002a3e4..e91cb44e 100644 +--- a/modules/caddyhttp/matchers_test.go ++++ b/modules/caddyhttp/matchers_test.go +@@ -967,6 +967,7 @@ func TestVarREMatcher(t *testing.T) { + desc string + match MatchVarsRE + input VarsMiddleware ++ headers http.Header + expect bool + expectRepl map[string]string + }{ +@@ -1001,6 +1002,14 @@ func TestVarREMatcher(t *testing.T) { + input: VarsMiddleware{"Var1": "var1Value"}, + expect: true, + }, ++ { ++ desc: "placeholder key value containing braces is not double-expanded", ++ match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: ".+", Name: "val"}}, ++ input: VarsMiddleware{}, ++ headers: http.Header{"X-Input": []string{"{env.HOME}"}}, ++ expect: true, ++ expectRepl: map[string]string{"val.0": "{env.HOME}"}, ++ }, + } { + tc := tc // capture range value + t.Run(tc.desc, func(t *testing.T) { +@@ -1018,7 +1027,7 @@ func TestVarREMatcher(t *testing.T) { + } + + // set up the fake request and its Replacer +- req := &http.Request{URL: new(url.URL), Method: http.MethodGet} ++ req := &http.Request{URL: new(url.URL), Method: http.MethodGet, Header: tc.headers} + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any)) +diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go +index d01f4a43..f19ca16f 100644 +--- a/modules/caddyhttp/vars.go ++++ b/modules/caddyhttp/vars.go +@@ -312,10 +312,12 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + for key, val := range m { + var varValue any ++ var fromPlaceholder bool + if strings.HasPrefix(key, "{") && + strings.HasSuffix(key, "}") && + strings.Count(key, "{") == 1 { + varValue, _ = repl.Get(strings.Trim(key, "{}")) ++ fromPlaceholder = true + } else { + varValue = vars[key] + } +@@ -334,7 +336,14 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) { + varStr = fmt.Sprintf("%v", vv) + } + +- valExpanded := repl.ReplaceAll(varStr, "") ++ // Only expand placeholders in values from literal variable names ++ // (e.g. map outputs). Values resolved from placeholder keys are ++ // already final and must not be re-expanded, as that would allow ++ // user input like {env.SECRET} to be evaluated. ++ valExpanded := varStr ++ if !fromPlaceholder { ++ valExpanded = repl.ReplaceAll(varStr, "") ++ } + if match := val.Match(valExpanded, repl); match { + return match, nil + } diff --git a/caddy.spec b/caddy.spec index 7cf082e..a49a377 100644 --- a/caddy.spec +++ b/caddy.spec @@ -28,6 +28,8 @@ Patch3: caddy-2.10.2-CVE-2026-27587.patch Patch4: caddy-2.10.2-CVE-2026-27588.patch Patch5: caddy-2.10.2-CVE-2026-27589.patch Patch6: caddy-2.10.2-CVE-2026-27590.patch +Patch7: caddy-2.10.2-CVE-2026-30851.patch +Patch8: caddy-2.10.2-CVE-2026-30852.patch BuildRequires: golang >= 1.25 @@ -312,7 +314,7 @@ fi %changelog * Thu Aug 06 2026 PkgAgent Robot - 2.10.2-1 - [Type] security -- [DESC] Update to 2.10.2 (fixes CVE-2026-27585, CVE-2026-27586, CVE-2026-27587, CVE-2026-27588, CVE-2026-27589, CVE-2026-27590) +- [DESC] Update to 2.10.2 (fixes CVE-2026-27585, CVE-2026-27586, CVE-2026-27587, CVE-2026-27588, CVE-2026-27589, CVE-2026-27590, CVE-2026-30851, CVE-2026-30852) * Tue Sep 23 2025 Huang Yang - 2.7.5-4 - [Type] bugfix -- Gitee