Add MatchLimit function for limiting pattern complexity

This commit adds the MatchLimit function, which it the
same as Match but will limit the complexity of the input pattern.
This is to avoid long running matches, specifically to avoid ReDos
attacks from arbritary inputs.

How it works:
The underlying match routine is recursive and may call itself when it
encounters a sandwiched wildcard pattern, such as: `user:*:name`.
Everytime it calls itself a counter is incremented.
The operation is stopped when counter > maxcomp*len(str).
This commit is contained in:
tidwall 2021-10-08 07:36:13 -07:00
parent 84b6aacbb9
commit 4c9fc61b49
2 changed files with 78 additions and 14 deletions

View File

@ -21,10 +21,47 @@ func Match(str, pattern string) bool {
if pattern == "*" { if pattern == "*" {
return true return true
} }
return match(str, pattern) return match(str, pattern, 0, nil, -1) == rMatch
}
// MatchLimit is the same as Match but will limit the complexity of the match
// operation. This is to avoid long running matches, specifically to avoid ReDos
// attacks from arbritary inputs.
//
// How it works:
// The underlying match routine is recursive and may call itself when it
// encounters a sandwiched wildcard pattern, such as: `user:*:name`.
// Everytime it calls itself a counter is incremented.
// The operation is stopped when counter > maxcomp*len(str).
func MatchLimit(str, pattern string, maxcomp int) (matched, stopped bool) {
if pattern == "*" {
return true, false
}
counter := 0
r := match(str, pattern, len(str), &counter, maxcomp)
if r == rStop {
return false, true
}
return r == rMatch, false
}
type result int
const (
rNoMatch result = iota
rMatch
rStop
)
func match(str, pat string, slen int, counter *int, maxcomp int) result {
// check complexity limit
if maxcomp > -1 {
if *counter > slen*maxcomp {
return rStop
}
*counter++
} }
func match(str, pat string) bool {
for len(pat) > 0 { for len(pat) > 0 {
var wild bool var wild bool
pc, ps := rune(pat[0]), 1 pc, ps := rune(pat[0]), 1
@ -42,7 +79,7 @@ func match(str, pat string) bool {
switch pc { switch pc {
case '?': case '?':
if ss == 0 { if ss == 0 {
return false return rNoMatch
} }
case '*': case '*':
// Ignore repeating stars. // Ignore repeating stars.
@ -50,39 +87,45 @@ func match(str, pat string) bool {
pat = pat[1:] pat = pat[1:]
} }
// If this is the last character then it must be a match. // If this star is the last character then it must be a match.
if len(pat) == 1 { if len(pat) == 1 {
return true return rMatch
} }
// Match and trim any non-wildcard suffix characters. // Match and trim any non-wildcard suffix characters.
var ok bool var ok bool
str, pat, ok = matchTrimSuffix(str, pat) str, pat, ok = matchTrimSuffix(str, pat)
if !ok { if !ok {
return false return rNoMatch
} }
// perform recursive wildcard search // Check for single star again.
if match(str, pat[1:]) { if len(pat) == 1 {
return true return rMatch
}
// Perform recursive wildcard search.
r := match(str, pat[1:], slen, counter, maxcomp)
if r != rNoMatch {
return r
} }
if len(str) == 0 { if len(str) == 0 {
return false return rNoMatch
} }
wild = true wild = true
default: default:
if ss == 0 { if ss == 0 {
return false return rNoMatch
} }
if pc == '\\' { if pc == '\\' {
pat = pat[ps:] pat = pat[ps:]
pc, ps = utf8.DecodeRuneInString(pat) pc, ps = utf8.DecodeRuneInString(pat)
if ps == 0 { if ps == 0 {
return false return rNoMatch
} }
} }
if sc != pc { if sc != pc {
return false return rNoMatch
} }
} }
str = str[ss:] str = str[ss:]
@ -90,7 +133,10 @@ func match(str, pat string) bool {
pat = pat[ps:] pat = pat[ps:]
} }
} }
return len(str) == 0 if len(str) == 0 {
return rMatch
}
return rNoMatch
} }
// matchTrimSuffix matches and trims any non-wildcard suffix characters. // matchTrimSuffix matches and trims any non-wildcard suffix characters.

View File

@ -462,7 +462,25 @@ func TestLotsaStars(t *testing.T) {
str = `*?**?**?**?**?**?***?**?**?**?**?*""` str = `*?**?**?**?**?**?***?**?**?**?**?*""`
pat = `*?*?*?*?*?*?**?**?**?**?**?**?**?*""` pat = `*?*?*?*?*?*?**?**?**?**?**?**?**?*""`
Match(str, pat) Match(str, pat)
}
func TestLimit(t *testing.T) {
var str, pat string
str = `,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,`
pat = `*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*"*,*`
_, stopped := MatchLimit(str, pat, 100)
if !stopped {
t.Fatal("expected true")
}
match, _ := MatchLimit(str, "*", 100)
if !match {
t.Fatal("expected true")
}
match, _ = MatchLimit(str, "*,*", 100)
if !match {
t.Fatal("expected true")
}
} }
func TestSuffix(t *testing.T) { func TestSuffix(t *testing.T) {