Skip to content

Commit 60dc27c

Browse files
committed
Add JSON and regex guardrails
1 parent 4db303b commit 60dc27c

4 files changed

Lines changed: 84 additions & 5 deletions

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ Goal: reduce host-side boilerplate for common scripting tasks.
296296
### Compatibility and Safety
297297

298298
- [x] Define deterministic behavior for locale-sensitive operations.
299-
- [ ] Add quotas/guards around potentially expensive operations.
299+
- [x] Add quotas/guards around potentially expensive operations.
300300
- [ ] Ensure new stdlib functions are capability-safe where required.
301301

302302
### Testing and Docs

docs/builtins.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ payload = JSON.parse("{\"id\":\"p-1\",\"score\":10}")
102102
payload[:score] # 10
103103
```
104104

105+
`JSON.parse` enforces a 1 MiB input limit.
106+
105107
### `JSON.stringify(value)`
106108

107109
Serializes supported values (`hash`/`object`, `array`, scalar primitives) into
@@ -111,6 +113,8 @@ a JSON string:
111113
raw = JSON.stringify({ id: "p-1", score: 10, tags: ["a", "b"] })
112114
```
113115

116+
`JSON.stringify` enforces a 1 MiB output limit.
117+
114118
## Regex
115119

116120
### `Regex.match(pattern, text)`
@@ -132,6 +136,8 @@ Regex.replace_all("ID-12 ID-34", "ID-[0-9]+", "X") # "X X"
132136
Regex.replace("ID-12", "ID-([0-9]+)", "X-$1") # "X-12"
133137
```
134138

139+
Regex helpers enforce input guards (max pattern size 16 KiB, max text size 1 MiB).
140+
135141
## Module Loading
136142

137143
### `require(module_name, as: alias?)`

vibes/builtins.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ import (
1313
"time"
1414
)
1515

16-
const randomIDAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
16+
const (
17+
randomIDAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
18+
maxJSONPayloadBytes = 1 << 20
19+
maxRegexInputBytes = 1 << 20
20+
maxRegexPatternSize = 16 << 10
21+
)
1722

1823
func builtinAssert(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) {
1924
if len(args) == 0 {
@@ -225,7 +230,12 @@ func builtinJSONParse(exec *Execution, receiver Value, args []Value, kwargs map[
225230
return NewNil(), fmt.Errorf("JSON.parse does not accept blocks")
226231
}
227232

228-
decoder := json.NewDecoder(strings.NewReader(args[0].String()))
233+
raw := args[0].String()
234+
if len(raw) > maxJSONPayloadBytes {
235+
return NewNil(), fmt.Errorf("JSON.parse input exceeds limit %d bytes", maxJSONPayloadBytes)
236+
}
237+
238+
decoder := json.NewDecoder(strings.NewReader(raw))
229239
decoder.UseNumber()
230240

231241
var decoded any
@@ -267,6 +277,9 @@ func builtinJSONStringify(exec *Execution, receiver Value, args []Value, kwargs
267277
if err != nil {
268278
return NewNil(), fmt.Errorf("JSON.stringify failed: %v", err)
269279
}
280+
if len(payload) > maxJSONPayloadBytes {
281+
return NewNil(), fmt.Errorf("JSON.stringify output exceeds limit %d bytes", maxJSONPayloadBytes)
282+
}
270283
return NewString(string(payload)), nil
271284
}
272285

@@ -384,12 +397,20 @@ func builtinRegexMatch(exec *Execution, receiver Value, args []Value, kwargs map
384397
if args[0].Kind() != KindString || args[1].Kind() != KindString {
385398
return NewNil(), fmt.Errorf("Regex.match expects string pattern and text")
386399
}
400+
pattern := args[0].String()
401+
text := args[1].String()
402+
if len(pattern) > maxRegexPatternSize {
403+
return NewNil(), fmt.Errorf("Regex.match pattern exceeds limit %d bytes", maxRegexPatternSize)
404+
}
405+
if len(text) > maxRegexInputBytes {
406+
return NewNil(), fmt.Errorf("Regex.match text exceeds limit %d bytes", maxRegexInputBytes)
407+
}
387408

388-
re, err := regexp.Compile(args[0].String())
409+
re, err := regexp.Compile(pattern)
389410
if err != nil {
390411
return NewNil(), fmt.Errorf("Regex.match invalid regex: %v", err)
391412
}
392-
match := re.FindString(args[1].String())
413+
match := re.FindString(text)
393414
if match == "" {
394415
return NewNil(), nil
395416
}
@@ -426,6 +447,12 @@ func builtinRegexReplaceInternal(args []Value, kwargs map[string]Value, block Va
426447
text := args[0].String()
427448
pattern := args[1].String()
428449
replacement := args[2].String()
450+
if len(pattern) > maxRegexPatternSize {
451+
return NewNil(), fmt.Errorf("%s pattern exceeds limit %d bytes", method, maxRegexPatternSize)
452+
}
453+
if len(text) > maxRegexInputBytes {
454+
return NewNil(), fmt.Errorf("%s text exceeds limit %d bytes", method, maxRegexInputBytes)
455+
}
429456

430457
re, err := regexp.Compile(pattern)
431458
if err != nil {

vibes/runtime_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,6 +1887,52 @@ func TestJSONAndRegexMalformedInputs(t *testing.T) {
18871887
}
18881888
}
18891889

1890+
func TestJSONAndRegexSizeGuards(t *testing.T) {
1891+
engine := MustNewEngine(Config{MemoryQuotaBytes: 4 << 20})
1892+
script, err := engine.Compile(`
1893+
def parse_raw(raw)
1894+
JSON.parse(raw)
1895+
end
1896+
1897+
def stringify_value(value)
1898+
JSON.stringify(value)
1899+
end
1900+
1901+
def regex_match_guard(pattern, text)
1902+
Regex.match(pattern, text)
1903+
end
1904+
`)
1905+
if err != nil {
1906+
t.Fatalf("compile error: %v", err)
1907+
}
1908+
1909+
largeJSON := `{"data":"` + strings.Repeat("x", maxJSONPayloadBytes) + `"}`
1910+
_, err = script.Call(context.Background(), "parse_raw", []Value{NewString(largeJSON)}, CallOptions{})
1911+
if err == nil || !strings.Contains(err.Error(), "JSON.parse input exceeds limit") {
1912+
t.Fatalf("expected JSON.parse size guard error, got %v", err)
1913+
}
1914+
1915+
largeValue := NewHash(map[string]Value{
1916+
"data": NewString(strings.Repeat("x", maxJSONPayloadBytes)),
1917+
})
1918+
_, err = script.Call(context.Background(), "stringify_value", []Value{largeValue}, CallOptions{})
1919+
if err == nil || !strings.Contains(err.Error(), "JSON.stringify output exceeds limit") {
1920+
t.Fatalf("expected JSON.stringify size guard error, got %v", err)
1921+
}
1922+
1923+
largePattern := strings.Repeat("a", maxRegexPatternSize+1)
1924+
_, err = script.Call(context.Background(), "regex_match_guard", []Value{NewString(largePattern), NewString("aaa")}, CallOptions{})
1925+
if err == nil || !strings.Contains(err.Error(), "Regex.match pattern exceeds limit") {
1926+
t.Fatalf("expected Regex.match pattern guard error, got %v", err)
1927+
}
1928+
1929+
largeText := strings.Repeat("a", maxRegexInputBytes+1)
1930+
_, err = script.Call(context.Background(), "regex_match_guard", []Value{NewString("a+"), NewString(largeText)}, CallOptions{})
1931+
if err == nil || !strings.Contains(err.Error(), "Regex.match text exceeds limit") {
1932+
t.Fatalf("expected Regex.match text guard error, got %v", err)
1933+
}
1934+
}
1935+
18901936
func TestLocaleSensitiveOperationsDeterministic(t *testing.T) {
18911937
script := compileScript(t, `
18921938
def locale_ops()

0 commit comments

Comments
 (0)