OWASP Top 10 for Java APIs
Injection, broken access control, SSRF, insecure deserialisation, mass assignment, and the dependency with the CVE — each with the Spring-specific fix.
The OWASP Top 10 is a list of what actually gets exploited, rebuilt from real breach data every few years. It is not a checklist to tick — most entries are categories with many shapes — but it is the best available answer to "where does the damage come from", and for a Java API a handful of the entries account for nearly all of it.
This lesson is the list, with what each one looks like in Spring. Several of them have already had a whole lesson in this course; those get a sentence and a pointer rather than a repeat.
A01 Broken access control — the biggest one, by a distance
First on the list, and first in almost every penetration test report. It is the authenticated endpoint that never asks whether the object belongs to the caller:
@GetMapping("/orders/{id}")
public OrderView get(@PathVariable Long id) {
return toView(orders.findById(id).orElseThrow()); // whose order?
}Change 42 to 43 and you are reading somebody else's. No test that logs in and checks a 200 will notice.
The fix is shape, not a check: put the owner in the query — findByIdAndCustomerId — so there is no window where the object exists unchecked. The authentication and authorisation lesson takes this apart properly.
Its API-specific twin is mass assignment, and it deserves its own paragraph because it looks like a convenience:
@PostMapping("/users")
public User create(@RequestBody User user) { return users.save(user); }Jackson binds every field the client sends. If User has role, the client sends {"name":"ana","role":"ADMIN"} and Jackson sets it. This is the mirror image of the DTO lesson's leak — that one was the entity going out, this is the client coming in — and the fix is the same: a request DTO with only the fields a client may set.
A02 Cryptographic failures
Mostly: data that should have been encrypted and was not, and passwords stored with a fast hash. The password storage lesson covers the second in full.
The parts worth adding here:
- TLS everywhere, including inside your network. "It is only internal traffic" assumes the network is trusted, which is the assumption every lateral-movement attack depends on.
- Never write your own crypto, and that includes constructing your own token format or comparing secrets with
equals. SecureRandom, notRandom, for anything a person should not be able to predict — tokens, reset links, session ids.Randomis seeded predictably and its sequence can be reconstructed.
A03 Injection
Data reaching a parser. The injection lesson is this one in full, including the input that reframes it — O'Reilly breaking a concatenated query with a syntax error, proving the defect is a correctness bug that happens to be exploitable.
For Java specifically: JPQL and HQL are injectable exactly like SQL, @Query with string building gives up every protection, and Runtime.exec with a concatenated command is the same bug pointed at a shell.
A04 Insecure design
The entry people skip, because it is not a bug you can grep for. It is a system that works exactly as designed and the design is wrong: a password reset that emails a link with no expiry, a refund endpoint with no limit, a signup flow with no rate limit that becomes an email-sending service for somebody else.
The practical form of this in a backend: threat-model the feature, not the code. Ask what an attacker would do with this endpoint if they had a million requests and no scruples, before writing it.
A05 Security misconfiguration
The most common cause of an incident, even where it is not the most common category, because it fails silently:
- A CORS policy that did not apply. In Spring Security,
http.cors()looks up theCorsConfigurationSourcebean by name; the wrong name means the configuration is silently never used. The CORS lesson shows both runs. allowedOrigins("*")with credentials, or anallowedOriginPatternsbroad enough to accept anything.- Actuator exposed.
/actuator/envand/actuator/heapdumpare your configuration and your memory. Exposehealthandinfo; put the rest behind authentication or a separate port. - Stack traces to clients. The error-handling lesson's correlation id exists precisely so the detail goes to the log instead.
- Default credentials, verbose errors, directory listings, debug flags left on.
Spring Security does add sensible headers by default — X-Content-Type-Options: nosniff, X-Frame-Options: DENY, no-store cache headers — and the usual way people meet them is by turning one off to fix something. Change the one you need.
A06 Vulnerable and outdated components
Here is this project's own audit, right now:
mysql2 <=3.23.0
Severity: high
MySQL2: Auth Plugin Downgrade to mysql_clear_password Leaks Plaintext Credentials
MySQL2: Unbounded zlib inflate in compressed MySQL protocol handler allows DoS
Depends on vulnerable versions of @prisma/config
Depends on vulnerable versions of mysql2
4 high severity vulnerabilitiesFour high-severity advisories, and they are accepted rather than fixed. The reasoning is the part worth learning:
mysql2 arrives as a transitive dependency of the Prisma CLI — a development dependency. This project uses PostgreSQL. The vulnerable code is never loaded, never reachable, and not shipped to production.
A CVE in your dependency tree is not automatically a vulnerability in your application. What matters is reachability: is the vulnerable code path reachable with attacker-controlled input in your deployment.
And the honest other half: you have to establish that, not assume it. An unexamined advisory and an accepted one look identical in a report and are completely different things. Write down which it is.
Tooling: mvn dependency-check or Snyk for Java, npm audit for the JavaScript side, Dependabot or Renovate to keep versions moving. Keeping current is cheaper than the emergency upgrade that a live exploit forces — which is the real lesson of Log4Shell, where the vulnerability was severe and the upgrade was what teams could not do quickly.
A07 Identification and authentication failures
Credential stuffing, weak session handling, and the login that tells an attacker which emails exist by answering differently for "no such user" and "wrong password". Sessions and tokens covers the mechanics; password storage covers the rest.
The one to add: rate limit authentication endpoints per account and per IP. Password hashing is slow by design, so an unlimited login endpoint is also a cheap denial of service against your own CPU.
A08 Software and data integrity failures
The category that includes insecure deserialisation, which is the most severe thing on this page.
Java's native serialisation reconstructs arbitrary object graphs, and a crafted stream can execute code during that reconstruction. The rule is simple and absolute: never deserialise untrusted input with ObjectInputStream. Use JSON with a schema and bind to a DTO whose fields you chose.
Jackson has its own version of this: polymorphic deserialisation with default typing enabled lets the payload name the class to instantiate. Leave enableDefaultTyping off, and if you need polymorphism, use @JsonSubTypes with an explicit allow-list.
Also here: your build pipeline. A dependency from an unverified source, or a CI that can be made to publish, is a supply chain problem — and it is the one gap this course still has no lesson for.
A09 Logging and monitoring failures
Not detecting an incident is what turns a breach into a long breach. Log authentication failures, authorisation denials and administrative actions — and make sure somebody looks.
The counterweight, which is a real incident shape of its own: never log a password, a token, a card number, or a whole request body. A log is read by more people than your database, retained longer, and shipped to third-party tools.
A10 Server-side request forgery
Your service fetches a URL the user supplied. The attacker supplies http://169.254.169.254/latest/meta-data/ — the cloud metadata endpoint — and your service, which is inside the network and holds a role, fetches it and hands back the result.
Anywhere a user-supplied URL is fetched: webhooks, "import from URL", avatar fetching, link previews, PDF renderers that follow images.
The defences, in order:
- Allow-list the hosts you will fetch. Not a block-list; a block-list loses to DNS rebinding and IPv6 notation.
- Resolve the hostname yourself and check the address before connecting — reject private ranges, loopback and link-local.
- Do not follow redirects, or re-check after each one.
- Use a separate egress path with no access to internal services or metadata.