| | |

Serialization 14 ๐Ÿ“„ Denial-of-Service via Deserialization โ€” Size Limits and Resource Exhaustion

โฑ๏ธ Estimated read: 9 min | |

Deserialization does not need to execute code to be dangerous. A payload that is small on the wire can expand into a structure that exhausts memory, blocks the event loop, or crashes the process. The attack class is CWE-400, uncontrolled resource consumption, and it appears in every format and every language: YAML’s billion laughs, JSON’s deeply nested arrays, .NET’s object graphs, and Apache CXF’s unbounded attachment headers . The mitigation is the same in principleโ€”bound what the deserializer is allowed to allocateโ€”but the limits must be placed at the layer that sees the expansion before it happens.

This chapter covers the resource-exhaustion attacks that target deserializers, the size and depth limits that mitigate them, and the fail-closed patterns that prevent a rejected payload from reaching a consumer that would materialize it. The sections that follow walk through the expansion mechanisms, the limit taxonomy, and the placement rules that determine whether a limit actually protects the system.

Key point: A deserialization DoS succeeds when the parser or a downstream consumer allocates more than the input’s size suggests; mitigate with input size caps, graph depth limits, element count limits, and expansion ratios, placed before the allocation happens and enforced fail-closed.


a. The expansion mechanisms

Resource exhaustion through deserialization takes four forms, each exploiting a different property of the parser or the data model.

Alias expansion is YAML’s billion laughs. A small document defines anchors on collections and aliases them repeatedly, nesting the aliases so the resolved structure grows exponentially. Symfony’s advisory describes the mechanism precisely: aliases referencing collections can themselves point to collections containing aliases, and “a small input can blow up into a multi-gigabyte structure and exhaust memory” . The parser resolves the aliases; the expansion is a data-structure operation, not a code-execution operation.

Nesting depth exhausts the stack rather than the heap. A JSON document with thousands of nested arrays or objects causes the parser’s recursive descent to overflow the call stack, crashing the process. The Apache Struts JSON plugin added a maxDepth of 64 specifically for this reason . The L4YAML threat model lists “deeply nested structures” alongside billion laughs as a DoS category .

Element count exhausts memory linearly. A JSON array with ten million elements, or a message with an unbounded number of attachment headers, causes the parser to allocate proportional to the count. Apache CXF’s CVE-2026-50645 is exactly this: no restriction on the number of attachment headers a message can contain, leading to uncontrolled memory consumption . The fix imposed a default maximum of 500 attachments per message.

String size exhausts memory with a single value. A JSON string field of a gigabyte, or a YAML scalar of similar size, forces the parser to allocate the full string before any downstream validation runs. The Struts JSON plugin’s maxStringLength default of 256KB and maxLength of 2MB are the bounds that prevent this .


b. The limit taxonomy and default values

The limits fall into four families, and a production deserializer should have all of them configured.

LimitProtects AgainstExample Default
Input sizeOversized payloads2 MB (Struts)
Nesting depthStack overflow64 (Struts)
Element countLinear memory growth10,000 (Struts)
Expansion ratioAlias/decompression bombs50โ€“1000

The defaults are starting points, not universal answers. A configuration file parser can tolerate a 10 MB input; an API endpoint that accepts user JSON should cap at 2 MB or less. A YAML parser for trusted deployment manifests can allow aliases; a parser for user-uploaded YAML should reject them or limit collection alias resolutions to 128, following the SnakeYAML model that Symfony adopted .

The expansion ratio is the limit that catches bombs that pass the size and count checks. safezip uses max_per_member_ratio and max_total_ratio defaults of 50.0 for ZIP bombs, aborting extraction before runaway decompression exhausts disk or memory . The principle applies to any format where the output can be larger than the input: measure the expansion, not just the input.


c. Placement and fail-closed enforcement

A limit that runs after the allocation has already happened does not prevent the allocation. The placement rules are mechanical.

Cap input size before parsing. Read the byte stream, check its length against the cap, and reject before handing it to the deserializer. This is the only limit that can run before the parser allocates anything.

