Skip to content

Commit 0c1953f

Browse files
committed
Cap regex replacement output growth
1 parent 89c15f3 commit 0c1953f

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

vibes/builtins.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,20 +465,60 @@ func builtinRegexReplaceInternal(args []Value, kwargs map[string]Value, block Va
465465
if len(text) > maxRegexInputBytes {
466466
return NewNil(), fmt.Errorf("%s text exceeds limit %d bytes", method, maxRegexInputBytes)
467467
}
468+
if len(replacement) > maxRegexInputBytes {
469+
return NewNil(), fmt.Errorf("%s replacement exceeds limit %d bytes", method, maxRegexInputBytes)
470+
}
468471

469472
re, err := regexp.Compile(pattern)
470473
if err != nil {
471474
return NewNil(), fmt.Errorf("%s invalid regex: %v", method, err)
472475
}
473476

474477
if replaceAll {
475-
return NewString(re.ReplaceAllString(text, replacement)), nil
478+
replaced, err := regexReplaceAllWithLimit(re, text, replacement, method)
479+
if err != nil {
480+
return NewNil(), err
481+
}
482+
return NewString(replaced), nil
476483
}
477484

478485
loc := re.FindStringSubmatchIndex(text)
479486
if loc == nil {
480487
return NewString(text), nil
481488
}
482489
replaced := string(re.ExpandString(nil, replacement, text, loc))
490+
outputLen := len(text) - (loc[1] - loc[0]) + len(replaced)
491+
if outputLen > maxRegexInputBytes {
492+
return NewNil(), fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes)
493+
}
483494
return NewString(text[:loc[0]] + replaced + text[loc[1]:]), nil
484495
}
496+
497+
func regexReplaceAllWithLimit(re *regexp.Regexp, text string, replacement string, method string) (string, error) {
498+
matches := re.FindAllStringSubmatchIndex(text, -1)
499+
if len(matches) == 0 {
500+
return text, nil
501+
}
502+
503+
out := make([]byte, 0, len(text))
504+
last := 0
505+
for _, loc := range matches {
506+
segmentLen := loc[0] - last
507+
if len(out) > maxRegexInputBytes-segmentLen {
508+
return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes)
509+
}
510+
out = append(out, text[last:loc[0]]...)
511+
out = re.ExpandString(out, replacement, text, loc)
512+
if len(out) > maxRegexInputBytes {
513+
return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes)
514+
}
515+
last = loc[1]
516+
}
517+
518+
tailLen := len(text) - last
519+
if len(out) > maxRegexInputBytes-tailLen {
520+
return "", fmt.Errorf("%s output exceeds limit %d bytes", method, maxRegexInputBytes)
521+
}
522+
out = append(out, text[last:]...)
523+
return string(out), nil
524+
}

vibes/runtime_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,10 @@ func TestJSONAndRegexSizeGuards(t *testing.T) {
20282028
def regex_match_guard(pattern, text)
20292029
Regex.match(pattern, text)
20302030
end
2031+
2032+
def regex_replace_all_guard(text, pattern, replacement)
2033+
Regex.replace_all(text, pattern, replacement)
2034+
end
20312035
`)
20322036
if err != nil {
20332037
t.Fatalf("compile error: %v", err)
@@ -2058,6 +2062,17 @@ func TestJSONAndRegexSizeGuards(t *testing.T) {
20582062
if err == nil || !strings.Contains(err.Error(), "Regex.match text exceeds limit") {
20592063
t.Fatalf("expected Regex.match text guard error, got %v", err)
20602064
}
2065+
2066+
hugeReplacement := strings.Repeat("x", maxRegexInputBytes/2)
2067+
_, err = script.Call(
2068+
context.Background(),
2069+
"regex_replace_all_guard",
2070+
[]Value{NewString("abc"), NewString(""), NewString(hugeReplacement)},
2071+
CallOptions{},
2072+
)
2073+
if err == nil || !strings.Contains(err.Error(), "Regex.replace_all output exceeds limit") {
2074+
t.Fatalf("expected Regex.replace_all output guard error, got %v", err)
2075+
}
20612076
}
20622077

20632078
func TestLocaleSensitiveOperationsDeterministic(t *testing.T) {

0 commit comments

Comments
 (0)