Version in base suite: 2.6.2-12 Base version: caddy_2.6.2-12 Target version: caddy_2.6.2-12+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/c/caddy/caddy_2.6.2-12.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/c/caddy/caddy_2.6.2-12+deb13u1.dsc changelog | 10 + control | 2 patches/CVE-2026-27585.patch | 26 +++ patches/CVE-2026-27587-1.patch | 28 ++++ patches/CVE-2026-27587-2.patch | 31 ++++ patches/CVE-2026-27588.patch | 112 ++++++++++++++++ patches/CVE-2026-27589-1.patch | 58 ++++++++ patches/CVE-2026-27589-2.patch | 25 +++ patches/CVE-2026-27589-3.patch | 24 +++ patches/CVE-2026-27589-4.patch | 30 ++++ patches/CVE-2026-27590.patch | 255 +++++++++++++++++++++++++++++++++++++ patches/CVE-2026-45692.patch | 279 +++++++++++++++++++++++++++++++++++++++++ patches/CVE-2026-52845.patch | 48 +++++++ patches/CVE-2026-52846.patch | 146 +++++++++++++++++++++ patches/series | 12 + 15 files changed, 1085 insertions(+), 1 deletion(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp0i8wt6wq/caddy_2.6.2-12.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp0i8wt6wq/caddy_2.6.2-12+deb13u1.dsc: no acceptable signature found diff -Nru caddy-2.6.2/debian/changelog caddy-2.6.2/debian/changelog --- caddy-2.6.2/debian/changelog 2025-05-10 07:19:59.000000000 +0000 +++ caddy-2.6.2/debian/changelog 2026-08-10 04:00:02.000000000 +0000 @@ -1,3 +1,13 @@ +caddy (2.6.2-12+deb13u1) trixie-security; urgency=high + + * Non-maintainer upload by the Security Team. + * Backport upstream fixes for multiple security issues: + CVE-2026-27585, CVE-2026-27587, CVE-2026-27588, CVE-2026-27589, + CVE-2026-27590, CVE-2026-45692, CVE-2026-52845, CVE-2026-52846 + * d/control: change Built-Using to Static-Built-Using (as in 2.11.2-1). + + -- Aron Xu Mon, 10 Aug 2026 12:00:02 +0800 + caddy (2.6.2-12) unstable; urgency=medium [ Guillaume Pernot ] diff -Nru caddy-2.6.2/debian/control caddy-2.6.2/debian/control --- caddy-2.6.2/debian/control 2025-05-10 07:19:59.000000000 +0000 +++ caddy-2.6.2/debian/control 2026-08-10 04:00:02.000000000 +0000 @@ -56,7 +56,7 @@ libnss3-tools, media-types, passwd, -Built-Using: ${misc:Built-Using} +Static-Built-Using: ${misc:Static-Built-Using} Description: Fast, lightweight web server with automatic HTTPS Caddy is a multi purpose web server written in Go. . diff -Nru caddy-2.6.2/debian/patches/CVE-2026-27585.patch caddy-2.6.2/debian/patches/CVE-2026-27585.patch --- caddy-2.6.2/debian/patches/CVE-2026-27585.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27585.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,26 @@ +From: Matthew Holt +Date: Thu, 19 Feb 2026 13:17:19 -0700 +Subject: [PATCH] fileserver: Replace \ with \\ in file matcher paths + +The test hunk of this commit is omitted: it relies on a testdata fixture whose +file name contains a backslash, which cannot be carried in a Debian source +package. Upstream removed that fixture again in cb436f0a. + +Origin: upstream, https://github.com/caddyserver/caddy/commit/a2825c5dd952769f139a16448dc1ca1be61b6058 +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-4xrr-hq4w-6vf4 +Bug-Debian: https://bugs.debian.org/1132041 +Applied-Upstream: 2.11.1 +CVE: CVE-2026-27585 +--- +diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go +index 152f3143..8ced5a19 100644 +--- a/modules/caddyhttp/fileserver/matcher.go ++++ b/modules/caddyhttp/fileserver/matcher.go +@@ -720,6 +720,7 @@ var globSafeRepl = strings.NewReplacer( + "*", "\\*", + "[", "\\[", + "?", "\\?", ++ "\\", "\\\\", + ) + + const ( diff -Nru caddy-2.6.2/debian/patches/CVE-2026-27587-1.patch caddy-2.6.2/debian/patches/CVE-2026-27587-1.patch --- caddy-2.6.2/debian/patches/CVE-2026-27587-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27587-1.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,28 @@ +From: Matthew Holt +Date: Mon, 9 Feb 2026 09:43:07 -0700 +Subject: [PATCH] caddyhttp: Lowercase comparison when matching with escape sequence + +The test hunk of this commit is omitted: it does not apply to 2.6.2's test table +without fuzz. + +Origin: upstream, https://github.com/caddyserver/caddy/commit/bd374ca9d72e296c9361aee76924b6540f22f0c0 +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-g7pc-pc7g-h8jh +Bug-Debian: https://bugs.debian.org/1132041 +Applied-Upstream: 2.11.1 +CVE: CVE-2026-27587 +--- +diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go +index 22976cfb..25666a48 100644 +--- a/modules/caddyhttp/matchers.go ++++ b/modules/caddyhttp/matchers.go +@@ -632,8 +632,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 -Nru caddy-2.6.2/debian/patches/CVE-2026-27587-2.patch caddy-2.6.2/debian/patches/CVE-2026-27587-2.patch --- caddy-2.6.2/debian/patches/CVE-2026-27587-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27587-2.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,31 @@ +From: Matt Holt +Date: Fri, 20 Feb 2026 10:54:50 -0700 +Subject: [PATCH] Merge commit from fork + +Necessary as otherwise the early-bail in `until = +strings.IndexByte(remaining, nextCh) ... if until == -1` can cause a +case-insensitive mismatch + +Co-authored-by: Asim Viladi Oglu Manizada + +The test hunk of this commit is omitted: it does not apply to 2.6.2's test table +without fuzz. + +Origin: upstream, https://github.com/caddyserver/caddy/commit/a1081194bfae4e0d8c227ec44aecb95eded55d1e +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-g7pc-pc7g-h8jh +Bug-Debian: https://bugs.debian.org/1132041 +Applied-Upstream: 2.11.1 +CVE: CVE-2026-27587 +--- +diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go +index eda8cd3a..27e5c5ae 100644 +--- a/modules/caddyhttp/matchers.go ++++ b/modules/caddyhttp/matchers.go +@@ -538,6 +538,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 diff -Nru caddy-2.6.2/debian/patches/CVE-2026-27588.patch caddy-2.6.2/debian/patches/CVE-2026-27588.patch --- caddy-2.6.2/debian/patches/CVE-2026-27588.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27588.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,112 @@ +Description: caddyhttp: normalize host matcher entries for the large-list fast path + MatchHost is documented and implemented as case-insensitive: the linear + matching loop uses strings.EqualFold(). For lists of more than 100 entries, + however, Match() takes a binary-search fast path that compares byte-exactly + ("m[pos] == reqHost"), while Provision() only lowercased entries into its + duplicate-detection map and never wrote the normalized value back into the + slice. After a fast-path miss the linear loop immediately breaks at the first + non-fuzzy entry, so exact hosts are never re-checked case-insensitively. + . + An attacker can therefore bypass host-gated routes, and any access controls + attached to them, merely by changing the case of the Host header. + . + Normalize non-fuzzy (non-wildcard, non-placeholder) entries in Provision() and + fold the request host in the fast path, matching upstream. Wildcard and + placeholder entries are left untouched. +Origin: backport, https://github.com/caddyserver/caddy/commit/eec32a0bb5a11651c6a7b04ce82dc50610f2b27e +Forwarded: not-needed +Reviewed-by: Aron Xu +Last-Update: 2026-08-10 + +--- a/modules/caddyhttp/matchers.go ++++ b/modules/caddyhttp/matchers.go +@@ -243,11 +243,16 @@ func (m MatchHost) Provision(_ caddy.Con + // (we could just remove them, but the user should know their config is erroneous) + seen := make(map[string]int) + for i, h := range m { +- h = strings.ToLower(h) +- if firstI, ok := seen[h]; ok { +- return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, h) ++ normalizedHost := strings.ToLower(h) ++ if firstI, ok := seen[normalizedHost]; ok { ++ return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, normalizedHost) + } +- seen[h] = i ++ // Normalize exact hosts for standardized comparison in large-list fastpath later on. ++ // Keep wildcards/placeholders untouched. ++ if !m.fuzzy(h) { ++ m[i] = normalizedHost ++ } ++ seen[normalizedHost] = i + } + + if m.large() { +@@ -284,14 +289,15 @@ func (m MatchHost) Match(r *http.Request + } + + 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 + } + } +--- a/modules/caddyhttp/matchers_test.go ++++ b/modules/caddyhttp/matchers_test.go +@@ -156,6 +156,48 @@ func TestHostMatcher(t *testing.T) { + } + } + ++func TestLargeHostMatcherIsCaseInsensitive(t *testing.T) { ++ // host matchers with a large number of entries use a binary search ++ // fast path when matching exact hostnames; make sure that fast path ++ // stays case-insensitive like the rest of the matcher ++ const n = 200 ++ matcher := make(MatchHost, 0, n+1) ++ for i := 0; i < n; i++ { ++ matcher = append(matcher, fmt.Sprintf("host-%d.example.com", i)) ++ } ++ matcher[7] = "HOST-7.Example.COM" // config values may be in any case, too ++ matcher = append(matcher, "*.wild.example.com") ++ ++ if err := matcher.Provision(caddy.Context{}); err != nil { ++ t.Fatalf("Provisioning: %v", err) ++ } ++ ++ for i, tc := range []struct { ++ input string ++ expect bool ++ }{ ++ {input: "host-42.example.com", expect: true}, ++ {input: "HOST-42.EXAMPLE.COM", expect: true}, ++ {input: "Host-42.Example.Com", expect: true}, ++ {input: "host-7.example.com", expect: true}, ++ {input: "HOST-7.EXAMPLE.COM", expect: true}, ++ {input: "HOST-42.EXAMPLE.COM:8080", expect: true}, ++ {input: "sub.WILD.example.com", expect: true}, ++ {input: "host-9999.example.com", expect: false}, ++ {input: "not-listed.example.net", expect: false}, ++ } { ++ req := &http.Request{Host: tc.input} ++ repl := caddy.NewReplacer() ++ ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) ++ req = req.WithContext(ctx) ++ ++ actual := matcher.Match(req) ++ if actual != tc.expect { ++ t.Errorf("Test %d: Expected %t, got %t for '%s'", i, tc.expect, actual, tc.input) ++ } ++ } ++} ++ + func TestPathMatcher(t *testing.T) { + for i, tc := range []struct { + match MatchPath // not URI-encoded because not parsing from a URI diff -Nru caddy-2.6.2/debian/patches/CVE-2026-27589-1.patch caddy-2.6.2/debian/patches/CVE-2026-27589-1.patch --- caddy-2.6.2/debian/patches/CVE-2026-27589-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27589-1.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,58 @@ +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. + +Origin: upstream, https://github.com/caddyserver/caddy/commit/42ca010e9d8da35e91edd69e724d3736678b5620 +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-879p-475x-rqh2 +Bug-Debian: https://bugs.debian.org/1132041 +Applied-Upstream: 2.11.1 +CVE: CVE-2026-27589 +--- +diff --git a/admin.go b/admin.go +index 0b8aedf9..ae9bae79 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 -Nru caddy-2.6.2/debian/patches/CVE-2026-27589-2.patch caddy-2.6.2/debian/patches/CVE-2026-27589-2.patch --- caddy-2.6.2/debian/patches/CVE-2026-27589-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27589-2.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,25 @@ +From: Matthew Holt +Date: Wed, 11 Feb 2026 09:52:56 -0700 +Subject: [PATCH] admin: Enforce origin implicitly based on request headers + +Origin: upstream, https://github.com/caddyserver/caddy/commit/72ac479f5d0472425fe150c4aacd03d1030b0077 +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-879p-475x-rqh2 +Bug-Debian: https://bugs.debian.org/1132041 +Applied-Upstream: 2.11.1 +CVE: CVE-2026-27589 +--- +diff --git a/admin.go b/admin.go +index ae9bae79..46f1bbda 100644 +--- a/admin.go ++++ b/admin.go +@@ -849,7 +849,9 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { + } + } + +- if h.enforceOrigin { ++ _, hasOriginHeader := r.Header["Origin"] ++ _, hasSecHeader := r.Header["Sec-Fetch-Mode"] ++ if h.enforceOrigin || hasOriginHeader || hasSecHeader { + // cross-site mitigation + origin, err := h.checkOrigin(r) + if err != nil { diff -Nru caddy-2.6.2/debian/patches/CVE-2026-27589-3.patch caddy-2.6.2/debian/patches/CVE-2026-27589-3.patch --- caddy-2.6.2/debian/patches/CVE-2026-27589-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27589-3.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,24 @@ +From: Matthew Holt +Date: Wed, 25 Mar 2026 16:33:24 -0600 +Subject: [PATCH] Add missing return to` handleError` in admin server + +Thanks to @Wernerina's LLM for finding this bug + +Origin: upstream, https://github.com/caddyserver/caddy/commit/c35ba5588d2ccf85bb3767811a65eac819dd277b +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-879p-475x-rqh2 +Bug-Debian: https://bugs.debian.org/1132041 +Applied-Upstream: 2.11.1 +CVE: CVE-2026-27589 +--- +diff --git a/admin.go b/admin.go +index 5ceb3dae..9c910212 100644 +--- a/admin.go ++++ b/admin.go +@@ -859,6 +859,7 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) { + Err: errors.New("invalid origin 'null'"), + Message: "Buggy browser is sending null Origin header.", + }) ++ return + } + + if h.enforceHost { diff -Nru caddy-2.6.2/debian/patches/CVE-2026-27589-4.patch caddy-2.6.2/debian/patches/CVE-2026-27589-4.patch --- caddy-2.6.2/debian/patches/CVE-2026-27589-4.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27589-4.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,30 @@ +From: Mohammed Al Sahaf +Date: Tue, 9 Jun 2026 02:29:52 +0300 +Subject: [PATCH] admin: fix origin comparison + +Signed-off-by: Mohammed Al Sahaf + +The test hunk of this commit is omitted: it extends admin_security_test.go, +which does not exist in 2.6.2. + +Origin: upstream, https://github.com/caddyserver/caddy/commit/1661cfd901ed876d62b41259f0d2eddfe255bb24 +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-879p-475x-rqh2 +Bug-Debian: https://bugs.debian.org/1132041 +Applied-Upstream: 2.11.1 +CVE: CVE-2026-27589 +--- +diff --git a/admin.go b/admin.go +index 97af846e..5a54753d 100644 +--- a/admin.go ++++ b/admin.go +@@ -978,7 +978,9 @@ func (h adminHandler) originAllowed(origin *url.URL) bool { + if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme { + continue + } +- if origin.Host == allowedOrigin.Host { ++ // Host comparison is case-insensitive per RFC 3986 §3.2.2; url.Parse ++ // does not normalize host case, so fold it here. ++ if strings.EqualFold(origin.Host, allowedOrigin.Host) { + return true + } + } diff -Nru caddy-2.6.2/debian/patches/CVE-2026-27590.patch caddy-2.6.2/debian/patches/CVE-2026-27590.patch --- caddy-2.6.2/debian/patches/CVE-2026-27590.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-27590.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,255 @@ +Description: fastcgi: split the request path on byte offsets, not folded offsets + splitPos() located the split path (e.g. ".php") in strings.ToLower(path) but + returned that index for slicing the *original* path. Unicode case folding can + change a string's length in bytes, so the returned offset could point at an + unrelated position of the original path, yielding a SCRIPT_NAME / + SCRIPT_FILENAME and PATH_INFO that do not correspond to the requested URI. + Where an attacker can influence file contents (upload features and the like), + a request containing ".php" can thereby be made to execute a different + on-disk file, up to remote code execution depending on the deployment. + . + Search the bytes of the path directly using ASCII-only case-insensitive + comparison, so the returned index is always a valid offset into the path. + Bytes >= utf8.RuneSelf are only ever compared for equality, so Unicode + equivalences (fullwidth, circled or mathematical letters folding onto ASCII) + can never make an uploaded file be detected as a script either. + . + This combines upstream 7c28c0c0 with the follow-up hardening in fb324331 + (CVE-2026-45135), which replaced the golang.org/x/text/search fallback that + 7c28c0c0 had introduced. Taking the final upstream state directly avoids + pulling a new module dependency into the package and avoids introducing the + CVE-2026-45135 flaw in the first place. + . + Deliberate deviation from upstream: upstream's Provision() change, which + rejects non-ASCII split_path values outright with a new ErrInvalidSplitPath, + is not backported. On a stable release that would turn a previously accepted + configuration into a hard startup failure; both operands are ASCII-folded at + comparison time instead, which is equivalent for all ASCII split paths. +Origin: backport, https://github.com/caddyserver/caddy/commit/7c28c0c07ac70a8960a166c7126150a408ba7464 +Forwarded: not-needed +Reviewed-by: Aron Xu +Last-Update: 2026-08-10 + +--- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go ++++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +@@ -374,6 +374,23 @@ func (t Transport) buildEnv(r *http.Requ + + // 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") ++// ++// The search is done directly on the bytes of path, using ASCII-only ++// case-insensitive comparison, so that the returned index is always a valid ++// offset into path. Searching a lower-cased copy of path instead would be ++// unsafe: Unicode case folding can change the length in bytes of a string ++// (e.g. "Ⱥ" is 2 bytes but "ⱥ" is 3), so an index found in the lower-cased ++// copy can point at a completely different position of the original path, ++// which yields a wrong SCRIPT_NAME/SCRIPT_FILENAME and PATH_INFO (path ++// confusion). Bytes which are part of a multi-byte UTF-8 sequence are only ++// ever compared for byte equality, so Unicode equivalences (e.g. fullwidth, ++// circled or mathematical letters folding to ASCII) can never make an ++// uploaded file be detected as a script either. ++// ++// 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 { +@@ -383,15 +400,42 @@ func (t Transport) splitPos(path string) + return 0 + } + +- lowerPath := strings.ToLower(path) ++ pathLen := len(path) ++ + for _, split := range t.SplitPath { +- if idx := strings.Index(lowerPath, strings.ToLower(split)); idx > -1 { +- return idx + len(split) ++ splitLen := len(split) ++ if splitLen > pathLen { ++ continue ++ } ++ ++ for i := 0; i <= pathLen-splitLen; i++ { ++ match := true ++ for j := 0; j < splitLen; j++ { ++ if asciiToLower(path[i+j]) != asciiToLower(split[j]) { ++ match = false ++ break ++ } ++ } ++ if match { ++ return i + splitLen ++ } + } + } ++ + return -1 + } + ++// asciiToLower maps ASCII upper-case letters to their lower-case counterpart. ++// Every other byte, including any byte of a multi-byte UTF-8 sequence (which ++// is always >= 0x80, hence never in the A-Z range), is returned unchanged, so ++// this never changes the length of the values being compared. ++func asciiToLower(c byte) byte { ++ if 'A' <= c && c <= 'Z' { ++ return c + 'a' - 'A' ++ } ++ return c ++} ++ + type envVars map[string]string + + // loggableEnv is a simple type to allow for speeding up zap log encoding. +--- /dev/null ++++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go +@@ -0,0 +1,148 @@ ++// 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 fastcgi ++ ++import "testing" ++ ++func TestSplitPos(t *testing.T) { ++ for _, tc := range []struct { ++ name string ++ path string ++ splitPath []string ++ want int ++ }{ ++ { ++ name: "simple php extension", ++ path: "/path/to/script.php", ++ splitPath: []string{".php"}, ++ want: 19, ++ }, ++ { ++ name: "php extension with path info", ++ path: "/path/to/script.php/some/path", ++ splitPath: []string{".php"}, ++ want: 19, ++ }, ++ { ++ name: "case insensitive match", ++ path: "/path/to/script.PhP/info", ++ splitPath: []string{".php"}, ++ want: 19, ++ }, ++ { ++ name: "case insensitive split path", ++ path: "/path/to/script.php/info", ++ splitPath: []string{".PHP"}, ++ want: 19, ++ }, ++ { ++ name: "no match", ++ path: "/path/to/script.txt", ++ splitPath: []string{".php"}, ++ want: -1, ++ }, ++ { ++ name: "no split path configured", ++ path: "/path/to/script.php", ++ splitPath: []string{}, ++ want: 0, ++ }, ++ { ++ name: "multiple split paths, second one matches", ++ path: "/path/to/script.phtml", ++ splitPath: []string{".php", ".phtml"}, ++ want: 21, ++ }, ++ { ++ name: "extension in the middle of the file name", ++ path: "/test.php.bak", ++ splitPath: []string{".php"}, ++ want: 9, ++ }, ++ // The following cases used to be computed on a lower-cased copy of ++ // the path; because Unicode case folding may change the length in ++ // bytes of a string, the resulting index did not refer to the same ++ // position in the original path (path confusion). ++ // "Ⱥ" (U+023A) is 2 bytes long, but "ⱥ" (U+2C65) is 3 bytes long. ++ { ++ name: "unicode path with case folding length expansion", ++ path: "/ȺȺȺȺshell.php.txt.php", ++ splitPath: []string{".php"}, ++ want: 18, ++ }, ++ { ++ name: "unicode path with path info", ++ path: "/ȺȺȺȺshell.php/path/info", ++ splitPath: []string{".php"}, ++ want: 18, ++ }, ++ { ++ name: "unicode path with upper case extension", ++ path: "/ȺȺȺȺshell.PHP", ++ splitPath: []string{".php"}, ++ want: 18, ++ }, ++ { ++ name: "unicode in the middle of the path", ++ path: "/path/Ⱥtest/script.php", ++ splitPath: []string{".php"}, ++ want: 23, ++ }, ++ { ++ name: "unicode in a directory name", ++ path: "/Ⱥ/script.php", ++ splitPath: []string{".php"}, ++ want: 14, ++ }, ++ { ++ // "İ" (U+0130) is 2 bytes long, but lower-casing it yields ++ // "i" followed by a combining dot above, i.e. 3 bytes. ++ name: "turkish capital I with dot", ++ path: "/İtest.php", ++ splitPath: []string{".php"}, ++ want: 11, ++ }, ++ { ++ // Unicode equivalences must not be used to have a file ++ // detected as a script: "p" (U+FF50) is not "p". ++ name: "fullwidth letter does not match", ++ path: "/shell.php", ++ splitPath: []string{".php"}, ++ want: -1, ++ }, ++ { ++ name: "split path longer than the path", ++ path: "/x", ++ splitPath: []string{".php"}, ++ want: -1, ++ }, ++ } { ++ t.Run(tc.name, func(t *testing.T) { ++ got := Transport{SplitPath: tc.splitPath}.splitPos(tc.path) ++ if got != tc.want { ++ t.Errorf("splitPos(%q) with split path %q: got %d, want %d", ++ tc.path, tc.splitPath, got, tc.want) ++ } ++ ++ // whatever happens, the returned index must always be a valid ++ // offset into the path, otherwise slicing the path panics or ++ // yields a script name that was never requested ++ if got > len(tc.path) { ++ t.Fatalf("splitPos(%q) returned %d, which is out of bounds (len=%d)", ++ tc.path, got, len(tc.path)) ++ } ++ }) ++ } ++} diff -Nru caddy-2.6.2/debian/patches/CVE-2026-45692.patch caddy-2.6.2/debian/patches/CVE-2026-45692.patch --- caddy-2.6.2/debian/patches/CVE-2026-45692.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-45692.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,279 @@ +Description: admin: agree on config paths between authorization and traversal + The remote admin API's authorization layer and the /config traversal layer did + not agree on which object a path refers to. enforceAccessControls() authorized + request paths with a plain strings.HasPrefix() against the configured + access_control paths, so "/config/.../srv" also authorized "/config/.../srv0"; + and unsyncedConfigAccess() parsed array indices with strconv.Atoi(), which + accepts non-canonical spellings such as "01", "+1" and "-0". A path that was + authorized for one config object could therefore resolve to a different config + object during traversal. + . + Require a path-segment boundary in the access check (upstream 2d332714) and + reject non-canonical array indices (upstream 18ab0f95). +Origin: backport, https://github.com/caddyserver/caddy/commit/2d332714829aa3b530424d47ae092074b09a2268 +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-x5w9-xh9r-mvfc +Bug-Debian: https://bugs.debian.org/1140773 +Forwarded: not-needed +Reviewd-by: Aron Xu +Last-Update: 2026-08-10 + +--- a/admin.go ++++ b/admin.go +@@ -185,8 +185,8 @@ type AdminAccess struct { + // AdminPermissions specifies what kinds of requests are allowed + // to be made to the admin endpoint. + type AdminPermissions struct { +- // The API paths allowed. Paths are simple prefix matches. +- // Any subpath of the specified paths will be allowed. ++ // The API paths allowed. A request path must either equal an ++ // allowed path or be a subpath with a path-segment boundary. + Paths []string `json:"paths,omitempty"` + + // The HTTP methods allowed for the given paths. +@@ -647,7 +647,7 @@ func (remote RemoteAdmin) enforceAccessC + // verify path + pathFound := accessPerm.Paths == nil + for _, allowedPath := range accessPerm.Paths { +- if strings.HasPrefix(r.URL.Path, allowedPath) { ++ if adminPathAllowed(r.URL.Path, allowedPath) { + pathFound = true + break + } +@@ -676,6 +676,24 @@ func (remote RemoteAdmin) enforceAccessC + } + } + ++// adminPathAllowed returns true if reqPath is covered by allowedPath. ++// A request path is covered only if it is exactly the allowed path or ++// is a subpath of it starting at a path-segment boundary; a bare string ++// prefix match is not sufficient, since e.g. "/foo/1" is a prefix of ++// "/foo/10" but refers to a different resource. ++func adminPathAllowed(reqPath, allowedPath string) bool { ++ if allowedPath == "" || allowedPath == "/" { ++ return strings.HasPrefix(reqPath, allowedPath) ++ } ++ if reqPath == allowedPath { ++ return true ++ } ++ if strings.HasSuffix(allowedPath, "/") { ++ return strings.HasPrefix(reqPath, allowedPath) ++ } ++ return strings.HasPrefix(reqPath, allowedPath+"/") ++} ++ + func stopAdminServer(srv *http.Server) error { + if srv == nil { + return fmt.Errorf("no admin server") +@@ -1058,6 +1076,25 @@ func handleStop(w http.ResponseWriter, r + return nil + } + ++// parseCanonicalArrayIndex parses idx as an array index, rejecting any ++// spelling that is not the canonical decimal representation of the number ++// (e.g. "01", "+1" or "-0"). Non-canonical spellings would let a request ++// path that is authorized for one array element resolve to a different ++// element during config traversal. ++func parseCanonicalArrayIndex(idx string) (int, error) { ++ if idx == "" { ++ return 0, fmt.Errorf("empty index") ++ } ++ i, err := strconv.Atoi(idx) ++ if err != nil { ++ return 0, err ++ } ++ if strconv.Itoa(i) != idx { ++ return 0, fmt.Errorf("non-canonical array index") ++ } ++ return i, nil ++} ++ + // unsyncedConfigAccess traverses into the current config and performs + // the operation at path according to method, using body and out as + // needed. This is a low-level, unsynchronized function; most callers +@@ -1116,7 +1153,7 @@ traverseLoop: + var idx int + if method != http.MethodPost { + idxStr := parts[len(parts)-1] +- idx, err = strconv.Atoi(idxStr) ++ idx, err = parseCanonicalArrayIndex(idxStr) + if err != nil { + return fmt.Errorf("[%s] invalid array index '%s': %v", + path, idxStr, err) +@@ -1208,7 +1245,7 @@ traverseLoop: + } + + case []any: +- partInt, err := strconv.Atoi(part) ++ partInt, err := parseCanonicalArrayIndex(part) + if err != nil { + return fmt.Errorf("[/%s] invalid array index '%s': %v", + strings.Join(parts[:i+1], "/"), part, err) +--- a/admin_test.go ++++ b/admin_test.go +@@ -15,9 +15,15 @@ + package caddy + + import ( ++ "bytes" ++ "crypto" ++ "crypto/tls" ++ "crypto/x509" + "encoding/json" ++ "errors" + "fmt" + "net/http" ++ "net/http/httptest" + "reflect" + "sync" + "testing" +@@ -192,6 +198,150 @@ func TestETags(t *testing.T) { + } + } + ++type testAdminPublicKey string ++ ++func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool { ++ other, ok := x.(testAdminPublicKey) ++ return ok && k == other ++} ++ ++func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) { ++ const authorizedKey testAdminPublicKey = "authorized" ++ peerCert := &x509.Certificate{PublicKey: authorizedKey} ++ ++ tests := []struct { ++ name string ++ allowedPath string ++ requestPath string ++ wantErr bool ++ }{ ++ { ++ name: "exact path", ++ allowedPath: "/pki/ca/prod", ++ requestPath: "/pki/ca/prod", ++ wantErr: false, ++ }, ++ { ++ name: "subpath", ++ allowedPath: "/pki/ca/prod", ++ requestPath: "/pki/ca/prod/certificates", ++ wantErr: false, ++ }, ++ { ++ name: "trailing slash subpath", ++ allowedPath: "/pki/ca/prod/", ++ requestPath: "/pki/ca/prod/certificates", ++ wantErr: false, ++ }, ++ { ++ name: "sibling with shared prefix", ++ allowedPath: "/pki/ca/prod", ++ requestPath: "/pki/ca/prod-backup", ++ wantErr: true, ++ }, ++ { ++ name: "same segment plus digit", ++ allowedPath: "/pki/ca/prod", ++ requestPath: "/pki/ca/prod1", ++ wantErr: true, ++ }, ++ { ++ name: "root path", ++ allowedPath: "/", ++ requestPath: "/pki/ca/prod", ++ wantErr: false, ++ }, ++ } ++ ++ for i, test := range tests { ++ t.Run(test.name, func(t *testing.T) { ++ remote := RemoteAdmin{ ++ AccessControl: []*AdminAccess{ ++ { ++ Permissions: []AdminPermissions{ ++ { ++ Methods: []string{http.MethodGet}, ++ Paths: []string{test.allowedPath}, ++ }, ++ }, ++ publicKeys: []crypto.PublicKey{authorizedKey}, ++ }, ++ }, ++ } ++ ++ req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil) ++ req.TLS = &tls.ConnectionState{ ++ VerifiedChains: [][]*x509.Certificate{{peerCert}}, ++ } ++ ++ err := remote.enforceAccessControls(req) ++ if test.wantErr { ++ if err == nil { ++ t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath) ++ return ++ } ++ var apiErr APIError ++ if !errors.As(err, &apiErr) { ++ t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err) ++ return ++ } ++ if apiErr.HTTPStatus != http.StatusForbidden { ++ t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus) ++ } ++ return ++ } ++ ++ if err != nil { ++ t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err) ++ } ++ }) ++ } ++} ++ ++func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) { ++ rawCfg = map[string]any{ ++ rawConfigKey: map[string]any{ ++ "list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}, ++ }, ++ } ++ ++ tests := []struct { ++ name string ++ path string ++ wantOutput string ++ wantErr bool ++ }{ ++ {name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"}, ++ {name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"}, ++ {name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"}, ++ {name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true}, ++ {name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true}, ++ {name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true}, ++ {name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true}, ++ } ++ ++ for i, tc := range tests { ++ t.Run(tc.name, func(t *testing.T) { ++ var gotOutput bytes.Buffer ++ err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput) ++ ++ if tc.wantErr { ++ if err == nil { ++ t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String()) ++ } ++ return ++ } ++ ++ if err != nil { ++ t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String()) ++ } ++ if gotOutput.String() != tc.wantOutput { ++ t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String()) ++ } ++ }) ++ } ++} ++ + func BenchmarkLoad(b *testing.B) { + for i := 0; i < b.N; i++ { + Load(testCfg, true) diff -Nru caddy-2.6.2/debian/patches/CVE-2026-52845.patch caddy-2.6.2/debian/patches/CVE-2026-52845.patch --- caddy-2.6.2/debian/patches/CVE-2026-52845.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-52845.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,48 @@ +Description: caddyhttp: drop request headers whose names contain an underscore + forward_auth's copy_headers deletes the exact client-supplied identity header + before copying the trusted value from the auth gateway. The FastCGI transport, + however, normalizes header names into CGI variables by replacing '-' with '_', + so a client-supplied "Remote_User" survives the delete step for "Remote-User" + and then collides with it in env["HTTP_REMOTE_USER"], where the winner is + decided by Go's randomized map iteration order. A remote client can thereby + inject or override identity and group headers trusted by PHP/FastCGI + applications behind Caddy. +Origin: backport, https://github.com/caddyserver/caddy/commit/3eb8e48ff052e1ad16d88c683672c306d2077a11 +Applied-Upstream: 2.11.4 +Forwarded: not-needed +Reviewed-by: Aron Xu +Last-Update: 2026-08-10 +--- +--- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go ++++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +@@ -466,7 +466,7 @@ var tlsProtocolStrings = map[uint16]stri + tls.VersionTLS13: "TLSv1.3", + } + +-var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") ++var headerNameReplacer = strings.NewReplacer("-", "_") + + // Interface guards + var ( +--- a/modules/caddyhttp/server.go ++++ b/modules/caddyhttp/server.go +@@ -281,6 +281,19 @@ func (s *Server) ServeHTTP(w http.Respon + return + } + ++ // Drop headers whose names contain `_`: once FastCGI/CGI/FrankenPHP etc. rewrites `-` to ++ // `_`, an underscore alias collides with the legitimate hyphenated header ++ // and can bypass `forward_auth copy_headers` (GHSA-f59h-q822-g45g). ++ for k := range r.Header { ++ if strings.ContainsRune(k, '_') { ++ delete(r.Header, k) ++ ++ if c := s.logger.Check(zapcore.DebugLevel, "dropping header containing underscore"); c != nil { ++ c.Write(zap.String("header", k)) ++ } ++ } ++ } ++ + // execute the primary handler chain + err := s.primaryHandlerChain.ServeHTTP(w, r) + duration = time.Since(start) diff -Nru caddy-2.6.2/debian/patches/CVE-2026-52846.patch caddy-2.6.2/debian/patches/CVE-2026-52846.patch --- caddy-2.6.2/debian/patches/CVE-2026-52846.patch 1970-01-01 00:00:00.000000000 +0000 +++ caddy-2.6.2/debian/patches/CVE-2026-52846.patch 2026-08-10 04:00:02.000000000 +0000 @@ -0,0 +1,146 @@ +From: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> +Date: Tue, 2 Jun 2026 03:35:02 +0800 +Subject: [PATCH] templates: Patch for GHSA-vcc4-2c75-vc9v (#7785) + +* Patch GHSA-vcc4-2c75-vc9v in stripHTML + +templates: fix funcStripHTML bypass via depth counter + +The previous false-start approach allowed XSS bypass via inputs like <<>img src=x onerror=alert(1)> and failed on stacked angle brackets. + +Replace the tagStart/inTag state machine with a depth counter that mirrors PHP strip_tags behaviour: each '<' increments depth, each '>' decrements it, and text is only emitted at depth zero. Quoted attribute values (both single and double) are tracked so '>' inside href values does not prematurely close a tag. + +Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> + +* Update tplcontext_test.go + +Templates: expand TestStripHTML with attack path coverage + +Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> + +--------- + +Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> + +Origin: upstream, https://github.com/caddyserver/caddy/commit/e2eee6a7fce366321294c9c2a79f3146891dcbdf +Bug: https://github.com/caddyserver/caddy/security/advisories/GHSA-vcc4-2c75-vc9v +Bug-Debian: https://bugs.debian.org/1140773 +Applied-Upstream: 2.11.4 +CVE: CVE-2026-52846 +--- +diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go +index ee553e7a..4e8ec925 100644 +--- a/modules/caddyhttp/templates/tplcontext.go ++++ b/modules/caddyhttp/templates/tplcontext.go +@@ -312,35 +312,32 @@ func (c TemplateContext) Host() (string, error) { + return host, nil + } + +-// funcStripHTML returns s without HTML tags. It is fairly naive +-// but works with most valid HTML inputs. ++// funcStripHTML returns s without HTML tags. Similar to PHP's strip_tags() + func (TemplateContext) funcStripHTML(s string) string { + var buf bytes.Buffer +- var inTag, inQuotes bool +- var tagStart int +- for i, ch := range s { +- if inTag { +- if ch == '>' && !inQuotes { +- inTag = false +- } else if ch == '<' && !inQuotes { +- // false start +- buf.WriteString(s[tagStart:i]) +- tagStart = i +- } else if ch == '"' { +- inQuotes = !inQuotes ++ depth := 0 ++ var quoteChar rune ++ for _, ch := range s { ++ switch { ++ case depth > 0 && quoteChar == 0 && (ch == '"' || ch == '\''): ++ // entering a quoted attribute value ++ quoteChar = ch ++ case depth > 0 && ch == quoteChar: ++ // leaving a quoted attribute value ++ quoteChar = 0 ++ case ch == '<' && quoteChar == 0: ++ depth++ ++ case ch == '>' && quoteChar == 0: ++ if depth > 0 { ++ depth-- ++ } else { ++ buf.WriteRune(ch) // stray '>' with no opening '<', keep it ++ } ++ default: ++ if depth == 0 { ++ buf.WriteRune(ch) + } +- continue +- } +- if ch == '<' { +- inTag = true +- tagStart = i +- continue + } +- buf.WriteRune(ch) +- } +- if inTag { +- // false start +- buf.WriteString(s[tagStart:]) + } + return buf.String() + } +diff --git a/modules/caddyhttp/templates/tplcontext_test.go b/modules/caddyhttp/templates/tplcontext_test.go +index 67ebbac7..1ff6caef 100644 +--- a/modules/caddyhttp/templates/tplcontext_test.go ++++ b/modules/caddyhttp/templates/tplcontext_test.go +@@ -419,14 +419,44 @@ func TestStripHTML(t *testing.T) { + expect: `h1`, + }, + { +- // tags not closed ++ // unclosed tag — trailing text must be stripped, not emitted + input: `hi`, +- expect: `' only closes one level ++ input: `hi`, ++ expect: ``, ++ }, ++ { ++ // XSS bypass via double opening bracket ++ input: `<<>img src=x onerror=alert('XSS')>`, ++ expect: ``, ++ }, ++ { ++ // stacked angle brackets (PHP strip_tags parity) ++ input: `<<<<<>>>>>hello`, ++ expect: `hello`, ++ }, ++ { ++ // unclosed tag strips trailing text ++ input: `hello ' inside double-quoted attribute must not close tag early ++ input: `text`, ++ expect: `text`, ++ }, ++ { ++ // '>' inside single-quoted attribute must not close tag early ++ input: `text`, ++ expect: `text`, ++ }, ++ { ++ // stray '>' with no opening '<' is preserved ++ input: `stray > bracket`, ++ expect: `stray > bracket`, + }, + } { + actual := tplContext.funcStripHTML(test.input) diff -Nru caddy-2.6.2/debian/patches/series caddy-2.6.2/debian/patches/series --- caddy-2.6.2/debian/patches/series 2025-03-18 10:24:55.000000000 +0000 +++ caddy-2.6.2/debian/patches/series 2026-08-10 04:00:02.000000000 +0000 @@ -5,3 +5,15 @@ 0005-Remove-obsolote-subcommands.patch 0006-Add-variable-for-reproducible-manpage-time-stamps.patch 0007-quic-go-0.50.0.patch +CVE-2026-27585.patch +CVE-2026-27587-1.patch +CVE-2026-27587-2.patch +CVE-2026-27588.patch +CVE-2026-27589-1.patch +CVE-2026-27589-2.patch +CVE-2026-27589-3.patch +CVE-2026-27589-4.patch +CVE-2026-27590.patch +CVE-2026-45692.patch +CVE-2026-52845.patch +CVE-2026-52846.patch