Commit Graph
2701 Commits
Author SHA1 Message Date
Abdellatif Anaflous 69ec5dfedf caddyauth: only replace known placeholders in basic auth credentials (#8017)
The basic auth provider expanded account usernames and passwords with
repl.ReplaceAll, which blanks any {...} the replacer does not recognize

Credentials are config data, so a username like alice-{bogus} was stored
as alice- and a password like ab{cd}ef was quietly rewritten into
different valid base64, turning a config mistake into a working
password the admin never set

Use ReplaceKnown, matching the respond headers fix in #8014 and the
header handler fix in #4880, so unknown braces survive and real
placeholders like {env.*} still expand
2026-09-26 18:52:53 +10:00
Mohammed Al SahafandZen Dodd fbc88ae232 internal: fix MaxSizeSubjectsListForLog off-by-one when maxToDisplay is 0 (#7970)
* internal: fix MaxSizeSubjectsListForLog off-by-one when maxToDisplay is 0

The loop appended before checking the length, so with maxToDisplay=0
one domain leaked into the output before the break check. Reorder so
the length check comes first.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* Update internal/logs.go

Co-authored-by: Zen Dodd <mail@steadytao.com>
Signed-off-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>

* fix doc

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Signed-off-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-26 18:51:36 +10:00
Abdellatif Anaflousandhktitof 81a0b6d838 requestbody: replace only known placeholders in the set body (#8008)
The set body is user-supplied and commonly JSON, but it was expanded with
ReplaceAll, which blanks any {...} the replacer does not recognize, so a
plain JSON body was sent empty and a body mixing JSON with a real
placeholder was mangled

Use ReplaceKnown, matching the respond body, so unrecognized braces are
left intact and real placeholders still expand. Regression test pins both
directions

Co-authored-by: hktitof <hktitof@users.noreply.github.com>
2026-09-26 18:50:06 +10:00
5493791423 caddyhttp: fix randString sameCase dictionary to match its docs (#7972)
* caddyhttp: fix randString sameCase dictionary to match its docs

The doc comment says randString excludes confusing characters like
I, l, 1, 0, O. When sameCase is true, uppercase letters and the
characters l and o should be excluded. But the sameCase dictionary
still contained '0'. Drop it.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* Update modules/caddyhttp/errors.go

Signed-off-by: Matt Holt <mholt@users.noreply.github.com>

* elaborate the doc, and expand tests

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* Update modules/caddyhttp/errors_randstring_test.go

Co-authored-by: Zen Dodd <mail@steadytao.com>
Signed-off-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Signed-off-by: Matt Holt <mholt@users.noreply.github.com>
Signed-off-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-25 16:01:50 +00:00
WeidiDeng 89db4b75c3 reverseproxy: Fix partial response not flushed to clients (#7849)
* flush buffer before aborting
* add tests
* close test http server after tests

---------

Signed-off-by: WeidiDeng <weidi_deng@icloud.com>
2026-09-25 22:36:28 +10:00
Abdellatif Anaflous 4845150fa6 caddyhttp: surface 413 for oversized request body placeholders (#7969)
when a request exceeds the request_body max_size limit, the request_body handler wraps the http.MaxBytesError into a caddyhttp.HandlerError carrying status 413, but the {http.request.body} and {http.request.body_base64} placeholders ran io.Copy with the error ignored, so they silently returned the truncated prefix as though it were the complete body. docs promise a 413 for reads past max_size, and silently truncating is a bad failure mode in templates and vars_regexp where the body value drives decisions. see #7691 and the narrow follow-up prescribed when #7692 was closed

reading the body now returns a dedicated RequestBodyLimitError marker instead of a generic HandlerError, and only when the read failure is actually the max_size limit. the consumers (template placeholder function, vars and vars_regexp matchers) recognize exactly that marker and wrap it in a status carrying handler error so the oversized request fails with 413, while every unrelated error value keeps its old behavior: templates still render it as text and the vars matchers still match on its error text. the surfaced error is stripped of its generated id and stack trace so a placeholder stringified into a response body cannot leak the call stack

negative regressions prove an unrelated HandlerError in templates, vars and vars_regexp does not start controlling request handling, plus integration tests for the template and vars_regexp 413 paths

ai assisted (GLM agent) under Abdel's direction and local verification

Signed-off-by: Abdel <hktitof@gmail.com>
2026-09-25 22:27:19 +10:00
Mohammed Al Sahaf 8c0631bb36 chore: remove AI moderator workflow (#8059)
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-09-25 14:05:42 +10:00
7ee4441f92 reverseproxy: Move websocket header normalization later (#7921)
* Move websocket header normalization later

A regression since #6621, these headers can be rewritten by cloneHeaders
and by .Add from user and transport ops. Moving this closer to where
it's round-tripped feels like the nicest solution.

I've put the harness I used to reproduce the issue at
  https://codeberg.org/nfreya/caddy-websocket-headers-repro

* Integration test for normalized websocket headers

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-09-23 14:09:48 -06:00
c798d4c238 fastcgi: explain the 411 a body without a length gets (#7956)
A request whose body has no length of its own — Transfer-Encoding: chunked, or
HTTP/2 and HTTP/3 requests sent without the header — cannot be forwarded over
FastCGI until it has been buffered in full, because CGI/1.1 requires
CONTENT_LENGTH and php-fpm hangs when it is absent or wrong and the body is not
empty. When request_buffers is too small to hold the whole body, the length
stays unknown and the request is refused with 411.

The refusal said none of that. RoundTrip passes r.ContentLength to Post, which
is -1 for such a request, so the operator got a bare "411 Length Required" with
either no error at all or strconv's "invalid syntax" on a value they never
wrote. On #7386 that sent two people looking for the fault in their backend.

The 411 now carries the reason and the remedy, and the unknown-length case is
told apart from a genuinely malformed value. ParseUint becomes ParseInt plus an
explicit negative check, which is strictly tighter: values above MaxInt64 used
to be accepted and are unreachable anyway, since CONTENT_LENGTH is always
FormatInt of an int64 by the time Do sees it.

No status code changes. Every request that was refused before is still refused;
raising request_buffers is still what makes a large chunked body work.

Tests: the reachable path through Post, the unusable CONTENT_LENGTH values Do
itself guards against, an integration test driving a chunked body through a
fastcgi reverse_proxy over a unix socket at each side of the buffer boundary
(including request_buffers -1, which succeeds at any size and is the remedy),
and a boundary test recording that a body exactly the size of the buffer counts
as partial.

That last one is deliberately not changed here. Telling "exactly the limit"
apart from "more to come" costs a read that a paused stream may never answer,
and bufferedBody also backs response_buffers: measured against a body that
delivers exactly the limit and stops, the current code hands the buffered
prefix on immediately, while peeking one byte first delivers nothing at all.

Co-authored-by: Aditya <205600203+Rohilalala@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-23 09:31:54 -06:00
amir darabyandMatt Holt 7e82a6b528 admin: fix warnings and status code in /load API (#7267)
* admin: fix warnings and status code in /load API

* fix admin.go: gci import formatting

---------

Signed-off-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-09-23 08:55:38 -06:00
Timo StarkandFaiyaz Rahman c18099a0af reverse_proxy with versions 3 silently ignores tls_trust_pool and verifies against the system roots (#8042)
* reverseproxy: build TLS client config once so HTTP/3 keeps tls_trust_pool

reuse the config built for the HTTP/1.1+2 transport and give the HTTP/3
transport a clone of it to honour the tls_trust_pool

Signed-off-by: Timo Stark <tippexs91@googlemail.com>
Co-authored-by: Faiyaz Rahman <faiyazrahman03@gmail.com>

* Adding issue reference

---------

Signed-off-by: Timo Stark <tippexs91@googlemail.com>
Co-authored-by: Faiyaz Rahman <faiyazrahman03@gmail.com>
2026-09-22 08:13:02 +10:00
X 128b9e75e0 core: synchronize Stop with concurrent config reloads (#8038)
Signed-off-by: hendrixx-cnc <tjhendrx@icloud.com>
2026-09-19 16:02:47 +00:00
techknowlogick df77f8bde1 chore(deps): bump cel-go and switch import to new url (#8030) 2026-09-17 04:09:23 +03:00
XiaoleC05 c1645c544d caddyfile: Expand imports inside named routes (#7986) 2026-09-16 09:09:57 -04:00
dependabot[bot] 894442fe86 build(deps): bump google.golang.org/grpc from 1.83.1 to 1.83.2 (#8028)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-16 08:14:12 -04:00
Turhan ACAR 9be8fb2794 reverseproxy: propagate TCP half-close on upgraded streams (#8027)
* reverseproxy: propagate TCP half-close on upgraded streams

handleUpgradeResponse starts one copy goroutine per direction but returns as
soon as the first one reports a result, and returning runs two unconditional
deferred closes (the client connection and the backend connection). A client
that ends its send direction with a TCP half-close therefore causes the whole
tunnel to be torn down: the backend connection is reset mid-conversation and
whatever it was about to send is never delivered.

Treat a clean EOF as the end of one direction rather than the end of the
tunnel. When a copy finishes without error, propagate the half-close to the
destination with CloseWrite where the connection supports it, and keep waiting
for the other direction so pending bytes can still drain. Real copy errors, the
stream timeout and request cancellation/shutdown still tear down immediately.

If the destination does not support CloseWrite, the peer cannot be told that
the direction ended, so the previous behavior is kept and the tunnel is torn
down rather than held open until the stream timeout.

This is the same class that was fixed upstream in net/http/httputil
(https://go.dev/issue/35892); Caddy has its own upgraded-stream copier, so the
standard library fix does not apply here.

Fixes #8026

* reverseproxy: follow net/http/httputil shape for the half-close

Replace the copyResult struct and the closeWriter/closeWrite helpers with
upstream's plain error channel and errCopyDone sentinel, propagating
CloseWrite inside the copier, as requested in review.

Behaviour is unchanged. The wait is repeated twice inside the existing
select because handleUpgradeResponse also selects on the stream timeout
and sizes the channel at 2 so both goroutines can exit when it fires
(#7418), which net/http/httputil has no equivalent of.

* reverseproxy: explain the repeated wait next to it

Move the reasoning for why the wait is repeated inside the select, rather
than written as upstream's two-line form, into a comment beside the loop
so a future reader does not have to find the review discussion.

* reverseproxy: add an upgraded-stream half-close integration test

Drive a real upgraded stream through the handler and check that closing one
direction is propagated to the other side instead of tearing the tunnel down.

Mirrors net/http/httputil's TestReverseProxyWebSocketHalfTCP, which covers the
same class upstream (https://go.dev/issue/35892): a backend that hijacks and
hands back a *net.TCPConn, the handler in front of it, and a client that dials
it directly, then the four close-read / close-write combinations.

Against the unfixed handler the two half-close cases fail - the surviving
direction never delivers its pending bytes - while the two close-read cases
pass, so the test isolates exactly the reported behaviour.

* reverseproxy: give the half-close test reads a deadline

Without one, a regression blocks in ReadFull until the package timeout
instead of failing the test, which is the flakiness an end-to-end test of
this kind is usually accused of. Ten seconds is far above any real latency
here and turns a hang into a clear failure.

* reverseproxy: hijack the test backend via http.NewResponseController

Matches how the rest of the tree reaches Hijack (responsewriter.go,
caddyauth.go, streaming.go itself) and keeps working if the writer is
wrapped, where a direct http.Hijacker assertion would not.
2026-09-16 10:10:24 +00:00
dependabot[bot] 15ee32d50c build(deps): bump google.golang.org/grpc from 1.82.1 to 1.83.1 (#7984)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.1 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.82.1...v1.83.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-16 19:38:32 +10:00
Zen Dodd ef1877210e Merge commit from fork
* caddyhttp: canonicalise paths after prefix stripping

* caddyhttp: cover suffix stripping in path canonicalisation
2026-09-14 12:50:44 -06:00
Abdellatif Anaflous 56e3a88efe caddyhttp: only replace known placeholders in respond headers (#8014)
The respond directive expanded header field names and values with
repl.ReplaceAll, which blanks any {...} the replacer does not recognize

Header values are config data, often JSON or carrying literal braces, so a
value like {key:value} was sent empty and a-{b}-c came out a--c

Use ReplaceKnown, matching the header handler fix in #4880 (same class as
#4418) and the respond body expansion a few lines below, so unknown braces
survive and real placeholders still expand
2026-09-13 12:08:21 +10:00
wangjingshuiku a75817f550 fix: require module path boundaries when matching packages (#7957)
Signed-off-by: wangjingshuiku <wangjingshuiku@163.com>
2026-09-13 12:08:09 +10:00
Faiyaz Rahman 425a3381fd acmeserver: say when the CA database is locked by another process (#8007)
Opening the bbolt database only reported timeout, which sounds like a
broken config. Catch bolt.ErrTimeout with errors.Is and say another
process holds the lock, and that caddy reload is the right command when
Caddy is already running. The error stays fatal.
2026-09-11 14:49:31 +10:00
Islam Elsayed 56ae39bdcc reverseproxy: replace only known placeholders in health check body (#8003)
The active health check body is user-supplied and commonly JSON, but it
was expanded with ReplaceAll, which blanks any `{...}` the replacer does
not recognize. Only globals are available here, so a JSON
health_request_body was sent as an empty body.

This is not limited to bodies without placeholders. A body that does use
one is mangled too: `{"token":"{env.SOME_VAR}"}` is sent as `"}`, since
the opening `{"token":"` is consumed as an unrecognized placeholder
before the real one is reached.

Use ReplaceKnown, matching the header values a few lines below, so
unrecognized braces are left intact and real placeholders still expand.

Only the body is changed. The Host header on the same path keeps
ReplaceAll deliberately: Go's HTTP server blanks a Host containing
braces, so leaving `{...}` intact there would send a Host the upstream
discards, where blanking it lets Go fall back to the URL host.
2026-09-09 08:48:08 -06:00
Y.Horie 769ed7e4a3 events: skip dispatch setup when nothing is subscribed (#7997)
App.Emit does a fair amount of work before it can discover that no handler
is bound: it derives three loggers, one of which formats the event's UUID
even when debug logging is off, and registers a replacer callback. Only
then does it reach "shortcut if event not bound at all".

Some events are emitted on every TLS handshake -- CertMagic emits
tls_get_certificate as the first statement of GetCertificateWithContext --
so on a server with no events configuration that work runs per handshake
and is discarded every time.

Return early when neither the event's name nor the catch-all is bound and
debug logging is off, which are exactly the conditions under which nothing
can observe the event. caddy.NewEvent still runs, so the returned Event is
unchanged for callers.

Benchmarks are included; measurements are in the pull request.
2026-09-09 13:33:54 +10:00
Jens-Uwe Mager c8f0667aee caddyhttp: demote Alt-Svc ErrNoAltSvcPort to debug level (#8000) 2026-09-08 13:26:18 +10:00
Alexandre Daubois e05f57dc7a listen: don't wedge a reloaded listener sharing a socket on Windows (#7999) 2026-09-07 12:24:09 -04:00
Faiyaz Rahman 9dd286c5e4 httpcaddyfile: give each adaptation its own directive order (#7995) 2026-09-06 09:20:44 -04:00
Jens-Uwe MagerandZen Dodd 8626fa3703 caddytls: synchronize storage cleaner with TLS.Stop via context and WaitGroup (#7954)
* caddytls: synchronize storage cleaner with TLS.Stop via context and WaitGroup

* Unify the storage cleanup synchronization to both tls and ech.

* Cannot embed the sync.WaitGroup directly as the TLS struct is copied.

* caddytls: propagate cancellable context to ECH rotation and add sync tests

Pass cancellable context to ECH key rotation so in-flight storage locks
and operations unblock when TLS.Stop is invoked. Add comprehensive tests
verifying TLS.Stop cleanly unblocks and waits for storage cleaner and
ECH workers.

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-05 07:37:57 -06:00
Abdellatif Anaflous f7d58438b0 admin: fix host allow-list comparison to be case-insensitive (#7993)
The admin host check compared r.Host against the allowed URL's Host
with byte-for-byte equality. url.Parse does not normalize host case,
so an allowed 'http://Example.com:2019' rejected a client Host of
'example.com:2019' with 'host not allowed', and the same happened for
an uppercase variant of a lowercase entry like localhost.

The Origin check got this treatment in 7973 already, the DNS rebinding
Host check right next to it did not. Both read the same allowedOrigins
list, so this applies the same strings.EqualFold treatment there.

Regression test covers both fold directions, the default localhost
entry and the negative case that must keep failing
2026-09-05 12:39:37 +00:00
Faiyaz RahmanandEugin Francis 8e90c9a0e1 caddyhttp: surface write timeout errors in access log (#7945)
Co-authored-by: Eugin Francis <13235453+euginfrancis@users.noreply.github.com>
Signed-off-by: Faiyaz Rahman <faiyazrahman03@gmail.com>
2026-09-04 16:56:07 -04:00
Sash 97a08d8251 fileserver: reject non-integer browse file_limit (#7988) 2026-09-04 16:37:02 +10:00
Faiyaz Rahman 19be5d8c58 caddyhttp: remove unused QUICConfig from the HTTP/3 server (#7980)
Signed-off-by: Faiyaz Rahman <faiyazrahman03@gmail.com>
2026-09-02 18:02:52 -04:00
Mohammed Al Sahaf 2cb7ebca45 caddyfile: fix importGraph self-loop and stale-edge bugs (#7971)
- willCycle now reports a cycle when from == to, so addEdge rejects
  self-loops (a self-importing file was previously accepted).
- removeNode now drops the removed node's outgoing edges and any
  incoming edges pointing at it, keeping the adjacency map consistent.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-09-03 07:48:22 +10:00
Mohammed Al Sahaf af29f9ea91 admin: fix origin allow-list host comparison to be case-insensitive (#7973)
The admin origin allow-list compared origin.Host against the allowed
URL's Host with byte-for-byte equality. url.Parse does not normalize
host case, so an allowed 'http://Example.com:8080' rejected a client
origin of 'http://example.com:8080' even though RFC 3986 §3.2.2 says
host names are case-insensitive. Use strings.EqualFold.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-09-03 07:46:43 +10:00
Mohammed Al Sahaf 7cc98d4d3f cmd: make splitModule strictly conform to Go module spec (#7974)
Per https://go.dev/ref/mod#go-mod-file-ident, '@' is not a valid module
path character. The old implementation used strings.LastIndex to allow
inputs like github.com/@user/module@v1.0.0 to parse, but such inputs
are not valid module paths per the spec. Reject them explicitly.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-09-03 07:43:34 +10:00
James Ko 9968758201 usagepool: avoid lock inversion after constructor failure (#7968) 2026-09-02 21:33:25 +00:00
Mohammed Al Sahaf 62a72977e5 core: fix ParseNetworkAddress port-range span off-by-one (#7975)
A port range is inclusive on both ends, so a range from start to end
spans (end - start + 1) ports. The old (end - start) > maxPortSpan
check let 0-65535 through even though that spans 65536 ports, one
above the 65535 cap. Include the trailing port in the count.

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-08-31 14:20:04 -06:00
Kévin Dunglas d0e93c23a1 cmd: upgrade automemlimit to v1.0.0 (#7978)
v1.0.0 renamed SetGoMemLimitWithOpts to Set and dropped the other Set*
helpers. The options this call passes are unchanged, as are the 0.9
default ratio and the AUTOMEMLIMIT handling, so behavior is the same.

Only the already-deprecated AUTOMEMLIMIT_DEBUG and AUTOMEMLIMIT_EXPERIMENT
environment variables are gone, and neither is used here.
2026-08-31 19:18:31 +03:00
Zen Dodd 8257349b58 chore: preserve canonical header keys (#7964) 2026-08-30 16:26:18 +00:00
Zen Dodd 502691f518 admin: stabilise log redaction test (#7942) 2026-08-28 13:36:59 -06:00
TowyTowyandClaude Fable 5 7bf1b9057b rewrite: fix strip_path_suffix ignoring percent-encoding (#7877)
StripPathSuffix is documented to behave like StripPathPrefix: the suffix
is matched in normalized (unescaped) space except where the pattern uses
an escape sequence. But suffix stripping was implemented as

    reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix)))

Reversing the strings moves the '%' to the *end* of each "%xx" escape,
which defeats trimPathPrefix's escape detection (it expects '%' to
precede the two hex digits). As a result the escape-aware, normalized
comparison never happened for suffixes: a decoded pattern failed to
match a percent-encoded path.

Concretely, StripPathPrefix "/a/b/c" strips "/a%2Fb/c/d" to "/d", but the
mirror StripPathSuffix "/b/c" left "/a/b%2Fc" untouched instead of
producing "/a"; likewise StripPathSuffix "bc" did not strip "/a%62c".
This has been the behavior since #4948, which introduced both the
escape-aware trimPathPrefix and the reverse-based suffix trimming.

Replace the reverse trick with a dedicated trimPathSuffix that iterates
from the ends of both strings and applies the same escape-aware,
case-insensitive comparison as trimPathPrefix. An escape in the pattern
itself is still compared literally, so "%2fsuffix" continues to require
the path to contain that exact escape.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 19:44:05 +10:00
David Carliez 3244ef4105 fileserver: reject short names in every path component (#7952)
* fileserver: reject short names in every path component
Signed-off-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>

* fileserver: validate short-name characters
Signed-off-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>

* fileserver: fail closed on extended short names
Signed-off-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>
2026-08-28 09:09:23 +00:00
d6637934e8 admin: normalize request path in remote admin access-control check (defense-in-depth) (#7910)
* admin: normalize request path in remote admin access-control check

Co-authored-by: atlarix-agent <agent@atlarix.dev>

* admin: fix empty allowedPath regression and dead code in path normalization

path.Clean("") returns ".", so cleaning allowedPath unconditionally
silently broke the allow-all behavior when Paths: [""] is configured.
Short-circuit the empty case before cleaning to preserve that behavior.

Also remove the dead strings.HasSuffix(allowedPath, "/") branch —
after path.Clean the path never has a trailing slash, so the unified
reqPath == allowedPath || HasPrefix(reqPath, allowedPath+"/") form
covers exact match, subpath boundary, and trailing-slash requests.

Co-authored-by: atlarix-agent <agent@atlarix.dev>
Co-authored-by: iabdullah215 <muhammadabdullah8040@gmail.com>

* admin: validate non-canonical configured paths at provisioning

path.Clean(allowedPath) silently broadens misconfigured values like
// or /.. into /, which grants unintended access to all endpoints.
Reject non-canonical paths during provisioning in
replaceRemoteAdminServer so misconfigurations fail fast with a
clear error. The path.Clean in adminPathAllowed remains as
defense-in-depth but is now a safe no-op on validated inputs.

Co-authored-by: atlarix-agent <agent@atlarix.dev>

* admin: validate non-canonical configured paths at provisioning

path.Clean(allowedPath) silently broadens misconfigured values like
// or /.. into /, which grants unintended access to all endpoints.
Reject non-canonical paths during provisioning in
replaceRemoteAdminServer so misconfigurations fail fast with a
clear error. The path.Clean in adminPathAllowed remains as
defense-in-depth but is now a safe no-op on validated inputs.

Co-authored-by: atlarix-agent <agent@atlarix.dev>

* admin: fix TrimRight → TrimSuffix in provisioning path validation

strings.TrimRight strips all trailing slashes, so a configured path
like /foo// passed validation (both slashes trimmed to /foo matching
path.Clean output) but was silently broadened to /foo at runtime.

Use strings.TrimSuffix instead, which removes exactly one trailing
slash — the only form the exemption was meant to allow (users write
/pki/ca/prod/ meaning the /pki/ca/prod scope).

Also update the // test case: with TrimSuffix, // is just / + one
trailing slash, which is valid under the exemption. Add a new test
for /foo// (double trailing slashes → wantErr: true).

Co-authored-by: atlarix-agent <agent@atlarix.dev>

* admin: reject non-canonical root permission path

* admin: preserve trailing-slash permission semantics

---------

Co-authored-by: atlarix-agent <agent@atlarix.dev>
Co-authored-by: iabdullah215 <muhammadabdullah8040@gmail.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-08-25 12:12:08 -06:00
Francis LavoieandClaude Opus 5 39af0aec31 rewrite: fix URI splitting when a query or fragment arrives via a placeholder (#7947)
* rewrite: don't drop a trailing '=' from the query string

buildQueryString scanned for '=' unconditionally when looking for the
end of a component, but '=' only delimits a key from its value once;
any further '=' bytes are literal data. When the query ended with '=',
that byte was consumed as a delimiter with nothing following it to
re-emit, so it was silently lost:

	?x=1&sig=YWJjZA==  =>  ?x=1&sig=YWJjZA=

This corrupts base64 padding in the last query parameter, which is the
shape of an S3 presigned URL (X-Amz-Signature), turning a valid
signature into a 403. Only the final '=' of the query was affected;
?sig=YWJjZA==&x=1 came through intact.

Disable the '=' search while consuming a value so that only '&' ends it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* rewrite: honor a query string injected by a replacement value

Which URI components get written back was decided from the literal
config string, before placeholders were expanded, but an injected query
is only detected after expansion. The two were never reconciled: for
`rewrite * {rp.header.X-Accel-Redirect}` there is no literal '?', so
qsStart stayed -1, and the correctly-built query string was computed and
then discarded by the `if qsStart >= 0` guard.

Only half the split was applied. The path was still truncated at the
injected '?', so the query was not preserved either -- it was dropped,
and any query already on the request survived in its place:

	GET /orig?keep=me, X-Accel-Redirect: /hello?some=param
	=> /hello?keep=me

Track whether a query was actually injected and include that in the
write-back condition. Appending a literal '?' to the rewrite value was
the known workaround precisely because it set qsStart; that keeps
working and is now unnecessary.

The flag is only set where the injected query is adopted, so an
explicitly configured query still wins, and a value with no '?' still
leaves the query untouched -- which is what the implicit rewrites of
try_files and php_fastcgi rely on. Those stay safe regardless, since
escapePathPlaceholders already escapes the two placeholders they use, so
a client-supplied %3F cannot split the URI.

Fixes #5208

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* rewrite: drop a fragment injected by a replacement value

The scan that separates path, query and fragment runs on the literal
config string, so a '#' arriving later via a replacement value was never
treated as a delimiter. It leaked into whichever component it landed in:

	X-Accel-Redirect: /hello?p=x#frag  =>  RawQuery = "p=x#frag"

Everything after '#' is fragment (RFC 3986 section 4.2) and a fragment is
never sent to the server, so drop it before the path is split, mirroring
how the scan already handles a literal '#'. An escaped %23 is unaffected,
so a real '#' in a path or query is still expressible, and a configured
fragment still wins over an injected one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:06:09 -06:00
0jaspahwa 8b62e3c60e caddyfile: clarify ArgErr documentation (#7960)
Signed-off-by: 0jaspahwa <ojaspahwa20@gmail.com>
2026-08-25 11:54:50 +00:00
Gautam R 51db7f0313 pki: honor skip_install_trust for explicit tls internal issuers (#7894) 2026-08-25 14:19:33 +10:00
Faiyaz Rahman 4974956b9c chore: fix lint errors from newer golangci-lint (#7958)
Signed-off-by: Faiyaz Rahman <faiyazrahman03@gmail.com>
2026-08-25 13:12:10 +10:00
Kévin Dunglas 0cf03d32f7 caddyhttp: mitigate slowloris via idle read/write deadlines (#7913)
* caddyhttp: mitigate slowloris via idle read/write deadlines

ReadTimeout and WriteTimeout previously applied as a single hard
deadline over the whole body/response through http.Server, so any
non-zero value also killed large transfers from legitimately slow
clients. Reset the deadline on every successful read/write instead
(via http.ResponseController), and give both a sane 1m default now
that doing so no longer penalizes slow-but-progressing clients.

* caddyhttp: split idle read/write timeouts from the existing hard ones

Reworking ReadTimeout/WriteTimeout's own semantics was an unwanted
behavior change for existing configs relying on the hard deadline.
Leave them untouched and add ReadIdleTimeout/WriteIdleTimeout instead,
reset on every successful read/write; both default to 1m since,
being new, no existing config could have depended on a different
value. Combining an idle timeout with its hard counterpart now gives
the same base+ceiling shape as Apache's mod_reqtimeout, for free.

* caddyhttp: cap idle-reset deadlines at the hard timeout ceiling

Deadlines are a single absolute value on the connection, not a min of
several: ReadTimeout/WriteTimeout's own hard deadline, set once by
net/http before the handler runs, was silently getting overwritten by
the first idle-reset Read/Write, voiding it entirely. Clamp the
idle-reset deadline to the hard one when both are set, so combining
them actually behaves like the advertised base+ceiling.

* caddyhttp: add ReadMinRate/WriteMinRate, Apache MinRate equivalent

Pure idle-reset alone doesn't bound a trickle that sends just enough
to never go idle. ReadMinRate/WriteMinRate (bytes/second) grow the
allowed deadline from a fixed start based on bytes transferred so far
instead of resetting to a flat window on every call, so a transfer
that doesn't sustain the configured rate falls behind real time and
gets cut, matching Apache mod_reqtimeout's MinRate. Zero (default)
keeps the existing flat idle-reset behavior unchanged.

* caddyhttp: use named return and consistent blank lines in idleDeadline

Matches the named-return style already used by ResponseWriterWrapper.ReadFrom.

* caddyhttp: chunk idleTimeoutWriter's Write/ReadFrom, cap at 64 KiB

SetWriteDeadline bounds the whole call it precedes, not just a stall
within it. net.Conn.Write loops internally until a buffer is fully
sent (unlike Read, which returns after one syscall), and
ResponseWriter.ReadFrom hands the entire remaining source to the
connection in one call. A single large Write, or any body copied via
io.Copy triggering the ReadFrom fast path (http.ServeContent, static
file serving), had its whole transfer bounded by one deadline,
silently truncating a slow-but-healthy transfer exactly like a hard
WriteTimeout would - the same bug found and fixed the same way in
FrankenPHP's go_ub_write (php/frankenphp#2574).

Cap each underlying call at 64 KiB and reset the deadline between
chunks instead. net/sendfile.go special-cases *io.LimitedReader, so
chunking ReadFrom still uses the sendfile fast path per chunk.

* caddyhttp: export idle-timeout types, add configurable MaxWriteChunk

Export IdleTimeoutReader/IdleTimeoutWriter/IdleDeadline so other
packages (request_body next) can reuse the same idle-reset mechanism
instead of reimplementing it, and turn the hardcoded 64 KiB write
chunk size into a configurable MaxWriteChunk field defaulting to the
same value - nginx's sendfile_max_chunk exists for the identical
reason and is admin-tunable rather than fixed.

* requestbody: idle-reset ReadTimeout/WriteTimeout, add MinRate/MaxWriteChunk

ReadTimeout/WriteTimeout set a single deadline once, so any transfer
running longer than the timeout got cut regardless of whether it was
actually stalled - the same bug the server-wide timeouts had before
switching to idle-reset. Reuse caddyhttp.IdleTimeoutReader/Writer here
too, giving per-route granularity nginx/Apache have via location/
directory scoping and Caddy's server-wide timeouts don't: a route
matching this handler can now set its own idle window independently
from the rest of the server block.

* caddyhttp: fold read/write min_rate into the idle-timeout directive

Two directives per rate (read_body_idle + read_body_min_rate) for a
value that's meaningless without the other. Fold min_rate into the
idle-timeout directive as an optional second argument instead.

* caddyhttp: split write pacing out of request_body into new timeouts handler

request_body is a request-body concern (max_size, set); ReadTimeout/
WriteTimeout/MinRate/MaxWriteChunk pace both directions, and write
pacing has nothing to do with the request body. Move all of it to a
dedicated http.handlers.timeouts module instead, mirroring the
server-wide timeouts option one level down.
2026-08-21 21:17:26 -06:00
bzyy1024 45ba3278b5 fastcgi: fix HTTPoxy vulnerability (#7934)
* Implement HTTPoxy mitigation in FastCGI
Added HTTPoxy mitigation to prevent trusting client-supplied Proxy header for HTTP_PROXY environment variable.

* Implement test for HTTPoxy vulnerability protection
Add test to ensure HTTPoxy vulnerability is mitigated by dropping client-supplied Proxy headers.

* gofmt: format code
* revert unrelated gofmt change to replacer_test.go
2026-08-19 13:07:24 +10:00
yangshenghui 0bbda5c728 fix: close resources on error paths (#7940) 2026-08-16 21:02:39 +10:00
Kévin Dunglas 789f60e337 caddyhttp: fix url_pattern authorization bypass via encoded-slash traversal (#7941)
* caddyhttp: match url_pattern against decoded, cleaned path

The url_pattern matcher evaluated the raw, percent-encoded request URI
while path-consuming handlers resolve the decoded, cleaned r.URL.Path.
An encoded-slash payload such as "..%2f" stayed a single opaque segment
for the WHATWG URLPattern parser, so "/public/..%2fadmin/secret" matched
"/public/*" while handlers decoded it to "/admin/secret", bypassing any
route-level access control built with url_pattern.

Match the same path model handlers resolve: decode the path, normalize
and clean it (mirroring the path matcher and #4407), then re-encode
through url.URL to a canonical escaped form before running the pattern.
The go-urlpattern library is spec-correct; the fix is in the integration.

* build(deps): bump github.com/dunglas/go-urlpattern to v1.0.0

Moves off the pseudo-version to the first tagged release.
2026-08-15 10:38:20 -06:00