in

OWASP Top 10 and Spring Boot: A Practical Security Checklist

The OWASP Top 10 lists the most commonly exploited weaknesses in enterprise applications; the 2025 edition added supply chain and error handling. For every item, the concrete countermeasure in Spring Boot — object-level authorization, secure configuration and headers, dependency scanning, parameterized queries, password and secret management, safe logging, fail-closed behavior — with code examples and a CI checklist.

Checklist mapping the OWASP Top 10 items to Spring Boot security countermeasures

The OWASP Top 10 is the list of the most commonly exploited weakness classes in web applications, and the shared language of enterprise security audits, penetration tests and customer security questionnaires. The 2025 edition brought two new entries: software supply chain failures and the mishandling of exceptional conditions. The list tells you "what goes wrong"; it doesn't answer "how do I prevent this in Spring Boot". This article fills that gap.

For every item we give the concrete countermeasure in Spring Boot, code examples and the gates to put into CI. The article is defensive: its purpose is to make your application ready for audits and attacks. Our privacy by design and Spring Security + Keycloak articles complement this list.

OWASP Top 10 (2025) and its Spring Boot counterparts

#

Weakness class

Main countermeasure in Spring Boot

A01

Broken access control

Deny by default; method security; object-level ownership/tenant checks (IDOR)

A02

Security misconfiguration

Actuator and error details closed; security headers; narrow CORS; profile-based hardening

A03

Software supply chain failures

Dependency and image scanning, SBOM, internal registry, artifact signing, a known-vulnerability gate in CI

A04

Cryptographic failures

TLS everywhere; passwords with Argon2/bcrypt; secrets in secret management; field-level encryption

A05

Injection

Parameterized queries (JPA, JdbcClient); binding in native SQL; input constraints against command and SpEL injection; output encoding

A06

Insecure design

Threat modeling; abuse scenarios for business flows; rate limiting; validation at the boundary

A07

Authentication failures

Central IdP (OIDC), MFA, short-lived tokens, session management, brute-force protection

A08

Software or data integrity failures

Safe deserialization (ObjectInputFilter, Jackson polymorphic typing off), signed artifacts and pipeline

A09

Security logging and alerting failures

Audit events; personal data and secrets masked; central logs and alert rules

A10

Mishandling of exceptional conditions

Fail closed; a global error handler; timeouts and resource limits; consistency on partial failure

A01 — Access control: deny by default, check the object

The most common finding is an endpoint with a role check but no object check: GET /api/invoices/4521 — the user has the "accounting" role, but 4521 is another tenant's invoice (IDOR). The role opens the permission; the ownership and tenant check closes the door.