Set depth and element limits in the parser configuration. The Struts JSON plugin exposes maxElements, maxDepth, maxLength, maxStringLength, and maxKeyLength as configurable options . A parser without these options should not be used for untrusted input, or should be wrapped in a layer that enforces them.

Fail closed on breach. A rejected payload must not be partially parsed, cached, or passed to a consumer. The Apache CXF fix imposes a hard maximum of 500 attachments; a message exceeding it is rejected, not truncated . The Symfony fix counts collection alias resolutions and throws when the limit is exceeded . The error is the outcome; there is no partial success.

Bound the consumer, not just the parser. A parser that returns a DAG of shared references has not allocated the expanded structure. A downstream str(), deepcopy(), or serialization will. The guard must either reject the document at parse time or ensure that no consumer can materialize the expansion. The OpenAPI.NET fix added a conversion budget of 5,000,000 materialized nodes and a depth of 64, throwing a diagnostic error instead of allowing OOM .


Complete Example Session

# ============================================
# PART 1: THE EXPANSION ATTACK
# ============================================
# A small YAML document with nested aliases
a: &a ["x","x","x","x","x","x","x","x","x"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]

# The document is ~200 bytes.
# The resolved structure is megabytes at depth 4,
# gigabytes at depth 8.

# ============================================
# PART 2: THE LIMIT CONFIGURATION
# ============================================
# Symfony: limit collection alias resolutions
# new Parser(maxAliasesForCollections: 128)
# or reject all aliases:
# Yaml::parse($input, Yaml::PARSE_EXCEPTION_ON_ALIAS)

# Struts JSON plugin:
# struts.json.maxElements=10000
# struts.json.maxDepth=64
# struts.json.maxLength=2097152
# struts.json.maxStringLength=262144

# ============================================
# PART 3: THE FAIL-CLOSED PATTERN
# ============================================
# try: data = parse(untrusted)
# except ResourceLimitError: return None
# The error is the result. No partial document.

# ============================================
# PART 4: THE CONSUMER BOUND
# ============================================
# If the parser returns a DAG, bound the materialization:
# - Max materialized nodes: 5,000,000
# - Max depth: 64
# Throw instead of OOM.

The parts covered the expansion attack, the limit configuration, the fail-closed pattern, and the consumer bound.


Quick Reference

Attack Categories

CategoryMechanismExample
Alias expansionNested anchors multiply structureYAML billion laughs
Nesting depthStack overflowDeeply nested JSON
Element countLinear memory growthCXF attachment headers
String sizeSingle large allocationOversized JSON string

Limit Defaults

LimitStruts JSONSymfony YAMLsafezip
Max input2 MBโ€”โ€”
Max depth64โ€”โ€”
Max elements10,000โ€”10,000
Max string256 KBโ€”โ€”
Alias/ratioโ€”128 collections50.0 ratio

Placement Rules

RuleDetail
Input capBefore parsing
Parser limitsIn deserializer config
Expansion budgetBefore consumer materializes
Fail closedError, not partial data

Best Practices

โœ… Do This:

# Cap input before parsing
if len(payload) > 2_000_000: return None                     # โœ…

# Configure parser limits
# struts.json.maxDepth=64
# struts.json.maxElements=10000                               # โœ…

# Reject aliases for untrusted YAML
Yaml::parse($input, Yaml::PARSE_EXCEPTION_ON_ALIAS);          # โœ… 

# Bound consumer materialization
if node_count > 5_000_000: raise DiagnosticError              # โœ…

โŒ Don’t Do This:

# Trust a small payload
# (a 200-byte bomb expands to gigabytes)                      # โŒ

# Parse without depth or element limits
json.loads(untrusted)  # no bounds                             # โŒ

# Let a DAG reach str() or deepcopy
str(yaml.safe_load(bomb))  # hangs                             # โŒ

# Truncate instead of reject
# (partial data is worse than no data)                        # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Safe parser still OOMsAlias expansion not boundedCollection alias limit
Consumer hangs after parseDAG materialized downstreamGuard at parse, before consumer
Element count bypassedNested structures not countedCount total nodes, not top-level
Legitimate data rejectedLimit too lowScale limits to use case
Filter only checks class namesNo size/depth boundsAdd resource limits

Real-World Examples

