How the request actually
reaches the vulnerable line.
Reference notes written at the level the code runs at: constant pool offsets, class loader delegation order, the Tomcat valve chain, the OGNL evaluation that made S2-045 possible, and the eighteen sinks that account for most of what turns up in a Java code review. Written for triage, not for a certification.
00Why this exists
You can find a Java web vulnerability by behaviour — send a payload, observe a response. Explaining it from the source is a different skill, and it is the one that triage demands. The gap between the two is almost always framework machinery: code you did not write, that ran between the socket and your method, and that decided what your method received.
These pages close that gap. Each one takes a layer, names the real classes and methods, and shows what an attacker can reach there. Where a claim can be verified on your own machine, the command to verify it is included and its output is real.
Code marked vulnerable is reproduced from the shape that appears in real applications, not from a CTF. Code marked fixed is the root-cause fix, not a filter in front of the bug. Every section ends with what is not a fix, because that is the part that gets argued about in a triage queue.
01The five layers
A finding sits in exactly one layer, and the fix belongs to whoever owns that layer. Most wasted triage time is spent fixing in the wrong one.
| Layer | Owned by | Representative failures |
|---|---|---|
NetworkNioEndpoint | Tomcat config | AJP reachable (Ghostcat, CVE-2020-1938), shutdown port exposed, connector bound to 0.0.0.0, TLS downgrade |
Trust boundaryCoyoteAdapter | — | Nothing is owned here. This is where bytes become HttpServletRequest and everything past it is attacker-controlled |
ContainerStandardWrapperValve | web.xml, your filters | Filter-ordering bypass, session fixation, path normalisation mismatch between filter and servlet |
| Framework Struts / Spring | Framework version + your config | OGNL and SpEL injection, mass assignment, security chain matcher ordering |
| Application your code | You | Everything in the sink catalogue. The overwhelming majority of exploitable findings |
The boundary row is the one worth internalising. Before
CoyoteAdapter.service() the request is byte chunks in a recycled buffer.
After it, it is an object graph your code reads. Nothing the parser produces
is trusted — not the method, not the path, not Content-Length,
and not Content-Type, which is exactly the field S2-045 abused.
02Pages
ClassNotFoundException and NoClassDefFoundError.
Http11Processor, the mapper, the valve chain,
the filter chain. Including the URI-decoding mismatch that bypasses filters.
excludedPatterns actually guards, and the full S2-045 chain.
DispatcherServlet.doDispatch, argument resolvers,
WebDataBinder, and why matcher order decides whether a rule applies.
readObject executes before your cast runs, the
CommonsCollections shape, and how ObjectInputFilter intercepts it.
03Versions these notes describe
Behaviour differs between versions in ways that matter. Everything here was checked against:
| Component | Version | Note |
|---|---|---|
| OpenJDK | 21.0.10 LTS | class file major 65 |
| Apache Tomcat | 10.1.61 | Servlet 6.0, jakarta.* namespace |
| Apache Struts | 6.x | historical S2 analysis references 2.3/2.5 |
| Spring Boot | 3.x | Spring Security 6 — AuthorizationFilter, not FilterSecurityInterceptor |
| PostgreSQL | 17.11 | for the JDBC protocol notes |
Oracle donated Java EE to Eclipse but not the javax trademark, so every
package was renamed. Tomcat 9 and earlier use javax.servlet; Tomcat 10 and
later use jakarta.servlet. The break is binary and silent: a WAR compiled
against javax.servlet.HttpServlet deployed to Tomcat 10 does not throw a
visible error — the servlet is never registered and every request returns
404. Much of what you will triage is still on javax.