Skip to content

Commit 6a1d3b6

Browse files
authored
Add regex safety checks and tests for pathological patterns (#2083)
1 parent b13d567 commit 6a1d3b6

File tree

3 files changed

+208
-3
lines changed

3 files changed

+208
-3
lines changed

.changeset/polite-cobras-grin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'grafana-zabbix': patch
3+
---
4+
5+
Fix CVE-2025-10630 by implementing regex scanner for exploits and adding timeout to compilation

pkg/zabbix/utils.go

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"regexp"
66
"strings"
7+
"time"
78

89
"github.com/dlclark/regexp2"
910
"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -65,6 +66,78 @@ func splitKeyParams(paramStr string) []string {
6566
return params
6667
}
6768

69+
// isPathologicalRegex detects potentially dangerous regex patterns that could cause ReDoS
70+
func isPathologicalRegex(pattern string) bool {
71+
// Check for consecutive quantifiers
72+
consecutiveQuantifiers := []string{`\*\*`, `\+\+`, `\*\+`, `\+\*`}
73+
for _, q := range consecutiveQuantifiers {
74+
if matched, _ := regexp.MatchString(q, pattern); matched {
75+
return true
76+
}
77+
}
78+
79+
// Check for nested quantifiers
80+
nestedQuantifiers := []string{
81+
`\([^)]*\+[^)]*\)\+`, // (a+)+
82+
`\([^)]*\*[^)]*\)\*`, // (a*)*
83+
`\([^)]*\+[^)]*\)\*`, // (a+)*
84+
`\([^)]*\*[^)]*\)\+`, // (a*)+
85+
}
86+
for _, nested := range nestedQuantifiers {
87+
if matched, _ := regexp.MatchString(nested, pattern); matched {
88+
return true
89+
}
90+
}
91+
92+
// Check for specific catastrophic patterns
93+
catastrophicPatterns := []string{
94+
`\(\.\*\)\*`, // (.*)*
95+
`\(\.\+\)\+`, // (.+)+
96+
`\(\.\*\)\+`, // (.*)+
97+
`\(\.\+\)\*`, // (.+)*
98+
}
99+
for _, catastrophic := range catastrophicPatterns {
100+
if matched, _ := regexp.MatchString(catastrophic, pattern); matched {
101+
return true
102+
}
103+
}
104+
105+
// Check for obvious overlapping alternation (manual check for exact duplicates)
106+
if strings.Contains(pattern, "(a|a)") ||
107+
strings.Contains(pattern, "(1|1)") ||
108+
strings.Contains(pattern, "(.*|.*)") {
109+
return true
110+
}
111+
112+
return false
113+
}
114+
115+
// safeRegexpCompile compiles a regex with timeout protection
116+
func safeRegexpCompile(pattern string) (*regexp2.Regexp, error) {
117+
// Channel to receive compilation result
118+
resultCh := make(chan struct {
119+
regex *regexp2.Regexp
120+
err error
121+
}, 1)
122+
123+
// Compile regex in goroutine with timeout
124+
go func() {
125+
regex, err := regexp2.Compile(pattern, regexp2.RE2)
126+
resultCh <- struct {
127+
regex *regexp2.Regexp
128+
err error
129+
}{regex, err}
130+
}()
131+
132+
// Wait for compilation or timeout
133+
select {
134+
case result := <-resultCh:
135+
return result.regex, result.err
136+
case <-time.After(5 * time.Second):
137+
return nil, fmt.Errorf("regex compilation timeout (5s) - pattern may be too complex")
138+
}
139+
}
140+
68141
func parseFilter(filter string) (*regexp2.Regexp, error) {
69142
vaildREModifiers := "imncsxrde"
70143
regex := regexp.MustCompile(`^/(.+)/([imncsxrde]*)$`)
@@ -75,17 +148,35 @@ func parseFilter(filter string) (*regexp2.Regexp, error) {
75148
return nil, nil
76149
}
77150

151+
regexPattern := matches[1]
152+
153+
// Security: Check for pathological regex patterns
154+
if isPathologicalRegex(regexPattern) {
155+
return nil, backend.DownstreamErrorf("error parsing regexp: potentially dangerous regex pattern detected")
156+
}
157+
158+
// Security: Limit regex pattern length
159+
if len(regexPattern) > 1000 {
160+
return nil, backend.DownstreamErrorf("error parsing regexp: pattern too long (max 1000 characters)")
161+
}
162+
78163
pattern := ""
79164
if matches[2] != "" {
80165
if flagRE.MatchString(matches[2]) {
81166
pattern += "(?" + matches[2] + ")"
82167
} else {
83-
return nil, backend.DownstreamError(fmt.Errorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers))
168+
return nil, backend.DownstreamErrorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers)
84169
}
85170
}
86-
pattern += matches[1]
171+
pattern += regexPattern
172+
173+
// Security: Test compilation with timeout
174+
compiled, err := safeRegexpCompile(pattern)
175+
if err != nil {
176+
return nil, backend.DownstreamErrorf("error parsing regexp: %v", err)
177+
}
87178

