How do I secure a Spring Boot app?

Direct answer

Lock down Actuator endpoints, keep the H2 console out of production, and use method-level authorisation rather than relying on URL patterns. Spring Security is capable and its defaults are reasonable, most problems come from configuration that was loosened during development and never tightened.

Muhammad HasanUpdated

Actuator

Actuator exposes operational endpoints, and several are sensitive. /env lists environment variables including credentials, /heapdump returns a memory dump containing whatever was in it, /mappings describes every route.

Recent versions expose only /health and /info over HTTP by default, but configuration accumulates:

management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: when-authorized
  server:
    port: 9090          # separate port, not publicly routed

Never include: "*". Put Actuator on a separate port that isn't exposed externally, and require authentication for anything beyond health.

The H2 console

spring.h2.console.enabled: false

A web-based database console. Occasionally enabled for local development and shipped, and it has been the entry point in real incidents. Confirm it's disabled in every production profile.

Method-level authorisation

URL-pattern rules are brittle. A new endpoint that doesn't match an existing pattern is unprotected, silently.

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { }

@PreAuthorize("#order.userId == authentication.principal.id")
public Order getOrder(Order order) { }

The second form is the important one. @PreAuthorize("isAuthenticated()") confirms a login and says nothing about ownership. That gap accounted for more serious findings than anything else in the 24-app study.

Better still, scope the query itself:

orderRepository.findByIdAndUserId(id, currentUser.getId())
    .orElseThrow(() -> new NotFoundException());

Be careful with SpEL in @PreAuthorize, expressions built from user input are an injection surface of their own.

Deserialisation

Java's history here is long. Avoid ObjectInputStream on anything untrusted.

For Jackson, don't enable polymorphic type handling (enableDefaultTyping, or @JsonTypeInfo with a permissive base) on untrusted input. That's the mechanism behind a long series of gadget-chain vulnerabilities. Where polymorphism is needed, use an explicit allowlist of permitted subtypes.

Queries

Spring Data derived queries and JPQL parameterise. String concatenation into @Query or EntityManager.createQuery doesn't:

// Vulnerable
em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'");

// Safe
em.createQuery("SELECT u FROM User u WHERE u.email = :email")
  .setParameter("email", email);

nativeQuery = true follows the same rules as raw SQL anywhere.

CSRF

On by default for session-based authentication. csrf().disable() appears in a great many tutorials and gets copied.

If you authenticate with bearer tokens rather than cookies, disabling it is correct, the browser attaches nothing automatically, so there's nothing to forge. If you use session cookies, leave it on.

Dependencies

The Spring ecosystem has had several high-profile CVEs and the transitive tree is deep. spring-boot-starter-parent gives you managed versions; keep it current, and run OWASP Dependency-Check or equivalent in CI.

Go deeper

Kolega for AppSec teams

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free