1. Symfony YAML alias limit

new Parser(maxAliasesForCollections: 128);

2. Symfony alias rejection

Yaml::parse($input, Yaml::PARSE_EXCEPTION_ON_ALIAS);

3. Struts JSON depth limit

struts.json.maxDepth=64

4. CXF attachment limit

Max 500 attachments per message

5. safezip ratio limit

max_total_ratio=50.0

6. OpenAPI.NET node budget

MaxNodeCount = 5_000_000; MaxDepth = 64;

7. Input size cap

if len(data) > MAX_BYTES: raise ValueError

8. Fail-closed error

except ResourceLimitError: return None

9. DAG guard placement

Reject at parse, before str() or deepcopy

10. L4YAML strict preset

parseYamlSafe input .strict  // untrusted input

Visual

The Expansion Problem

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SMALL INPUT                  LARGE STRUCTURE           โ”‚
โ”‚                                                         โ”‚
โ”‚  a: &a [9 items]             9 nodes                    โ”‚
โ”‚  b: &b [9 ร— *a]              81 nodes                   โ”‚
โ”‚  c: &c [9 ร— *b]              729 nodes                  โ”‚
โ”‚  d: &d [9 ร— *c]              6,561 nodes                โ”‚
โ”‚                                                         โ”‚
โ”‚  ~200 bytes โ†’ megabytes at depth 8                      โ”‚
โ”‚  The parser resolves; the consumer explodes             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Limit Placement

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  LAYER 1: INPUT SIZE CAP                                โ”‚
โ”‚  Before parsing                                         โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  LAYER 2: PARSER LIMITS                                 โ”‚
โ”‚  Depth, elements, string size                           โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  LAYER 3: EXPANSION BUDGET                              โ”‚
โ”‚  Alias count, materialized nodes                        โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  CONSUMER                                               โ”‚
โ”‚  Only receives bounded data                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Fail-Closed Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  LIMIT EXCEEDED                                         โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Raise ResourceLimitError                               โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  Caller returns error / None / 400                      โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  No partial document, no cached DAG, no retry           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The DAG Trap

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PARSER RETURNS DAG                                     โ”‚
โ”‚  (small memory, shared references)                      โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  CONSUMER CALLS str() OR deepcopy()                     โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  EXPANSION HAPPENS                                      โ”‚
โ”‚  (memory explodes)                                      โ”‚
โ”‚                                                         โ”‚
โ”‚  Guard must be before the consumer, not after           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Attack classCWE-400 (uncontrolled resource consumption)
Alias bombYAML nested anchors
Depth bombStack overflow from nesting
Count bombUnbounded elements (CXF attachments)
String bombSingle large allocation
Input cap2 MB (Struts)
Depth limit64 (Struts)
Alias limit128 collections (Symfony)
Fail-closedError, not partial data

Key takeaways:

  • Deserialization DoS does not need code execution. A small payload that expands into a large structure exhausts memory or stack without a single gadget chain. The attack class is CWE-400, and it appears in every format .
  • Alias expansion is the YAML-specific bomb. Nested anchors and aliases multiply the resolved structure exponentially. Symfony’s fix counts collection alias resolutions and limits them to 128, following the SnakeYAML model .
  • Depth and element limits are the JSON and generic defenses. The Struts JSON plugin exposes maxDepth (64), maxElements (10,000), and maxStringLength (256KB) as configurable bounds . A parser without these options is not safe for untrusted input.
  • The guard must be before the allocation, not after. A parser that returns a DAG of shared references has not allocated the expansion. The consumer that calls str() or deepcopy() will. Bound the consumer or reject at parse time .
  • Fail closed. A rejected payload must not be partially parsed, cached, or retried. The error is the outcome, and the caller must treat it as such.

Remember: Resource exhaustion is deserialization’s quiet attack. It does not need a gadget chain or a vulnerable class; it needs a parser that trusts the input’s size and a consumer that materializes what the parser returns. Cap the input before parsing, set depth and element limits in the deserializer, bound alias expansion or reject aliases entirely, and fail closed when a limit is breached. The bomb is small; the explosion is deferred; the limit must run before the deferral becomes memory.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!