http.authorizeHttpRequests(a -> a
    .requestMatchers("/actuator/health/**").permitAll()
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/**").authenticated()
    .anyRequest().denyAll());                                   // every path not on the list is closed

@Service
public class InvoiceService {
    @PreAuthorize("hasRole('ACCOUNTING')")
    @PostAuthorize("returnObject.tenantId == authentication.token.claims['tenant_id']")   // object level: another tenant's data is never returned
    public InvoiceDto get(long id) { return repo.dto(id).orElseThrow(InvoiceNotFound::new); }
}

In addition: the tenant filter enforced in the database too (see RLS); authorization tests on every release (user A gets 403 on B's record); admin endpoints on a separate network; CORS for known origins only.

A02 — Configuration: know what is open in production

management:
  endpoints:
    web:
      exposure:
        include: health, prometheus                 # env, beans, heapdump, threaddump are never exposed
  endpoint:
    health:
      show-details: when-authorized
server:
  error:
    include-stacktrace: never                       # stack traces never reach the client
    include-message: never
spring:
  jackson:
    deserialization:
      fail-on-unknown-properties: true             # unexpected fields aren't silently accepted
  h2:
    console:
      enabled: false
http.headers(h -> h
    .contentSecurityPolicy(c -> c.policyDirectives("default-src 'self'; frame-ancestors 'none'; object-src 'none'"))
    .httpStrictTransportSecurity(s -> s.includeSubDomains(true).maxAgeInSeconds(31536000))
    .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
    .referrerPolicy(r -> r.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)));

The rule: the production profile starts from hardened defaults; development conveniences (the H2 console, detailed errors, wide CORS) are enabled only in the dev profile, and CI tests that these settings are off in the production profile.

A03 — Supply chain: your dependencies are your code too

  • Known-vulnerability scanning mandatory in CI: OWASP Dependency-Check or Trivy/Grype; a vulnerability above the CVSS threshold (e.g. 7+) breaks the build; image scanning does the same for containers.

  • Internal registry and pinning: Dependencies from an internal Maven proxy; versions locked; no -SNAPSHOT or version ranges in production. Adding a dependency is a review step (source, maintenance status, author).

  • SBOM: The CycloneDX Maven plugin produces an SBOM on every release; customer security questionnaires and the "do we use this library" question during an incident are answered in seconds.

  • Artifact integrity: CI produces signed artifacts; deployment accepts only signed images; the CI runners themselves are up to date and narrowly permissioned.

  • The non-existent package trap: For package names invented by AI assistants, the internal registry and review are the only protection (see AI coding assistants).

A04 — Cryptography: the right primitive in the right place

@Bean
PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();   // bcrypt by default; switch to Argon2id with {argon2}, old hashes keep verifying
}

Passwords only with slow, salted hashes (bcrypt/Argon2id) — SHA-256 is not suitable for passwords; ideally the password never reaches the application at all (central IdP). TLS 1.2+ everywhere in transit, including between internal services. Secrets in secret management (Vault, cloud KMS, Kubernetes Secrets + External Secrets), not in code or images. Application-layer encryption for personal and financial fields (AES-GCM, KMS-managed keys, key rotation); don't write your own crypto, use the JDK and libraries such as Tink. SecureRandom for randomness; at least 128 bits of entropy for tokens and codes.

A05 — Injection: data is never code

// WRONG: string concatenation
jdbc.sql("SELECT * FROM customer WHERE name = '" + name + "'");

// RIGHT: bind parameters (JPA, JdbcClient, native queries alike)
jdbc.sql("SELECT id, name FROM customer WHERE name = :name AND tenant_id = :tenant").param("name", name).param("tenant", tenant).query(CustomerDto.class).list();

// Parts that can't be parameterized, such as a sort column: an allow-list
Set<String> sortable = Set.of("name", "date", "amount");
if (!sortable.contains(sort)) throw new InvalidRequest("sort");

Other injection surfaces: operating-system commands (user input never reaches ProcessBuilder), LDAP filters, building SpEL expressions from user input (never), external entities in XML parsers (XXE: disallow-doctype-decl on DocumentBuilderFactory), raw output in template engines (Thymeleaf th:utext only for trusted content), log injection (line-break characters stripped). Length and format constraints at the boundary with Bean Validation don't prevent injection but shrink the attack surface.

A06 — Insecure design: the code is right, the flow is wrong

Business flows that can be abused with correctly written code: unlimited "forgot my password" attempts, trying a coupon code thousands of times, missing quantity validation in the refund flow, the price coming from the client. The countermeasure is at design time: the "how would I abuse this" question for every feature (lightweight threat modeling), rate limits and per-user/IP quotas on critical endpoints (Bucket4j or the gateway layer), business rules validated on the server and in the domain model (price, quantity, state transitions), second-channel confirmation for critical operations.

A07 — Authentication

Don't write login yourself: a central IdP, OIDC, MFA, short-lived access tokens, rotating refresh tokens, brute-force and credential-stuffing protection, session-fixation protection (a Spring Security default), session and token revocation on logout. Details in the Keycloak article.

A08 — Integrity: what are you deserializing?

// If Java serialization is in use (don't; but it lingers in old systems), a filter is mandatory
ObjectInputFilter.Config.setSerialFilter(ObjectInputFilter.Config.createFilter("com.acme.model.*;java.base/*;!*"));

// Jackson: polymorphic type information stays off
// ObjectMapper.enableDefaultTyping() / @JsonTypeInfo(use = Id.CLASS) are not used

Also: artifact and image signing (in CI), protecting the pipeline itself (branch protection, mandatory review, narrowly scoped CI secrets), loading third-party scripts with SRI, signature verification in software-update mechanisms.

A09 — Logging and alerting: being able to see the incident

@Component
public class AuditLogger {
    @EventListener
    public void loginFailed(AbstractAuthenticationFailureEvent e) {
        log.warn("audit=login_failed user={} ip={}", Mask.user(e.getAuthentication().getName()), requestContext.ip());
    }
    @EventListener
    public void accessDenied(AuthorizationDeniedEvent<?> e) {
        log.warn("audit=access_denied user={} resource={}", e.getAuthentication().get().getName(), e.getAuthorizationResult());
    }
}

What to log: logins and failed logins, authorization denials, critical data changes (who, what, when), admin operations, integration failures. What not to log: passwords, tokens, card numbers, personal data in full (masking). Logs are central and immutable; alert rules: N failed logins per minute from one IP, a sudden rise in authorization denials, admin operations outside working hours. Without logs an incident closes without a "what happened"; without alerts, logs are only material for archaeology.

A10 — Exceptional conditions: what happens at the moment of failure?

@RestControllerAdvice
public class GlobalErrorHandler {
    @ExceptionHandler(Exception.class)
    ProblemDetail unexpected(Exception e, HttpServletRequest req) {
        String incidentId = UUID.randomUUID().toString();
        log.error("incidentId={} path={}", incidentId, req.getRequestURI(), e);   // details in the log
        var p = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
        p.setDetail("Unexpected error. Support code: " + incidentId);           // only a correlation code to the client
        return p;
    }
}

Principles: fail closed — if the authorization service doesn't respond, access is denied, not granted; error messages don't leak internals (stack traces, SQL, file paths); timeouts and circuit breakers on every external call (see Resilience4j); request body, file size and page size are limited; consistency on partial failure (transactions, outbox) and idempotent retries. The "what happens on failure" question is part of the design review for every endpoint.

The CI checklist

  • Secret scanning (gitleaks/trufflehog) on every commit; a finding breaks the build.

  • SAST (SpotBugs + Find Security Bugs, Semgrep or a commercial tool) on every PR; high findings block the merge.

  • Dependency and image scanning; a CVSS threshold; SBOM generation.

  • A security-configuration test: an automated test against the application started with the production profile for actuator, error details, CORS and headers.

  • Authorization tests: for every resource, an "another user/tenant gets 403" test.

  • DAST (OWASP ZAP) scheduled on staging; penetration testing at least yearly and on major releases.

  • Time-to-patch for security fixes (such as 7 days for a critical CVE) as a tracked metric.

Common mistakes in the field

  1. Role but no ownership: IDOR, the number-one finding in penetration tests.

  2. Actuator's env and heapdump open: Secrets in a single request.

  3. Dependency scanning in "warn" mode: Nobody reads it; it must be a gate.

  4. SHA-256 for passwords: A leaked table is cracked within hours.

  5. String concatenation in sort/filter fields: The exception to "we use parameterized queries".

  6. Tokens and personal data in logs: The logging system becomes a second leak surface.

  7. Failing open: "Temporarily allow" when the authorization service is down.

Frequently asked questions

Is OWASP Top 10 compliance a certification?

No; it's an awareness and priority list. It is used as a reference in audits and customer questionnaires; for comprehensive verification, the OWASP ASVS (Application Security Verification Standard) levels are used. The controls in this article cover most of ASVS levels 1–2.

Are Spring Boot's defaults enough?

Spring Security's defaults are good (CSRF, session fixation, some headers); but decisions such as actuator exposure, error details, CORS and object-level authorization belong to the application. "We added Spring Security" is a beginning, not security.

Where should a small team start?

Three steps: a secret + dependency scanning gate in CI (one day), configuration hardening in the production profile (one day), an authorization test for every resource (ongoing). Then logging and alerting. These four steps close most penetration-test findings in advance.

Conclusion

The OWASP Top 10 is a to-do list, not a list of fears: authorization that denies by default and checks the object, a hardened production configuration, dependency scanning that works as a gate, the right cryptographic primitives, parameterized queries everywhere, design with abuse scenarios, central authentication, safe deserialization, masked logging with alerts, failing closed. Each is a few lines in Spring Boot or one CI step; their sum is an audit-ready application.

For a security assessment of your applications, setting up CI security gates or closing penetration-test findings, see our maintenance and support and enterprise application development services.