Protecting Your Java Application from Malicious Payloads with Jackson’s StreamReadConstraints
When working with modern Java web applications, the Jackson ObjectMapper is often at the heart of data processing. It quietly takes care of…
Protecting Your Java Application from Malicious Payloads with Jackson’s StreamReadConstraints
When working with modern Java web applications, the Jackson ObjectMapper is often at the heart of data processing. It quietly takes care of converting JSON payloads into Java objects — a crucial step in almost every REST API or microservice. This deserialization process, while convenient, can also become a potential attack surface if not properly configured.
Imagine a scenario where your application exposes an endpoint expecting a JSON body. Normally, users send small, well-structured payloads — but what if an attacker decides to send a massive JSON input, perhaps hundreds of megabytes or even larger? As the ObjectMapper starts deserializing the payload, your application’s heap memory can quickly be overwhelmed. The result: OutOfMemoryError, degraded performance, or even a full application crash.
Fortunately, Jackson provides a simple yet powerful mechanism to protect against such attacks — the StreamReadConstraints API. By configuring these constraints, you can limit aspects of the incoming JSON stream (such as its length, nesting depth, or number of properties). When a payload exceeds these limits, Jackson throws an exception instead of consuming more memory, keeping your application safe and stable.
Jackson’s StreamReadConstraints allows you to fine-tune how much data your application will accept during deserialization by setting specific limits on different aspects of the incoming JSON. The maxStringLength parameter defines the maximum number of characters allowed in any individual JSON string value — useful to prevent excessively long text fields from consuming too much memory. The maxNumberLength sets the limit on the length of numeric values, ensuring that an attacker cannot send an arbitrarily large number that would strain parsing resources. The maxNestingDepth controls how deeply JSON objects and arrays can be nested, helping protect against recursive or deeply nested structures that could cause stack overflows or slow parsing. The maxDocumentLength sets an upper bound on the total input length (in characters or bytes, depending on whether Jackson is reading from a Reader or an InputStream). In practice, this limits how much data the parser will consume from the incoming JSON before throwing a StreamConstraintsException, protecting your application from extremely large payloads. Finally, the maxNameLength parameter limits the length of JSON field names, which can otherwise be exploited to create unnecessarily large or obfuscated payloads. Together, these constraints provide a robust defense layer that ensures your ObjectMapper deserializes only manageable and predictable data structures.
To see how these constraints can be applied in practice, let’s look at a simple example using a Spring Boot Java application. Below, we define a custom ObjectMapper bean configured with StreamReadConstraints. These limits can be externalized into a properties file so that they can be easily adjusted without code changes:
@Bean
@Primary
public ObjectMapper objectMapper() {
Integer maxStringLength = properties.getMaxStringLength();
Integer maxNumberLength = properties.getMaxNumberLength();
Integer maxNestingDepth = properties.getMaxNestingDepth();
Integer maxNameLength = properties.getMaxNameLength();
Long maxDocumentLength = properties.getMaxDocumentLength();
if (maxStringLength == null || maxNumberLength == null || maxNestingDepth == null ||
maxNameLength == null || maxDocumentLength == null) {
log.error("ObjectMapperProperties values must not be null: maxStringLength={}, maxNumberLength={}, maxNestingDepth={}, maxNameLength={}, maxDocumentLength={}",
maxStringLength, maxNumberLength, maxNestingDepth, maxNameLength, maxDocumentLength);
throw new NullPointerException("ObjectMapperProperties values must not be null");
}
ObjectMapper defaultObjectMapper = new ObjectMapper();
defaultObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
StreamReadConstraints streamReadConstraints = StreamReadConstraints.builder()
.maxStringLength(maxStringLength)
.maxNumberLength(maxNumberLength)
.maxNestingDepth(maxNestingDepth)
.maxNameLength(maxNameLength)
.maxDocumentLength(maxDocumentLength)
.build();
defaultObjectMapper.getFactory().setStreamReadConstraints(streamReadConstraints);
return defaultObjectMapper;
}
Here, each constraint value is retrieved from a configuration class (ObjectMapperProperties) so that you can easily adjust them through your application settings. The StreamReadConstraints builder allows you to define all relevant limits — from the maximum nesting depth to the maximum document size — and then attach them to the ObjectMapper factory. Once this configuration is in place, if a request payload exceeds any of these constraints, Jackson will immediately throw an exception during deserialization, preventing excessive memory usage and protecting your service from denial-of-service (DoS) attacks.
Once this custom ObjectMapper bean is defined and marked with the @Primary annotation, Spring Boot will automatically use it wherever JSON serialization or deserialization is required across the application. This includes all controller methods that consume or produce JSON — such as the TransactionController shown below.
In the following example, the endpoint /transaction accepts a JSON payload representing a MerchantTransactionDto. When a request is received, Spring automatically invokes the configured ObjectMapper to deserialize the incoming JSON into the merchantTransactionDto object.
@Slf4j
@RestController
public class TransactionController {
@PostMapping(value = "/transaction",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseDto> acceptTransaction(@RequestBody MerchantTransactionDto merchantTransactionDto) {
if (merchantTransactionDto == null) {
ResponseDto responseDto = new ResponseDto("Request body must not be null");
return ResponseEntity.badRequest().body(responseDto);
}
log.info("Received request to process a transaction");
boolean isMerchantNameEmpty = StringUtils.isBlank(merchantTransactionDto.getMerchantName());
boolean isAmountEmpty = StringUtils.isBlank(merchantTransactionDto.getAmount());
boolean isCategoryEmpty = StringUtils.isBlank(merchantTransactionDto.getCategory());
if (isMerchantNameEmpty || isAmountEmpty || isCategoryEmpty) {
log.error("One or more fields in MerchantTransactionDto are empty: merchantNameEmpty={}, amountEmpty={}, categoryEmpty={}",
isMerchantNameEmpty, isAmountEmpty, isCategoryEmpty);
ResponseDto responseDto = new ResponseDto("One or more fields in MerchantTransactionDto are empty");
return ResponseEntity.badRequest().body(responseDto);
}
log.info("About to process the transaction: {}", merchantTransactionDto);
ResponseDto responseDto = new ResponseDto("ObjectMapper successfully processed the request");
return ResponseEntity.ok(responseDto);
}
}
Since this controller relies on the same ObjectMapper managed by Spring’s application context, any incoming JSON request will automatically be subject to the constraints defined in your StreamReadConstraints configuration. This means that if a client tries to send a payload exceeding one of your configured limits — for example, a very large JSON document or an excessively deep structure — Jackson will throw a StreamConstraintsException before deserialization completes. This effectively shields the endpoint from processing malicious or oversized payloads without requiring any additional logic inside the controller itself.
Configuring StreamReadConstraints on your ObjectMapper is a simple yet highly effective way to harden your Java application against malicious or accidental oversized JSON payloads. By setting limits on aspects like string length, document size, and nesting depth, you give your application an automatic safeguard that prevents excessive memory consumption and potential denial-of-service scenarios — all without adding extra complexity to your business logic.
Since Spring Boot automatically uses the configured ObjectMapper for JSON deserialization, these constraints apply consistently across all your REST endpoints. It’s a lightweight, configuration-driven layer of protection that can make a significant difference in production environments.
You can find the full working example, including the Spring Boot configuration and the sample controller, in my GitHub repository:
메타데이터
- post_id
- 692bb57d673b
- slug
- protecting-your-java-application-from-malicious-payloads-with-jacksons-streamreadconstraints-692bb57d673b
- url
- https://medium.com/@ilicetoantonio/protecting-your-java-application-from-malicious-payloads-with-jacksons-streamreadconstraints-692bb57d673b
- canonical_url
- https://medium.com/@ilicetoantonio/protecting-your-java-application-from-malicious-payloads-with-jacksons-streamreadconstraints-692bb57d673b
- author_url
- https://medium.com/@ilicetoantonio
- status
- ok
- fetched_at
- 2026-07-24 03:33:04