From b723bd94d411956ad2925166d6327e590ac5e721 Mon Sep 17 00:00:00 2001 From: Ville Vesilehto Date: Mon, 5 Jan 2026 19:48:48 +0200 Subject: [PATCH] fix(plugins): add regex length limit (#7802) --- plugin/errors/README.md | 2 +- plugin/errors/setup.go | 7 +++++++ plugin/errors/setup_test.go | 17 +++++++++++++++++ plugin/rewrite/README.md | 3 ++- plugin/rewrite/cname_target.go | 3 +++ plugin/rewrite/cname_target_test.go | 12 ++++++++++++ plugin/rewrite/name.go | 7 +++++++ plugin/rewrite/name_test.go | 11 +++++++++++ plugin/rewrite/rcode.go | 3 +++ plugin/rewrite/rcode_test.go | 12 ++++++++++++ plugin/rewrite/ttl.go | 3 +++ plugin/rewrite/ttl_test.go | 12 ++++++++++++ plugin/template/README.md | 3 ++- plugin/template/setup.go | 7 +++++++ plugin/template/setup_test.go | 18 ++++++++++++++++++ 15 files changed, 117 insertions(+), 3 deletions(-) diff --git a/plugin/errors/README.md b/plugin/errors/README.md index 35210ca22..ed91f602e 100644 --- a/plugin/errors/README.md +++ b/plugin/errors/README.md @@ -29,7 +29,7 @@ errors { Option `stacktrace` will log a stacktrace during panic recovery. -Option `consolidate` allows collecting several error messages matching the regular expression **REGEXP** during **DURATION**. After the **DURATION** since receiving the first such message, the consolidated message will be printed to standard output with +Option `consolidate` allows collecting several error messages matching the regular expression **REGEXP** during **DURATION**. **REGEXP** must not exceed 10000 characters. After the **DURATION** since receiving the first such message, the consolidated message will be printed to standard output with log level, which is configurable by optional option **LEVEL**. Supported options for **LEVEL** option are `warning`,`error`,`info` and `debug`. ~~~ 2 errors like '^read udp .* i/o timeout$' occurred in last 30s diff --git a/plugin/errors/setup.go b/plugin/errors/setup.go index f6e99aba0..b9aa649ba 100644 --- a/plugin/errors/setup.go +++ b/plugin/errors/setup.go @@ -9,6 +9,10 @@ import ( "github.com/coredns/coredns/plugin" ) +// maxRegexpLen is a hard limit on the length of a regex pattern to prevent +// OOM during regex compilation with malicious input. +const maxRegexpLen = 10000 + func init() { plugin.Register("errors", setup) } func setup(c *caddy.Controller) error { @@ -78,6 +82,9 @@ func parseConsolidate(c *caddy.Controller) (*pattern, error) { if err != nil { return nil, c.Err(err.Error()) } + if len(args[1]) > maxRegexpLen { + return nil, c.Errf("regex pattern too long: %d > %d", len(args[1]), maxRegexpLen) + } re, err := regexp.Compile(args[1]) if err != nil { return nil, c.Err(err.Error()) diff --git a/plugin/errors/setup_test.go b/plugin/errors/setup_test.go index c09d6cbbc..02a34c5d2 100644 --- a/plugin/errors/setup_test.go +++ b/plugin/errors/setup_test.go @@ -2,6 +2,7 @@ package errors import ( "bytes" + "fmt" golog "log" "strings" "testing" @@ -250,3 +251,19 @@ func TestShowFirstOption(t *testing.T) { }) } } + +func TestErrorsParseLargeRegex(t *testing.T) { + largeRegex := strings.Repeat("a", maxRegexpLen+1) + config := fmt.Sprintf(`errors { + consolidate 1m %s + }`, largeRegex) + + c := caddy.NewTestController("dns", config) + _, err := errorsParse(c) + if err == nil { + t.Fatal("Expected error for large regex, got nil") + } + if !strings.Contains(err.Error(), "too long") { + t.Errorf("Expected 'too long' error, got: %v", err) + } +} diff --git a/plugin/rewrite/README.md b/plugin/rewrite/README.md index f5e4c0332..5f4d393de 100644 --- a/plugin/rewrite/README.md +++ b/plugin/rewrite/README.md @@ -74,7 +74,8 @@ The match type, e.g., `exact`, `substring`, etc., triggers rewrite: * **substring**: on a partial match of the name in the question section of a request * **prefix**: when the name begins with the matching string * **suffix**: when the name ends with the matching string -* **regex**: when the name in the question section of a request matches a regular expression +* **regex**: when the name in the question section of a request matches a regular expression. + Regex patterns must not exceed 10000 characters. If the match type is omitted, the `exact` match type is assumed. If OPTIONS are given, the type must be specified. diff --git a/plugin/rewrite/cname_target.go b/plugin/rewrite/cname_target.go index 46d501801..57ee451b2 100644 --- a/plugin/rewrite/cname_target.go +++ b/plugin/rewrite/cname_target.go @@ -144,6 +144,9 @@ func newCNAMERule(nextAction string, args ...string) (Rule, error) { Upstream: upstream.New(), } if rewriteType == RegexMatch { + if len(paramFromTarget) > maxRegexpLen { + return nil, fmt.Errorf("regex pattern too long in a cname rule: %d > %d", len(paramFromTarget), maxRegexpLen) + } re, err := regexp.Compile(paramFromTarget) if err != nil { return nil, fmt.Errorf("invalid cname rewrite regex pattern: %w", err) diff --git a/plugin/rewrite/cname_target_test.go b/plugin/rewrite/cname_target_test.go index 7b0483f6d..ff25e0217 100644 --- a/plugin/rewrite/cname_target_test.go +++ b/plugin/rewrite/cname_target_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "reflect" + "strings" "testing" "github.com/coredns/coredns/plugin" @@ -263,3 +264,14 @@ func TestCNAMETargetRewrite_upstreamFailurePaths(t *testing.T) { }) } } + +func TestNewCNAMERuleLargeRegex(t *testing.T) { + largeRegex := strings.Repeat("a", maxRegexpLen+1) + _, err := newCNAMERule("stop", "regex", largeRegex, "replacement") + if err == nil { + t.Fatal("Expected error for large regex, got nil") + } + if !strings.Contains(err.Error(), "too long") { + t.Errorf("Expected 'too long' error, got: %v", err) + } +} diff --git a/plugin/rewrite/name.go b/plugin/rewrite/name.go index 65d80d0f0..ab4200352 100644 --- a/plugin/rewrite/name.go +++ b/plugin/rewrite/name.go @@ -13,6 +13,10 @@ import ( "github.com/miekg/dns" ) +// maxRegexpLen is a hard limit on the length of a regex pattern to prevent +// OOM during regex compilation with malicious input. +const maxRegexpLen = 10000 + // stringRewriter rewrites a string type stringRewriter interface { rewriteString(src string) string @@ -438,6 +442,9 @@ func getSubExprUsage(s string) int { // isValidRegexPattern returns a regular expression for pattern matching or errors, if any. func isValidRegexPattern(rewriteFrom, rewriteTo string) (*regexp.Regexp, error) { + if len(rewriteFrom) > maxRegexpLen { + return nil, fmt.Errorf("regex pattern too long: %d > %d", len(rewriteFrom), maxRegexpLen) + } rewriteFromPattern, err := regexp.Compile(rewriteFrom) if err != nil { return nil, fmt.Errorf("invalid regex matching pattern: %s", rewriteFrom) diff --git a/plugin/rewrite/name_test.go b/plugin/rewrite/name_test.go index e85c773a6..aa302d8c3 100644 --- a/plugin/rewrite/name_test.go +++ b/plugin/rewrite/name_test.go @@ -368,3 +368,14 @@ func TestNewNameRule(t *testing.T) { t.Fatalf("Test %d: FAIL, expected fail=%t, but received fail=%t: (%s) %s, rule=%v", i, tc.expectedFail, failed, tc.next, tc.args, rule) } } + +func TestNewNameRuleLargeRegex(t *testing.T) { + largeRegex := strings.Repeat("a", maxRegexpLen+1) + _, err := newNameRule("stop", "regex", largeRegex, "replacement") + if err == nil { + t.Fatal("Expected error for large regex, got nil") + } + if !strings.Contains(err.Error(), "too long") { + t.Errorf("Expected 'too long' error, got: %v", err) + } +} diff --git a/plugin/rewrite/rcode.go b/plugin/rewrite/rcode.go index cf12a8c26..3a2a8d30b 100644 --- a/plugin/rewrite/rcode.go +++ b/plugin/rewrite/rcode.go @@ -142,6 +142,9 @@ func newRCodeRule(nextAction string, args ...string) (Rule, error) { plugin.Name(args[1]).Normalize(), }, nil case RegexMatch: + if len(args[1]) > maxRegexpLen { + return nil, fmt.Errorf("regex pattern too long in a rcode rule: %d > %d", len(args[1]), maxRegexpLen) + } regexPattern, err := regexp.Compile(args[1]) if err != nil { return nil, fmt.Errorf("invalid regex pattern in a rcode rule: %s", args[1]) diff --git a/plugin/rewrite/rcode_test.go b/plugin/rewrite/rcode_test.go index 8f0c2e672..97d6459be 100644 --- a/plugin/rewrite/rcode_test.go +++ b/plugin/rewrite/rcode_test.go @@ -1,6 +1,7 @@ package rewrite import ( + "strings" "testing" "github.com/coredns/coredns/plugin/test" @@ -70,3 +71,14 @@ func TestRCodeRewrite(t *testing.T) { t.Fatalf("RCode rewrite did not apply changes, request=%#v, err=%v", request.Req, err) } } + +func TestNewRCodeRuleLargeRegex(t *testing.T) { + largeRegex := strings.Repeat("a", maxRegexpLen+1) + _, err := newRCodeRule("stop", "regex", largeRegex, "SERVFAIL", "NXDOMAIN") + if err == nil { + t.Fatal("Expected error for large regex, got nil") + } + if !strings.Contains(err.Error(), "too long") { + t.Errorf("Expected 'too long' error, got: %v", err) + } +} diff --git a/plugin/rewrite/ttl.go b/plugin/rewrite/ttl.go index a4722a966..f33a7f0dc 100644 --- a/plugin/rewrite/ttl.go +++ b/plugin/rewrite/ttl.go @@ -140,6 +140,9 @@ func newTTLRule(nextAction string, args ...string) (Rule, error) { plugin.Name(args[1]).Normalize(), }, nil case RegexMatch: + if len(args[1]) > maxRegexpLen { + return nil, fmt.Errorf("regex pattern too long in a ttl rule: %d > %d", len(args[1]), maxRegexpLen) + } regexPattern, err := regexp.Compile(args[1]) if err != nil { return nil, fmt.Errorf("invalid regex pattern in a ttl rule: %s", args[1]) diff --git a/plugin/rewrite/ttl_test.go b/plugin/rewrite/ttl_test.go index 46ecadb0d..7e3389d4c 100644 --- a/plugin/rewrite/ttl_test.go +++ b/plugin/rewrite/ttl_test.go @@ -3,6 +3,7 @@ package rewrite import ( "context" "reflect" + "strings" "testing" "github.com/coredns/coredns/plugin" @@ -156,3 +157,14 @@ func doTTLTests(t *testing.T, rules []Rule) { } } } + +func TestNewTTLRuleLargeRegex(t *testing.T) { + largeRegex := strings.Repeat("a", maxRegexpLen+1) + _, err := newTTLRule("stop", "regex", largeRegex, "300") + if err == nil { + t.Fatal("Expected error for large regex, got nil") + } + if !strings.Contains(err.Error(), "too long") { + t.Errorf("Expected 'too long' error, got: %v", err) + } +} diff --git a/plugin/template/README.md b/plugin/template/README.md index 1bca90662..6ac5b9548 100644 --- a/plugin/template/README.md +++ b/plugin/template/README.md @@ -26,7 +26,8 @@ template CLASS TYPE [ZONE...] { * **TYPE** the query type (A, PTR, ... can be ANY to match all types). * **ZONE** the zone scope(s) for this template. Defaults to the server zones. * `match` **REGEX** [Go regexp](https://golang.org/pkg/regexp/) that are matched against the incoming question name. - Specifying no regex matches everything (default: `.*`). First matching regex wins. + Specifying no regex matches everything (default: `.*`). First matching regex wins. Regex patterns + must not exceed 10000 characters. * `answer|additional|authority` **RR** A [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) style resource record fragment built by a [Go template](https://golang.org/pkg/text/template/) that contains the reply. Specifying no answer will result in a response with an empty answer section. diff --git a/plugin/template/setup.go b/plugin/template/setup.go index cb2c706f1..8dda86def 100644 --- a/plugin/template/setup.go +++ b/plugin/template/setup.go @@ -13,6 +13,10 @@ import ( "github.com/miekg/dns" ) +// maxRegexpLen is a hard limit on the length of a regex pattern to prevent +// OOM during regex compilation with malicious input. +const maxRegexpLen = 10000 + func init() { plugin.Register("template", setupTemplate) } func setupTemplate(c *caddy.Controller) error { @@ -67,6 +71,9 @@ func templateParse(c *caddy.Controller) (handler Handler, err error) { return handler, c.ArgErr() } for _, regex := range args { + if len(regex) > maxRegexpLen { + return handler, c.Errf("regex pattern too long: %d > %d", len(regex), maxRegexpLen) + } r, err := regexp.Compile(regex) if err != nil { return handler, c.Errf("could not parse regex: %s, %v", regex, err) diff --git a/plugin/template/setup_test.go b/plugin/template/setup_test.go index 345525da6..54fcd0a8d 100644 --- a/plugin/template/setup_test.go +++ b/plugin/template/setup_test.go @@ -1,6 +1,8 @@ package template import ( + "fmt" + "strings" "testing" "github.com/coredns/caddy" @@ -198,3 +200,19 @@ func TestSetupParse(t *testing.T) { } } } + +func TestSetupParseLargeRegex(t *testing.T) { + largeRegex := strings.Repeat("a", maxRegexpLen+1) + config := fmt.Sprintf(`template ANY A example.com { + match %s + }`, largeRegex) + + c := caddy.NewTestController("dns", config) + _, err := templateParse(c) + if err == nil { + t.Fatal("Expected error for large regex, got nil") + } + if !strings.Contains(err.Error(), "too long") { + t.Errorf("Expected 'too long' error, got: %v", err) + } +}