Overview
CVE-2017-8046 describes a vulnerable path in Spring Data REST where crafted JSON payloads sent via PATCH requests could cause a server to deserialize input in a way that instantiates attacker-controlled Java types. When a server deserializes untrusted input with polymorphic typing enabled, it can lead to remote code execution, data exposure, or full host compromise. This is particularly dangerous in environments using Spring Data REST with exposed PATCH endpoints that merge request data into domain objects. The advisory notes that affected versions include Spring Data REST before 2.6.9 (Ingalls SR9) and before 3.0.1 (Kay SR1), with Spring Boot versions prior to 1.5.9 and 2.0 M6 also being at risk. While the class of vulnerability is rooted in deserialization, practical risk manifests in ways that undermine object-level access controls when handling partial updates via PATCH, effectively enabling unauthorized object manipulation or code execution.
In practice, an attacker could submit a JSON payload containing type metadata or shape that, when deserialized by a misconfigured ObjectMapper, causes the runtime to instantiate an unexpected class or gadget chain. If the update logic binds directly to entity classes or merges payloads into existing objects without strict validation or authorization, the attacker could alter fields they should not touch or trigger code execution paths during object construction or merging. This undermines object-level authorization for resources identified by IDs in the URL path and can escalate from a simple data modification to full app compromise depending on the gadget chain exposed by the attacker-controlled payload.
Mitigations focus on upgrading to patched releases, and on changing code paths to avoid untrusted deserialization of domain objects. Key steps include disabling polymorphic typing in Jackson, replacing direct binding to entities with strictly defined DTOs, applying explicit authorization checks for update operations, and validating input rigorously. In short, revert risky deserialization features, enforce explicit mappings, and verify permissions on a per-resource basis to prevent object-level authorization failures in Spring Boot applications.
Affected Versions
Spring Data REST before 2.6.9 (Ingalls SR9) and before 3.0.1 (Kay SR1); Spring Boot before 1.5.9 and before 2.0 M6
Code Fix Example
Spring Boot API Security Remediation
VULNERABLE PATTERN (bad):
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
private final UserRepository userRepository;
private final ObjectMapper mapper;
public DemoController(UserRepository repo) {
this.userRepository = repo;
// Vulnerable: polymorphic typing enabled
this.mapper = new ObjectMapper();
this.mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
}
@PatchMapping("/users/{id}")
public User patchUser(@PathVariable Long id, @RequestBody Object payload) {
User existing = userRepository.findById(id).orElseThrow(() -> new RuntimeException("Not found"));
// Deserializes into a User, but with default typing enabled this can instantiate attacker-controlled types
User updated = mapper.convertValue(payload, User.class);
existing.merge(updated);
userRepository.save(existing);
return existing;
}
}
FIXED PATTERN (good):
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoControllerFixed {
private final UserRepository userRepository;
public DemoControllerFixed(UserRepository repo) {
this.userRepository = repo;
}
// Secure path: use a strict DTO and explicit field mapping
@PatchMapping("/users/{id}")
public User patchUserSecured(@PathVariable Long id, @RequestBody UpdateUserDTO dto) {
User existing = userRepository.findById(id).orElseThrow(() -> new RuntimeException("Not found"));
if (dto.getName() != null) existing.setName(dto.getName());
if (dto.getEmail() != null) existing.setEmail(dto.getEmail());
userRepository.save(existing);
return existing;
}
}
class UpdateUserDTO {
private String name;
private String email;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
class User {
private Long id;
private String name;
private String email;
public void merge(User other) {
if (other.name != null) this.name = other.name;
if (other.email != null) this.email = other.email;
}
// getters/setters omitted for brevity
}