luap.in/java
Java & Java Web Security Internals Companion to the AppSec Handbook

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.

Convention

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.

LayerOwned byRepresentative failures
Network
NioEndpoint
Tomcat config AJP reachable (Ghostcat, CVE-2020-1938), shutdown port exposed, connector bound to 0.0.0.0, TLS downgrade
Trust boundary
CoyoteAdapter
— Nothing is owned here. This is where bytes become HttpServletRequest and everything past it is attacker-controlled
Container
StandardWrapperValve
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.

03Versions these notes describe

Behaviour differs between versions in ways that matter. Everything here was checked against:

ComponentVersionNote
OpenJDK21.0.10 LTSclass file major 65
Apache Tomcat10.1.61Servlet 6.0, jakarta.* namespace
Apache Struts6.xhistorical S2 analysis references 2.3/2.5
Spring Boot3.xSpring Security 6 — AuthorizationFilter, not FilterSecurityInterceptor
PostgreSQL17.11for the JDBC protocol notes
javax vs jakarta

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.

luap.in/java · source Java 21 · Tomcat 10.1.61 · Struts 6 · Spring Boot 3