OWASP Top 10 for Spring Boot: Real Vulnerabilities and How to Fix Them

The OWASP Top 10 lists the most critical web application security risks. Spring Boot apps have their own common failure patterns: exposed Actuator endpoints, secrets in properties files, SQL built from string concatenation, and Spring Security misconfiguration.

This guide covers the vulnerabilities that actually appear in Spring Boot applications and how to fix each one.


1. SQL Injection

SQL injection remains one of the most critical vulnerabilities. It allows attackers to manipulate database queries.

Vulnerable code

// DANGEROUS — concatenating user input into SQL
@Repository
public class OrderRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<Order> findByStatus(String status) {
        // If status = "PENDING' OR '1'='1", this returns ALL orders
        String sql = "SELECT * FROM orders WHERE status = '" + status + "'";
        return jdbcTemplate.query(sql, orderRowMapper);
    }
}

Fixed code

// Safe: parameterized query — the driver handles escaping
public List<Order> findByStatus(String status) {
    return jdbcTemplate.query(
        "SELECT * FROM orders WHERE status = ?",
        orderRowMapper,
        status  // parameter, not concatenated
    );
}

// Safe: Spring Data JPA with @Query (named parameters)
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<Order> findByStatus(@Param("status") OrderStatus status);

// Safe: Spring Data derived queries (no SQL string at all)
List<Order> findByStatus(OrderStatus status);

Rule: never concatenate user input into SQL. Use parameterized queries or JPA. The driver handles escaping.

JPQL injection (Spring Data)

JPQL is also injectable if you build query strings dynamically:

// VULNERABLE
@Query("SELECT o FROM Order o WHERE o." + fieldName + " = :value")

// Safe: use Criteria API or Specifications for dynamic queries
Specification<Order> spec = (root, query, cb) ->
    cb.equal(root.get(fieldName), value);

2. Broken Authentication

Problem: weak password hashing

// VULNERABLE — MD5 is broken, SHA-256 without salt is insufficient
String hashedPassword = DigestUtils.md5Hex(password);

// Safe: BCrypt with strength 12
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hashedPassword = encoder.encode(password);
boolean matches = encoder.matches(rawPassword, hashedPassword);

Spring Security auto-configures BCrypt when you add the starter. Always use the PasswordEncoder bean — don’t create your own hashing logic.

Problem: JWT vulnerabilities

// VULNERABLE — algorithm confusion attack: accepts HS256 when RS256 expected
Jwts.parser().parseClaimsJws(token);  // no algorithm restriction

// Safe: explicitly specify allowed algorithms
Jwts.parser()
    .verifyWith(signingKey)            // set the verification key
    .requireAlgorithm(Jwts.SIG.RS256) // only accept RS256
    .build()
    .parseSignedClaims(token);

Also validate JWT claims:

Jwts.parser()
    .verifyWith(signingKey)
    .requireAudience("order-service")          // validate audience
    .requireIssuer("https://auth.example.com") // validate issuer
    .clockSkewSeconds(30)                       // allow 30s clock skew
    .build()
    .parseSignedClaims(token);

3. Exposed Actuator Endpoints

Actuator endpoints expose application internals. In 2024, exposed /actuator/env was listed as a common vulnerability in enterprise Spring Boot applications — it reveals environment variables, database URLs, and API keys.

Vulnerable configuration

# DANGEROUS — exposes everything including secrets
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

Secure configuration

management:
  server:
    port: 8081                      # separate port — not exposed to the internet
  endpoints:
    web:
      exposure:
        include: health, prometheus  # only expose what's needed
  endpoint:
    health:
      show-details: never           # no connection details publicly
    env:
      keys-to-sanitize: "password,secret,key,token,credentials,vcap_services"
// If management port must be the same port, restrict with Spring Security
@Configuration
@Order(1)
public class ActuatorSecurityConfig {

