The OWASP Top 10 covers the vulnerabilities that actually get Java applications compromised. Here's how each one manifests in Spring Boot and how to fix it properly.
JOptimize Team
Security vulnerabilities in Java applications rarely come from exotic exploits. They come from predictable mistakes: SQL built by concatenating user input, secrets stored in source code, API endpoints that trust the user's claimed identity, and dependencies with known CVEs that nobody updated. The OWASP Top 10 catalogs the most common vulnerabilities precisely because they keep appearing in production systems.
This guide maps each OWASP Top 10 category to concrete Spring Boot patterns and shows how to fix them.
Injection attacks work when user input is used to construct queries or commands without sanitization. SQL injection is the classic example:
// VULNERABLE: string concatenation in JPQL @Query("SELECT u FROM User u WHERE u.email = '" + email + "'") // An attacker passes: ' OR '1'='1 → returns all users // VULNERABLE: native SQL with concatenation jdbcTemplate.query("SELECT * FROM users WHERE email = '" + email + "'", ...);
The fix is parameterized queries — always:
// SAFE: Spring Data method name query — parameterized automatically Optional<User> findByEmail(String email); // SAFE: @Query with named parameters — never concatenate @Query("SELECT u FROM User u WHERE u.email = :email") Optional<User> findByEmail(@Param("email") String email); // SAFE: JdbcTemplate with parameters jdbcTemplate.query("SELECT * FROM users WHERE email = ?", new Object[]{email}, userRowMapper); // SAFE: Criteria API — no string SQL at all CriteriaQuery<User> query = cb.createQuery(User.class); Root<User> root = query.from(User.class); query.where(cb.equal(root.get("email"), email));
JPA's parameterized queries are immune to SQL injection because the parameter values are never interpreted as SQL syntax.
Broken authentication includes weak passwords, no rate limiting on login, missing MFA, and insecure token handling:
// SECURE: BCrypt with appropriate cost factor @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); // Cost 12 — ~300ms per hash, brute-force resistant } // SECURE: Account lockout after failed attempts @Service public class LoginAttemptService { private final Cache<String, Integer> attempts = Caffeine.newBuilder() .expireAfterWrite(15, TimeUnit.MINUTES) .build(); public void recordFailedAttempt(String username) { int count = attempts.getIfPresent(username) != null ? attempts.getIfPresent(username) : 0; attempts.put(username, count + 1); } public boolean isBlocked(String username) { Integer count = attempts.getIfPresent(username); return count != null && count >= 5; // Block after 5 failures } }
Insecure Direct Object References (IDOR) occur when users can access other users' data by changing an ID:
// VULNERABLE: any authenticated user can get any order @GetMapping("/orders/{orderId}") public OrderDto getOrder(@PathVariable Long orderId) { return orderService.findById(orderId); // No ownership check! } // Attacker changes orderId from 100 to 101 and reads someone else's order // SECURE: always check ownership @GetMapping("/orders/{orderId}") public OrderDto getOrder(@PathVariable Long orderId, @AuthenticationPrincipal UserDetails user) { Order order = orderService.findById(orderId); if (!order.getCustomerId().equals(user.getUserId())) { throw new AccessDeniedException("Access denied"); } return OrderMapper.toDto(order); } // BETTER: make the query do the ownership check @Query("SELECT o FROM Order o WHERE o.id = :id AND o.customerId = :userId") Optional<Order> findByIdAndCustomerId(@Param("id") Long id, @Param("userId") Long userId); // BEST: method security annotation for role-based access @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)") public OrderDto getOrder(Long orderId) { ... }
Default configurations expose sensitive information:
# application.yml — secure defaults spring: mvc: problemdetails: enabled: true # Consistent error format, no stack traces management: endpoints: web: exposure: # NEVER expose all actuator endpoints publicly include: health,info # health and info behind auth in production: endpoint: health: show-details: when-authorized server: error: include-stacktrace: never # No stack traces in responses include-message: never # No exception messages include-exception: false servlet: session: cookie: http-only: true secure: true same-site: strict
// Secure security headers @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.headers(headers -> headers .contentTypeOptions(withDefaults()) // X-Content-Type-Options .frameOptions(frame -> frame.deny()) // X-Frame-Options: DENY .xssProtection(withDefaults()) // X-XSS-Protection .contentSecurityPolicy(csp -> csp.policyDirectives( "default-src 'self'; script-src 'self'; frame-ancestors 'none'" )) .httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000).includeSubDomains(true)) ); return http.build(); }
Dependencies with known CVEs are one of the most common attack vectors:
<!-- pom.xml: OWASP Dependency Check plugin --> <plugin> <groupId>org.owasp</groupId> <artifactId>dependency-check-maven</artifactId> <version>10.0.3</version> <configuration> <failBuildOnCVSS>7</failBuildOnCVSS> <!-- Fail CI on CVSS >= 7 --> <suppressionFile>suppression.xml</suppressionFile> </configuration> <executions> <execution> <goals><goal>check</goal></goals> </execution> </executions> </plugin>
# .github/dependabot.yml — automatic dependency updates version: 2 updates: - package-ecosystem: maven directory: / schedule: interval: weekly open-pull-requests-limit: 10
Using weak algorithms or storing sensitive data incorrectly:
// VULNERABLE: MD5 for passwords String hash = DigestUtils.md5Hex(password); // Crackable in seconds // VULNERABLE: storing credit cards in plain text user.setCreditCard(cardNumber); // Never store raw PAN // SECURE: BCrypt for passwords string hash = passwordEncoder.encode(password); // SECURE: encrypt sensitive fields at rest @Convert(converter = EncryptedStringConverter.class) private String taxId; // Encrypted in database, decrypted in app // SECURE: use strong random for tokens String token = Base64.getUrlEncoder().encodeToString( SecureRandom.getInstanceStrong().generateSeed(32) // 256 bits );
SSRF occurs when user-controlled URLs are fetched server-side, allowing attackers to reach internal services:
// VULNERABLE: user controls the URL public String fetchPreview(String url) { return restTemplate.getForObject(url, String.class); // Attacker sends: http://169.254.169.254/latest/meta-data/ (AWS metadata!) } // SECURE: whitelist allowed domains public String fetchPreview(String url) { URI uri = URI.create(url); List<String> allowed = List.of("api.myapp.com", "cdn.myapp.com"); if (!allowed.contains(uri.getHost())) { throw new IllegalArgumentException("URL not in allowed list: " + uri.getHost()); } return restTemplate.getForObject(url, String.class); }
Secrets in source code get committed and leaked:
// VULNERABLE private static final String API_KEY = "sk-prod-abc123xyz..."; // In source code! // SECURE: environment variables @Value("${external.api.key}") // From env var EXTERNAL_API_KEY private String apiKey;
# application.yml — no secrets, only references spring: datasource: password: ${DB_PASSWORD} # Set in environment, not here jwt: secret: ${JWT_SECRET} # Set in environment, never committed
Use git-secrets or truffleHog in your CI pipeline to scan for accidentally committed secrets.
/actuator/env shows all configuration including passwords; restrict exposure in production@PreAuthorize without enabling method security — @EnableMethodSecurity is required; without it, the annotation is silently ignoredThe OWASP Top 10 in Spring Boot: use parameterized queries (never concatenate SQL), BCrypt with cost 12 for passwords, ownership checks on every resource access, secure HTTP headers, dependency scanning in CI, encrypted sensitive fields, SSRF allowlists, and secrets in environment variables. Most vulnerabilities are preventable with consistent application of these patterns.
JOptimize includes 80+ rules covering the OWASP Top 10 — flagging hardcoded secrets, SQL injection patterns, and missing authorization checks directly in IntelliJ.
Security isn't a feature. It's a baseline.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.