gohugoio/hugo · error

too many YAML aliases for non-scalar nodes

Error message

too many YAML aliases for non-scalar nodes

What it means

After YAML unmarshaling, Hugo counts how often each non-scalar (map/slice) node is referenced via YAML aliases and fails if any exceeds a size-scaled limit (100 for files under ~2 KB, up to 10,000 for larger files). This defends against Billion-Laughs-style expansion attacks: goccy/go-yaml itself isn't vulnerable at parse time, but rendering the massively self-referencing structure later (e.g. via RenderString) would be (see goccy/go-yaml issue 461).

Source

Thrown at parser/metadecoders/decoder.go:231

// we can easily get a delayed laughter when we try to render this very big structure later,
// e.g. via RenderString.
func validateAliasLimitForCollections(v any, limit int) error {
	if limit <= 0 {
		limit = 1000
	}

	collectionRefCounts := make(map[uintptr]int)

	checkCollectionRef := func(v *any) error {
		// Conversion of a Pointer to a uintptr (but not back to Pointer) is considered safe.
		// See https://pkg.go.dev/unsafe#pkg-functions
		ptr := uintptr(unsafe.Pointer(v))
		if ptr == 0 {
			return nil
		}
		collectionRefCounts[ptr]++
		if collectionRefCounts[ptr] > limit {
			return fmt.Errorf("too many YAML aliases for non-scalar nodes")
		}
		return nil
	}

	var validate func(v any) error
	validate = func(v any) error {
		switch vv := v.(type) {
		case *map[string]any:
			if err := checkCollectionRef(&v); err != nil {
				return err
			}
			for _, vvv := range *vv {
				if err := validate(vvv); err != nil {
					return err
				}
			}
		case map[string]any:
			if err := checkCollectionRef(&v); err != nil {

View on GitHub ↗ (pinned to 8a468df065)

Solutions

  1. Reduce alias usage: expand the shared anchor inline for some entries, or restructure so each row stores only its own data and the shared defaults are merged in templates instead.
  2. Split one small alias-dense file into larger or multiple files — the limit scales with file size (100/<2KB, 5000/<10KB, 10000 above), so padding legitimate data into a larger file raises the cap.
  3. Convert the data file to JSON or TOML, which have no aliases and thus bypass the check.
  4. If the file is untrusted input, treat this as the guard working correctly — do not attempt to bypass it.

Example fix

# before (small file, one anchor aliased hundreds of times)
defaults: &d {a: 1, b: 2}
rows:
  - *d
  - *d
  # ... 200 more

# after (store rows plainly; merge defaults in the template)
defaults: {a: 1, b: 2}
rows:
  - {}
  - {}
Defensive patterns

Strategy: validation

Validate before calling

# Pre-scan YAML for alias-expansion abuse before feeding it to Hugo
grep -c '\*[A-Za-z0-9_]' data.yaml   # large counts of aliases on mappings/sequences are suspect

Try / catch

if err != nil && strings.Contains(err.Error(), "too many YAML aliases") {
    // reject the document as untrusted (billion-laughs style); do not raise limits
}

Prevention

When it happens

Trigger: Parsing YAML front matter, data files, or config where a single anchored map or sequence (&anchor) is referenced with *alias more times than calculateCollectionAliasLimit allows for the file's size — most aggressively in small files (<2 KB, limit 100), which is where malicious payloads live.

Common situations: Legitimate data files that use one shared anchor (e.g. a common defaults map) across thousands of rows in a small file; security scanners or fuzzers feeding Billion Laughs payloads; content pipelines that generate alias-heavy YAML. Introduced alongside the goccy/go-yaml migration in Hugo v0.152.0, so files that worked on older Hugo may newly fail.

Related errors


AI-assisted analysis of gohugoio/hugo@8a468df065 (2026-07-31). Data as JSON: /data/errors/6430f203e5d182be.json. Report an issue: GitHub ↗.