    @Bean
    public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
                .requestMatchers(EndpointRequest.to(PrometheusEndpoint.class))
                    .hasIpAddress("10.0.0.0/8")  // monitoring server IP range
                .anyRequest().hasRole("ADMIN")
            )
            .build();
    }
}

4. Sensitive Data Exposure

Problem: secrets in properties files

# DANGEROUS — committed to version control
spring:
  datasource:
    password: myS3cr3tP@ssword
  security:
    oauth2:
      client:
        registration:
          google:
            client-secret: abc123xyz

Solution: Spring Cloud Vault

# application.yaml — no secrets here
spring:
  config:
    import: vault://

spring:
  cloud:
    vault:
      host: vault.example.com
      port: 8200
      authentication: KUBERNETES   # use K8s service account token
      kubernetes:
        role: order-service

Vault fetches the secrets at startup. They never appear in source code or environment variables visible via /actuator/env.

Solution: Kubernetes Secrets + Spring Boot

# application.yaml
spring:
  datasource:
    password: ${DB_PASSWORD}  # resolved from env var

# k8s deployment
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-credentials
        key: password

Mask sensitive fields in logs

// Mask sensitive request/response fields
@Component
public class LoggingFilter extends OncePerRequestFilter {

    private static final Set<String> SENSITIVE_HEADERS = Set.of(
        "Authorization", "X-Api-Key", "Cookie"
    );

    @Override
    protected void doFilterInternal(HttpServletRequest request, ...) {
        // Log headers but mask sensitive ones
        Collections.list(request.getHeaderNames()).forEach(name -> {
            String value = SENSITIVE_HEADERS.contains(name) ? "***" : request.getHeader(name);
            log.debug("Header {}: {}", name, value);
        });
        filterChain.doFilter(request, response);
    }
}

5. Broken Access Control

Problem: missing method-level authorization

// VULNERABLE — any authenticated user can access any user's orders
@GetMapping("/orders/{userId}")
public List<Order> getOrders(@PathVariable Long userId) {
    return orderService.findByUserId(userId);  // no ownership check
}

Fixed: verify ownership

@GetMapping("/orders/{userId}")
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public List<Order> getOrders(@PathVariable Long userId) {
    return orderService.findByUserId(userId);
}

Or enforce in the service:

@Service
public class OrderService {

    public List<Order> findByUserId(Long userId) {
        Long currentUserId = SecurityContextHolder.getContext()
            .getAuthentication().getPrincipal().getId();

        if (!currentUserId.equals(userId) && !isAdmin()) {
            throw new AccessDeniedException("Access denied to user " + userId + "'s orders");
        }

        return orderRepository.findByUserId(userId);
    }
}

Enable method security

@Configuration
@EnableMethodSecurity  // Spring Security 6+ (replaces @EnableGlobalMethodSecurity)
public class SecurityConfig {
    // @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter now work
}

Spring Security 7 changes (Spring Boot 4)