88-
return regexp2.Compile(pattern, regexp2.RE2)
179+
return compiled, nil
89180
}
90181

91182
func isRegex(filter string) bool {

pkg/zabbix/utils_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package zabbix
22

33
import (
44
"reflect"
5+
"strings"
56
"testing"
67

78
"github.com/dlclark/regexp2"
@@ -95,6 +96,48 @@ func TestParseFilter(t *testing.T) {
9596
expectNoError: false,
9697
expectedError: "",
9798
},
99+
{
100+
name: "Pathological regex - nested quantifiers (a+)+",
101+
filter: "/^(a+)+$/",
102+
want: nil,
103+
expectNoError: false,
104+
expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
105+
},
106+
{
107+
name: "Pathological regex - (.*)* pattern",
108+
filter: "/(.*)*/",
109+
want: nil,
110+
expectNoError: false,
111+
expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
112+
},
113+
{
114+
name: "Pathological regex - overlapping alternation",
115+
filter: "/(a|a)*/",
116+
want: nil,
117+
expectNoError: false,
118+
expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
119+
},
120+
{
121+
name: "Pathological regex - consecutive quantifiers",
122+
filter: "/a**/",
123+
want: nil,
124+
expectNoError: false,
125+
expectedError: "error parsing regexp: potentially dangerous regex pattern detected",
126+
},
127+
{
128+
name: "Pattern too long",
129+
filter: "/" + strings.Repeat("a", 1001) + "/",
130+
want: nil,
131+
expectNoError: false,
132+
expectedError: "error parsing regexp: pattern too long (max 1000 characters)",
133+
},
134+
{
135+
name: "Safe complex regex",
136+
filter: "/^[a-zA-Z0-9_-]+\\.[a-zA-Z]{2,}$/",
137+
want: regexp2.MustCompile("^[a-zA-Z0-9_-]+\\.[a-zA-Z]{2,}$", regexp2.RE2),
138+
expectNoError: true,
139+
expectedError: "",
140+
},
98141
}
99142

100143
for _, tt := range tests {
@@ -113,3 +156,69 @@ func TestParseFilter(t *testing.T) {
113156
})
114157
}
115158
}
159+
160+
func TestIsPathologicalRegex(t *testing.T) {
161+
tests := []struct {
162+
name string
163+
pattern string
164+
expected bool
165+
}{
166+
{
167+
name: "Safe pattern",
168+
pattern: "^[a-zA-Z0-9]+$",
169+
expected: false,
170+
},
171+
{
172+
name: "Nested quantifiers (a+)+",
173+
pattern: "^(a+)+$",
174+
expected: true,
175+
},
176+
{
177+
name: "Nested quantifiers (a*)*",
178+
pattern: "(a*)*",
179+
expected: true,
180+
},
181+
{
182+
name: "Overlapping alternation (a|a)*",
183+
pattern: "(a|a)*",
184+
expected: true,
185+
},
186+
{
187+
name: "Consecutive quantifiers **",
188+
pattern: "a**",
189+
expected: true,
190+
},
191+
{
192+
name: "Consecutive quantifiers ++",
193+
pattern: "a++",
194+
expected: true,
195+
},
196+
{
197+
name: "Catastrophic (.*)* pattern",
198+
pattern: "(.*)*",
199+
expected: true,
200+
},
201+
{
202+
name: "Catastrophic (.+)+ pattern",
203+
pattern: "(.+)+",
204+
expected: true,
205+
},
206+
{
207+
name: "Safe alternation with different patterns",
208+
pattern: "(cat|dog)*",
209+
expected: false,
210+
},
211+
{
212+
name: "Safe quantifier usage",
213+
pattern: "[0-9]+\\.[0-9]*",
214+
expected: false,
215+
},
216+
}
217+
218+
for _, tt := range tests {
219+
t.Run(tt.name, func(t *testing.T) {
220+
result := isPathologicalRegex(tt.pattern)
221+
assert.Equal(t, tt.expected, result, "Pattern: %s", tt.pattern)
222+
})
223+
}
224+
}

0 commit comments

Comments
 (0)