caddytls: preserve regexp quantifiers during placeholder expansion

Expand only known placeholders in TLS regexp patterns, matching HTTP regexp behavior. Test numeric quantifiers and selection of the client-certificate-verifying fallback policy.

Addresses scan report b6c9c0a1-79a7-4254-b057-b1d8fc96b412.

Code written by GitHub Copilot (GPT-6.1 Sol, Claude Opus 5.5); reviewed by mholt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Matthew HoltandCopilot committed 2026-11-11 11:06:41 -07:00
1 parent b0731f7d2a
commit 9461da6583
2 files changed
+61 -2

No files matched your search

+3 -2
View File
@@ -175,10 +175,11 @@ type MatchRegexp struct {
compiled *regexp.Regexp
}
// Provision compiles the regular expression which may include placeholders.
// Provision compiles the regular expression which may include known placeholders.
// Unknown placeholders are preserved so regexp quantifiers remain intact.
func (mre *MatchRegexp) Provision(caddy.Context) error {
repl := caddy.NewReplacer()
re, err := regexp.Compile(repl.ReplaceAll(mre.Pattern, ""))
re, err := regexp.Compile(repl.ReplaceKnown(mre.Pattern, ""))
if err != nil {
return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
}
@@ -0,0 +1,58 @@
package caddytls
import (
"context"
"crypto/tls"
"crypto/x509"
"testing"
"github.com/caddyserver/caddy/v2"
)
func TestServerNameRegexpQuantifiers(t *testing.T) {
t.Setenv("CADDY_TEST_SNI_SUFFIX", "example")
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
for _, tc := range []struct {
pattern, input string
match bool
}{
{`^a{2}\.example$`, "aa.example", true},
{`^a{2}\.example$`, "a.example", false},
{`^a{2,}\.example$`, "aaa.example", true},
{`^a{2,}\.example$`, "a.example", false},
{`^a{2,3}\.example$`, "aaa.example", true},
{`^a{2,3}\.example$`, "a.example", false},
{`^a{2,3}\.example$`, "aaaa.example", false},
{`^a{2}\.{env.CADDY_TEST_SNI_SUFFIX}$`, "aa.example", true},
{`^a{unknown}\.example$`, "a.example", false},
} {
t.Run(tc.pattern+"/"+tc.input, func(t *testing.T) {
matcher := &MatchServerNameRE{MatchRegexp: MatchRegexp{Pattern: tc.pattern}}
if err := matcher.Provision(ctx); err != nil {
t.Fatal(err)
}
public := &tls.Config{MinVersion: tls.VersionTLS12}
protected := &tls.Config{
MinVersion: tls.VersionTLS12,
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: x509.NewCertPool(),
}
policies := ConnectionPolicies{
{matchers: []ConnectionMatcher{matcher}, TLSConfig: public},
{TLSConfig: protected},
}
selected, err := policies.TLSConfig(ctx).GetConfigForClient(&tls.ClientHelloInfo{ServerName: tc.input})
if err != nil {
t.Fatal(err)
}
want := protected
if tc.match {
want = public
}
if selected != want {
t.Fatalf("selected client authentication %v, want %v", selected.ClientAuth, want.ClientAuth)
}
})
}
}