// Spring Security 6 — lambda DSL required
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/**").authenticated()
    .anyRequest().permitAll()
);

// Spring Security 7 removed authorizeRequests() (non-lambda)
// and introduced stricter defaults: CSRF enabled for APIs too

6. Security Misconfiguration

Problem: CORS misconfiguration

// VULNERABLE — allows any origin
@CrossOrigin(origins = "*")
@RestController
public class OrderController { }

// Safe: restrict to your frontend domain
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("Authorization", "Content-Type")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

Problem: disabled CSRF for APIs

Spring Security enables CSRF protection by default. It’s sometimes disabled entirely for REST APIs:

// VULNERABLE — completely disabled
http.csrf(csrf -> csrf.disable());

// Better: disable only for API endpoints (stateless JWT)
http.csrf(csrf -> csrf
    .ignoringRequestMatchers("/api/**")  // stateless JWT, no CSRF needed
    // Keep CSRF for web form endpoints
);

If your REST API uses JWT Bearer tokens (stateless), CSRF is not needed — CSRF attacks require cookies. If you’re using cookie-based sessions, keep CSRF enabled.

Security headers

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .headers(headers -> headers
                .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
                .xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
                .contentSecurityPolicy(csp ->
                    csp.policyDirectives("default-src 'self'; script-src 'self'; style-src 'self'"))
                .referrerPolicy(referrer ->
                    referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN))
                .permissionsPolicy(permissions ->
                    permissions.policy("camera=(), microphone=(), geolocation=()"))
            )
            .build();
    }
}

7. Vulnerable Dependencies

Spring Boot’s parent POM manages dependency versions, but you can override them:

# Check for vulnerable dependencies with OWASP dependency check
mvn org.owasp:dependency-check-maven:check

# Or with Trivy
trivy fs --security-checks vuln .

Add to CI pipeline:

# GitHub Actions
- name: Check for vulnerable dependencies
  run: ./mvnw org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7

Fails the build if any CVE with CVSS score ≥ 7 is found.


8. XSS (Cross-Site Scripting)

For REST APIs returning JSON, XSS is less common (browsers don’t execute JSON as HTML). But if you render HTML with Thymeleaf:

<!-- SAFE — Thymeleaf automatically escapes HTML -->
<p th:text="${userInput}">...</p>

<!-- VULNERABLE — th:utext renders raw HTML -->
<p th:utext="${userInput}">...</p>

For REST APIs, add Content Security Policy headers (see security headers above) and validate input:

// Sanitize HTML input if you must accept and display it
@Autowired
private HtmlUtils htmlUtils;

public String sanitizeInput(String input) {
    return HtmlUtils.htmlEscape(input);  // escapes < > & " '
}

9. Insecure Deserialization

Never deserialize untrusted data with Java serialization:

// VULNERABLE — Java ObjectInputStream can execute arbitrary code
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject();  // RCE if the stream is malicious

// Safe: use JSON with a typed class
@RequestBody OrderRequest request  // Spring MVC + Jackson — safe

For Jackson, disable polymorphic deserialization:

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    // Never enable: mapper.enableDefaultTyping() — unsafe polymorphic deserialization
    // Instead, use @JsonTypeInfo on specific classes where needed
    return mapper;
}

Security Checklist

Before every deployment

  • No secrets in application.properties or environment variables — use Vault/K8s Secrets
  • Actuator on separate port, only health and prometheus exposed publicly
  • CORS restricted to specific origins
  • SQL queries use parameterized values, not concatenation
  • Method security enabled, ownership checks on resource endpoints
  • Security headers configured (CSP, X-Frame-Options, Referrer-Policy)
  • OWASP dependency check passes in CI
  • Docker image scanned with Trivy
  • JWT validation includes algorithm, audience, and issuer checks
  • Sensitive fields masked in logs

Quick Reference

// Method security
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")

// Password hashing
new BCryptPasswordEncoder(12).encode(password);

// Parameterized SQL
jdbcTemplate.query("SELECT * FROM orders WHERE status = ?", mapper, status);

// Security headers
http.headers(h -> h.frameOptions(f -> f.deny())
                   .contentSecurityPolicy(c -> c.policyDirectives("default-src 'self'")));

// CORS
registry.addMapping("/api/**").allowedOrigins("https://app.example.com");

Summary

The most common Spring Boot security failures are exposed Actuator endpoints, secrets in properties files, missing method-level authorization, and SQL injection from string concatenation. Fix them: put Actuator on a separate management port and expose only what’s needed; use Vault or Kubernetes Secrets; add @PreAuthorize ownership checks; use parameterized queries or JPA. Add OWASP dependency checks and Trivy image scanning to CI. Enable method security and configure restrictive CORS before deploying to production.

Abhay

Abhay Pratap Singh

DevOps Engineer passionate about automation, cloud infrastructure, and self-hosted tools. I write about Kubernetes, Terraform, DNS, and everything in between.