Overview
CVE-2017-8046 describes a vulnerability where malicious PATCH requests to servers using Spring Data REST could lead to remote code execution due to unsafe deserialization of JSON payloads. The issue affected Spring Data REST versions prior to 2.6.9 (Ingalls SR9) and prior to 3.0.1 (Kay SR1), with Spring Boot versions prior to 1.5.9 and 2.0 M6 also susceptible when combined with vulnerable Data REST configurations. Attackers could craft JSON data that, when deserialized on the server, could cause arbitrary Java code execution in the application process. This falls under CWE-20 (Improper Input Validation) and specifically enables deserialization-based gadget chains when PATCH semantics are exposed on REST endpoints. The impact is severe: attacker-controlled deserialization can compromise confidentiality, integrity, and availability, potentially enabling full server takeover depending on the gadgets on the classpath. The vulnerability manifests in real Spring Boot deployments where PATCH-based partial updates are applied directly to domain objects via untrusted JSON.
Affected Versions
Spring Data REST: prior to 2.6.9 (Ingalls SR9) and prior to 3.0.1 (Kay SR1); Spring Boot: prior to 1.5.9 and 2.0 M6
Code Fix Example
Spring Boot API Security Remediation
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.io.IOException;
import java.math.BigDecimal;
@RestController
public class WidgetController {
@Autowired private WidgetRepository repo;
// Vulnerable approach (illustrative only)
@PatchMapping("/widgets/{id}/patch-vulnerable")
public Widget patchWidgetVulnerable(@PathVariable Long id, @RequestBody JsonNode patch) throws IOException {
Widget w = repo.findById(id).orElseThrow(() -> new ResourceNotFoundException("Widget not found"));
ObjectMapper mapper = new ObjectMapper();
// BAD: enabling default typing can deserialize malicious payloads into arbitrary types
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Widget updated = mapper.readerForUpdating(w).readValue(patch.toString(), Widget.class);
repo.save(updated);
return updated;
}
// Safe approach (fix)
@PatchMapping("/widgets/{id}/patch-safe")
public Widget patchWidgetSafe(@PathVariable Long id, @RequestBody WidgetPatchDto patch) {
Widget w = repo.findById(id).orElseThrow(() -> new ResourceNotFoundException("Widget not found"));
if (patch.getName() != null) w.setName(patch.getName());
if (patch.getPrice() != null) w.setPrice(patch.getPrice());
repo.save(w);
return w;
}
public static class WidgetPatchDto {
private String name;
private BigDecimal price;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
}
}