OWASP Top 10 2026: The Vulnerabilities Still Breaking Production APIs
I audit production APIs every few months. These vulnerabilities were documented years ago. They still show up — in funded startups, in enterprise codebases, in systems that passed a "security review." Here's the no-fluff breakdown: what they are, what they look like in real code, and exactly how to fix them.
API1 · CRITICAL Broken Object Level Authorization (BOLA)
Top of the list every year. The fix is four lines of code. I still find it in every audit.
User A changes an order ID in the URL from 1042 to 1043 and reads user B's data. Authentication passed — they're logged in. Ownership was never checked. That's BOLA.
Java · Spring Boot · fix — ownership baked into the query
// BAD — fetches any order, checks ownership after
Order order = orderRepo.findById(orderId).orElseThrow();
if (!order.getUserId().equals(currentUser.getId())) throw new ForbiddenException();
// GOOD — if it doesn't belong to this user, it simply doesn't exist for them
@Query("SELECT o FROM Order o WHERE o.id = :id AND o.userId = :userId")
Optional<Order> findByIdAndUserId(Long id, Long userId);
Never query by ID alone. Always include the authenticated user's identity. Ownership is a database concern — not something you verify in the controller after the fact.
API2 · CRITICAL Broken Authentication
Not weak passwords. In 2026 this means: JWTs with 24-hour expiry, alg: none accepted by the library, or a signing secret that's literally secret copied from a three-year-old tutorial that nobody updated.
The API accepted alg: none in the JWT header — meaning anyone could craft a token for any user with zero signature. The library allowed it because nobody hardcoded the expected algorithm server-side. It had been live for 11 months.
Java · enforce RS256, validate iss + aud — never trust the token's own alg claim
// WRONG — token tells you which algorithm to trust
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
// RIGHT — hardcode RS256, check issuer and audience every time
Jwts.parserBuilder()
.setSigningKey(rsaPublicKey)
.requireIssuer("https://auth.yourdomain.com")
.requireAudience("https://api.yourdomain.com")
.build()
.parseClaimsJws(token);
Decode and inspect JWT payloads without installing anything — LearnHubly Base64 Decoder. For the full JWT vs cookies decision: JWT vs Session Cookies for Microservices.
API3 · HIGH Broken Object Property Level Auth (BOPLA)
Two patterns, same cause. Mass assignment: user sends {"name":"Alice","isAdmin":true}, controller binds it directly to the entity. Excessive exposure: API dumps the full user object — including passwordHash and stripeCustomerId — because the frontend only shows the name field.
Java · never bind request bodies directly to JPA entities
// BAD — attacker sends {"name":"Alice","role":"ADMIN","credit":9999}
@PutMapping("/users/{id}")
public User update(@RequestBody User user) { return repo.save(user); }
// GOOD — explicit DTO with only what users can change
record UpdateUserRequest(@NotBlank @Size(max=100) String name, @Email String email) {}
@PutMapping("/users/{id}")
public UserResponse update(@PathVariable Long id, @RequestBody @Valid UpdateUserRequest req) {
User u = repo.findById(id).orElseThrow();
u.setName(req.name());
u.setEmail(req.email());
return mapper.toResponse(repo.save(u));
}
API4 · HIGH Unrestricted Resource Consumption
No rate limiting is an open buffet. I watched a misconfigured webhook integration — retrying on failure with no backoff — take down a 50,000 req/day service in under 20 minutes. Two layers, always: application level and gateway level.
Java · Resilience4j rate limiter — return 429, not 500
# application.yml
resilience4j.ratelimiter.instances.api:
limitForPeriod: 100
limitRefreshPeriod: 1m
timeoutDuration: 0s
@RateLimiter(name = "api", fallbackMethod = "tooManyRequests")
public List<Order> getOrders() { ... }
Test your rate limiting and inspect Retry-After headers live — LearnHubly REST API Tester. No installation needed.
API5 · HIGH Broken Function Level Authorization
The admin endpoint any authenticated user can reach if they know the URL. Attackers are not creative. If your user API is at /api/v1/users, they try /api/v1/admin/users in the first five minutes.
Java · @PreAuthorize + deny-all default — never rely on URL patterns alone
@PreAuthorize("hasRole('ADMIN')")
public List<UserResponse> getAllUsers() { ... }
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
API6 · CRITICAL Server-Side Request Forgery (SSRF)
Your API fetches a URL the user supplied. The user supplies http://169.254.169.254/latest/meta-data/iam/security-credentials/. On AWS that returns your instance's temporary credentials. Game over.
Java · block private IPs + allowlist before any outbound fetch
public void validateUrl(String raw) throws Exception {
URL url = new URL(raw);
InetAddress addr = InetAddress.getByName(url.getHost());
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress())
throw new SecurityException("SSRF blocked: internal IP");
}
API7 · HIGH Security Misconfiguration
CORS left wide open. Stack traces in error responses. /actuator/env publicly reachable. I find at least one of these in every system I touch.
Check your live API headers — LearnHubly REST API Tester. Look for Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options.
API8 · CRITICAL Injection
Still here. Still in codebases I review in 2026. Teams concatenate user input into SQL and call it "internal only" — as if attackers respect access controls.
Java · parameterised queries — no exceptions, ever
@Query("SELECT u FROM User u WHERE u.name = :name")
Optional<User> findByName(@Param("name") String name);
API9 · MEDIUM Improper Inventory Management
Shadow APIs — endpoints that exist in production but are undocumented, unmonitored, and usually unprotected. Assignment ownership. Set sunset dates. Enforce them.
API10 · MEDIUM Unsafe Consumption of Third-Party APIs
Your API trusts external responses like it should never trust user input. Validate. Set timeouts. Circuit break.
Conclusion
Let me put some numbers on this. The average cost of a data breach in the US in 2025 was $4.88 million. The average time to detect and contain it: 258 days. That is eight months of an attacker sitting inside your systems — reading data, escalating privileges, and moving laterally — while your monitoring dashboards show nothing unusual.
The ten vulnerabilities on this list are responsible for the majority of those breaches. Not zero-days. Not nation-state exploits requiring custom tooling. BOLA. Broken auth. SQL injection. Misconfigured CORS. Things that are preventable with patterns every Spring Boot developer already has access to.
I have audited systems where a BOLA vulnerability exposed 2.3 million user records. The endpoint had been live for 14 months. It was flagged in an internal review 9 months earlier and deprioritised because "we have bigger things to ship." The breach notification letter cost more than six months of engineering time would have.
Every week a known vulnerability sits unfixed is a week where a breach is a matter of when, not if. Security debt compounds faster than technical debt — and the interest payment is paid in user trust, regulatory fines, and engineering hours spent on incident response instead of product.
Here is what I have seen work in teams that take this seriously:
- Security is part of the definition of done. Not a separate ticket. Not a quarterly audit. A checkbox on every PR — does this endpoint validate object ownership? Does this new query use parameterised statements?
- The checklist above lives in your repo. Not on a wiki nobody reads. In a
SECURITY.mdthat gets referenced in every PR template. - One person owns API inventory. Shadow APIs exist because nobody is responsible for tracking them. Assign ownership. Set sunset dates. Enforce them.
- Short-lived tokens are non-negotiable. I have never seen a team regret switching to 15-minute access tokens. I have seen teams deeply regret not doing it after a breach.
The OWASP Top 10 does not change dramatically year over year — and that is the indictment. These are known, documented, solvable problems. The code to fix all ten of them fits in this article. The only variable is whether your team treats security as a first-class engineering concern or as something to revisit "after the launch."
Make the call now. Run the checklist. Fix what you find. Ship it.
— Priya Singh
Test your API right now — no setup needed
Inspect security headers, test rate limits, decode JWT payloads — all in the browser.