Recursive AI

Read untrusted YAML safely

All oneyaml Go recipes

YAML from a request body, an upload or another tenant can be built to exhaust memory or the stack. oneyaml bounds nesting depth, alias expansion, number size and patch work by default; for input you don’t control, add Untrusted().

GO
package main
 
import (
	"errors"
	"fmt"
	"strings"
 
	oneyaml "oneyaml.dev/go"
)
 
func main() {
	// A normal request body reads as usual.
	var ok map[string]any
	fmt.Println("ok:", oneyaml.Unmarshal([]byte("name: web\nports: [80]\n"), &ok, oneyaml.Untrusted()))
 
	// "Billion laughs": nine levels of aliases, each repeated ten times.
	var b strings.Builder
	b.WriteString("l0: &l0 [x, x, x, x, x, x, x, x, x, x]\n")
	for i := 1; i < 9; i++ {
		refs := strings.TrimSuffix(strings.Repeat(fmt.Sprintf("*l%d, ", i-1), 10), ", ")
		fmt.Fprintf(&b, "l%d: &l%d [%s]\n", i, i, refs)
	}
	var v any
	err := oneyaml.Unmarshal([]byte(b.String()), &v, oneyaml.Untrusted())
	var le *oneyaml.LimitError
	if errors.As(err, &le) {
		fmt.Println("refused:", le.Limit, errors.Is(err, oneyaml.ErrLimit))
	}
 
	// Deep nesting is refused too, without overflowing the stack.
	err = oneyaml.Unmarshal([]byte(strings.Repeat("[", 100_000)), &v, oneyaml.Untrusted())
	if errors.As(err, &le) {
		fmt.Println("deep:", le.Limit)
	}
 
	// Size the limits yourself: start from UntrustedLimits and change fields.
	limits := oneyaml.UntrustedLimits()
	limits.MaxInputBytes = 1 << 10
	err = oneyaml.Unmarshal([]byte(strings.Repeat("a: b\n", 1000)), &v, oneyaml.Limits(limits))
	if errors.As(err, &le) {
		fmt.Println("too big:", le.Limit)
	}
}
Text
ok: <nil>
refused: MaxAliasNodes true
deep: MaxDepth
too big: MaxInputBytes

Good to know

  • Bounded by default: nesting depth, alias expansion, the digits of a number and the work of a patch, unless you pass NoLimits().
  • Bounded only with Untrusted(): input size, documents per stream and scalar length. Pass Limits(...) with your own LimitSet to size them; start from UntrustedLimits() and write the struct with field names.
  • A limit error is a *LimitError whose Limit names the bound, and it matches oneyaml.ErrLimit with errors.Is.
  • Your own hooks (UnmarshalYAML and the like) run outside these bounds, and the compat packages have no Untrusted() mode.
  • No unsafe and no cgo. A panic caused by input is treated as a security bug.

Every limit and its value is in Limits.