Limiting Alias Expansion in a YAML Parser Across Languages
Where each parser sets its alias limit: SnakeYAML options, Go's built-in check, Ruby's aliases flag, DuckDB's node budget and a PyYAML guard, all tested.

An alias bomb is a small document that is expensive to use, and the defense is a limit. The parsers disagree about where the limit lives, what it counts and what the default is. Some refuse aliases outright, some count them, some measure the expanded size and some leave you to build the guard yourself. This post collects the controls in one place, with tests, and explains why the number of aliases is a poor measure. It belongs to the cluster on YAML security pitfalls and follows Billion Laughs style attacks through YAML aliases, which shows the attack and the cost.
What to limit
Three quantities describe the risk, and a good control measures at least one.
Number of aliases. Easy to count, as a parser sees them in order. It correlates weakly with cost, for the reason below.
Expanded size. The number of nodes the document becomes when each alias is replaced by what it points to. This is what memory and time depend on.
Nesting depth. How deep the structure goes. It bounds recursion in the parser and in everything that walks the result.
A file size cap is a fourth control, and it is separate: the bomb in the previous post was 378 bytes.
Counting aliases is a weak signal
A tempting defense is "reject documents with more than N aliases". We counted anchors and aliases in three documents using PyYAML's event stream:
ci file anchors=1 aliases=2
bomb depth 7 anchors=7 aliases=54
no aliases anchors=0 aliases=0The ordinary CI file, with one anchor reused through two merge keys, has 2 aliases. The depth-seven bomb, which expands to 34 MB, has 54. There is no number of aliases that cleanly separates the two: set the limit at 50 and the bomb passes by a margin of four aliases, set it at 10 and a large legitimate CI file with many reused jobs fails. The reason is that cost grows with the product of the fan-out at each level, not with the count of references. Nine aliases per level and seven levels is 54 references and 9 to the power 7 nodes.
So a pure alias count catches clumsy attacks and misses careful ones. Treat it as a coarse limit, and prefer a measure of expanded size when the parser offers one.
SnakeYAML: a count with options
SnakeYAML's LoaderOptions has setMaxAliasesForCollections, default 50. It counts aliases to non-scalar nodes (lists and maps), which are the ones that multiply. We loaded a document of 61 lines in which each list holds two references to the one before:
max=50: YAMLException: Number of aliases for non-scalar nodes exceeds the specified max=50
max=1000: loaded 61 keys in 5 msThe default refused it. Raising the limit to 1000 let it through, and the load took 5 milliseconds, because the loader shares the nodes. The cost comes later, when something walks it. That is the point of the numbers: a limit is a policy, and raising it for a legitimate large file raises the exposure for every input. Keep the default for untrusted sources and use a separate, higher setting only for files you control.
The same options object limits nesting. setNestingDepthLimit(10) rejected a 30-level list with YAMLException: Nesting Depth exceeded max 10. Both controls matter, as one stops fan-out and the other stops depth. There are further settings for the number of code points in a document and for scalar sizes; read the options for your version.
Go: built in, not configurable
gopkg.in/yaml.v3 decodes with an internal check on the ratio between decoded nodes and alias expansions. A nine-level bomb:
alias bomb: yaml: document contains excessive aliasingand a document with one anchor and two aliases decoded without complaint. There is nothing to configure in that version, which is convenient and means you cannot loosen it for a legitimate case. If a real file trips it, the fix is to restructure the file. The check protects the decoder, not the code you write after it, so it does not replace a size cap.
Ruby: aliases are off until you ask
Psych's safe_load and load reject aliases by default:
safe_load with alias: Psych::AliasesNotEnabled: Alias parsing was not enabled. To enable it, pass `aliases: true` to `Psych::load` or `Psych::safe_load`.Enabling them is a deliberate argument, aliases: true. For input that does not need anchors, which includes most uploaded data, leave them off. This is the strongest default of the group: the dangerous feature does not exist until the caller opts in, and the opt-in is visible in code review. Pair it with a size cap, since a flat file with no aliases can still be huge.
DuckDB: a node budget for the expanded size
DuckDB's yaml extension measures expansion directly. The setting is a function call:
SELECT yaml_set_max_expansion_nodes(1000);
SELECT count(*) FROM read_yaml('bomb.yaml');Invalid Input Error: YAML expansion exceeded the maximum node budget (1000); the input may contain an alias/anchor expansion bomb. Raise the limit with yaml_set_max_expansion_nodes(N) if this is a legitimate document.The same depth-seven file read without a budget took about 51 seconds and 1 GB of memory, so the setting is the difference between an error and an outage. There is a companion yaml_set_max_nesting_depth for depth and yaml_set_max_input_size for bytes. Set all three in any session that reads files from outside; whatever the defaults are in your version, writing them down in the query makes them explicit. The extension is described in using DuckDB to query data stored as YAML.
PyYAML: build the guard
PyYAML has no alias limit. It shares nodes, so loading is cheap and the damage arrives later. You can add the control yourself by composing the document into nodes and walking them with memoization, which measures the expanded size without expanding anything:
def expanded_size(node, memo, depth=0):
if depth > MAX_DEPTH:
raise Rejected("nesting too deep")
if id(node) in memo:
return memo[id(node)]
size = 1
if isinstance(node, yaml.SequenceNode):
size += sum(expanded_size(c, memo, depth + 1) for c in node.value)
elif isinstance(node, yaml.MappingNode):
size += sum(expanded_size(k, memo, depth + 1) + expanded_size(v, memo, depth + 1) for k, v in node.value)
memo[id(node)] = size
if size > MAX_EXPANDED_NODES:
raise Rejected("expands too far")
return sizeWith a budget of 10,000 nodes this rejected the depth-seven bomb (expands to more than 10000 nodes) and accepted a document with one reused mapping. The complete guard, with byte caps and type checks, is in handling YAML uploads from users. The node count is the right measure, and you build the check once and reuse it.
Choosing limits
Look at your legitimate files first. Run the guard in measuring mode over every YAML file in your repositories and record the largest expanded size and the deepest nesting. Our CI-style file measured a handful of nodes; a large Kubernetes bundle will measure thousands. Set the budget at a few times the largest legitimate value, set the depth to twice the deepest, and log every rejection with the source so you can tell a real file that grew from an attack. Different sources deserve different limits: files from your own repository can be generous, files from users strict.
When an alias limit is the wrong control
The attack is not aliases. A flat file with millions of small keys, or one enormous scalar, uses no aliases. A size cap and a node budget cover it; an alias limit does not.
The cost is downstream. A parser that handles aliases efficiently can feed a serializer, a validator or a database that expands them. Limit at the boundary where the cost occurs.
You need aliases for legitimate files. If your CI files or manifests use them heavily, a low alias limit will fail ordinary pull requests. Raise it for trusted sources.
You trust the source. Files committed by your own team need less protection than uploads, though a compromised contributor or a copied snippet can still carry a bomb. Reasonable defaults cost nothing.
The limits are documented in the SnakeYAML wiki, the Psych reference and the Go yaml.v3 package. The behavior of aliases is specified in the YAML 1.2.2 specification. To read a file's structure by eye, use the YAML formatter.


