In the landscape of 2025, application security is no longer a final checkbox before deployment—it is the foundation of software architecture. With the rise of AI-driven cyberattacks and increasingly complex supply chain vulnerabilities, the OWASP Top 10 remains the definitive standard for developers to measure their security posture.
For Java developers, particularly those working with Java 21 and Spring Boot 3.3+, the implementation of these security controls has evolved. It’s not just about SQL injection anymore; it’s about broken access controls, cryptographic failures, and securing the software supply chain.
In this deep-dive guide, we will dismantle the critical risks facing Java applications today and implement robust, production-ready defense strategies.
Prerequisites and Environment #
To follow the code examples in this guide, ensure your development environment meets the following criteria:
- JDK: Java 21 LTS (or newer)
- Framework: Spring Boot 3.3+ (Spring Security 6)
- Build Tool: Maven or Gradle
- Database: PostgreSQL (for SQL examples)
Dependency Configuration #
We will be using standard Spring Security libraries along with validation and cryptography utilities. Add the following dependencies to your pom.xml:
<dependencies>
<!-- Core Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Input Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Rate Limiting (Bucket4j) for DoS protection -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.7.0</version>
</dependency>
<!-- OWASP Java HTML Sanitizer for XSS -->
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20240325.1</version>
</dependency>
</dependencies>1. Broken Access Control (A01): The Silent Killer #
Since 2021, Broken Access Control has held the number one spot, and in 2025, it remains the most common vulnerability. This occurs when users can act outside of their intended permissions. In Java, this often manifests as “Insecure Direct Object References” (IDOR) or missing method-level security.
The Threat Landscape #
Attackers simply modify a URL (e.g., changing /app/account?id=123 to ?id=124) to view another user’s data.
Prevention Strategy: Defense in Depth #
We need a layered approach:
- URL Security: Filter-based authorization.
- Method Security: Annotation-based checks.
- Domain Security: Checking ownership inside the business logic.
Implementation with Spring Security 6 #
First, let’s visualize the flow of a secure request.
The Code: Secure Configuration #
This configuration ensures that endpoints are locked down by default (“Deny by Default”).
package com.javadevpro.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // Enables @PreAuthorize
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Only disable if stateless (JWT)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
}The Code: Method-Level Protection (Handling IDOR) #
Do not rely solely on URL matching. Use Spring Expression Language (SpEL) to verify data ownership at the controller or service layer.
package com.javadevpro.security.service;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
// Only allow access if the user has the ADMIN role OR is the owner of the order
@PreAuthorize("hasRole('ADMIN') or @securityService.isOwner(authentication, #orderId)")
public OrderDto getOrder(Long orderId) {
// Business logic to fetch order
return orderRepository.findById(orderId)
.map(this::convertToDto)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
}
}Best Practice: Create a custom SecurityService bean to handle complex ownership logic rather than embedding it all in SpEL strings.
2. Cryptographic Failures (A02): Protecting Sensitive Data #
Formerly known as “Sensitive Data Exposure,” this category highlights failures in cryptography. In 2025, with quantum computing on the horizon, weak algorithms like MD5, SHA-1, or even standard RSA with short keys are unacceptable.
Algorithm Comparison Table #
| Algorithm | Use Case | Status in 2025 | Recommendation |
|---|---|---|---|
| MD5 / SHA-1 | Hashing | BROKEN | Do not use. Vulnerable to collisions. |
| BCrypt | Passwords | LEGACY | Acceptable for legacy, but Argon2 is preferred. |
| Argon2id | Passwords | RECOMMENDED | Memory-hard, resistant to GPU/ASIC cracking. |
| AES-GCM | Data Encryption | STANDARD | Use AES-256 in GCM mode (provides integrity). |
| RSA-2048 | Key Exchange | WEAKENING | Upgrade to RSA-4096 or Elliptic Curve (Ed25519). |
Implementation: Switching to Argon2 #
Spring Security makes it easy to switch password encoders. While BCrypt is the default, Argon2 is the winner of the Password Hashing Competition and provides better resistance against modern hardware attacks.
package com.javadevpro.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// saltLength, hashLength, parallelism, memory, iterations
int saltLength = 16;
int hashLength = 32;
int parallelism = 1;
int memory = 4096;
int iterations = 3;
return new Argon2PasswordEncoder(
saltLength,
hashLength,
parallelism,
memory,
iterations
);
}
}Common Pitfall: Storing secrets (API keys, DB passwords) in application.properties and committing them to Git.
Solution: Use Environment Variables, Spring Cloud Vault, or Docker Secrets.
3. Injection (A03): Beyond SQL #
While SQL Injection (SQLi) is well-known, Injection encompasses NoSQL injection, Command injection, and LDAP injection. In 2025, Object-Relational Mapping (ORM) frameworks like Hibernate handle most SQLi, but developers still introduce vulnerabilities through dynamic query building.
The Mistake: Dynamic JPQL #
// VULNERABLE CODE - DO NOT USE
public List<User> search(String username) {
// If username is "admin' OR '1'='1", this dumps the table
String query = "FROM User u WHERE u.username = '" + username + "'";
return entityManager.createQuery(query).getResultList();
}The Fix: Parameterization & Criteria API #
Always use parameterized queries. If you need dynamic filtering, use the JPA Criteria API or libraries like QueryDSL.
package com.javadevpro.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface UserRepository extends JpaRepository<User, Long> {
// Safe: Spring Data uses PreparedStatement under the hood
List<User> findByUsername(String username);
// Safe: Explicit JPQL with named parameters
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = 'ACTIVE'")
User findActiveUserByEmail(@Param("email") String email);
}4. Insecure Design (A04): Shift Left #
This is a cultural category rather than a coding one. It emphasizes that you cannot “code” your way out of a flawed design.
Scenario: An e-commerce site allows unlimited coupon attempts. Flaw: The code works, but the business logic allows brute-forcing discounts.
Design Pattern: Rate Limiting #
To prevent abuse (part of secure design), implement Rate Limiting. We use Bucket4j here.
package com.javadevpro.security.ratelimit;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RateLimitingService {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
public Bucket resolveBucket(String apiKey) {
return cache.computeIfAbsent(apiKey, this::newBucket);
}
private Bucket newBucket(String apiKey) {
// Allow 10 requests per minute
Bandwidth limit = Bandwidth.classic(10, Refill.greedy(10, Duration.ofMinutes(1)));
return Bucket.builder()
.addLimit(limit)
.build();
}
}Use an interceptor to check this bucket before processing requests. If the bucket is empty, return HTTP 429 (Too Many Requests).
5. Security Misconfiguration (A05) #
Modern Java applications are complex, involving Docker, Kubernetes, and extensive framework configurations. A single open port or default password can compromise the system.
The Spring Boot Actuator Risk #
Spring Boot Actuator is powerful for monitoring but dangerous if exposed. By default, it exposes detailed system metrics.
Vulnerable Config:
management.endpoints.web.exposure.include=*Secure Config (application.yml):
management:
endpoints:
web:
exposure:
include: "health,info,metrics" # Only expose what is needed
endpoint:
health:
show-details: "when_authorized" # Don't show DB details to anonymous usersAdditionally, ensure your SecurityFilterChain specifically secures the actuator endpoints:
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("OPS")6. Vulnerable and Outdated Components (A06) #
Supply chain attacks (like Log4Shell) have taught us that we are responsible for our dependencies. In 2025, using a Software Bill of Materials (SBOM) is standard practice.
Strategy: Automated Scanning #
Don’t rely on manual checks. Integrate the OWASP Dependency Check or CycloneDX into your Maven build.
Add this plugin to your pom.xml:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.9</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<failBuildOnCVSS>7.0</failBuildOnCVSS> <!-- Fail build on High severity -->
</configuration>
</plugin>Run mvn verify to generate a report and break the build if critical vulnerabilities are found.
7. Identification and Authentication Failures (A07) #
This category covers weak passwords, lack of Multi-Factor Authentication (MFA), and session fixation.
Best Practices for 2025: #
- Enforce Password Complexity: Don’t just require symbols; require length (12+ chars).
- Delay Failed Logins: Prevent timing attacks and brute force.
- Invalidate Session IDs: Rotate session IDs on login.
Code: Protecting Against Session Fixation #
Spring Security handles this by default, but verify your configuration explicitly:
http
.sessionManagement(session -> session
.sessionFixation().migrateSession() // Creates new session ID, keeps attributes
.maximumSessions(1) // Prevent concurrent logins
.expiredUrl("/login?expired")
);8. Software and Data Integrity Failures (A08) #
This relates to code and infrastructure that does not protect against integrity violations. An example is deserializing untrusted data, which can lead to Remote Code Execution (RCE).
Java Deserialization #
Java’s native serialization is notoriously insecure.
Rule: Avoid java.io.Serializable where possible. Prefer JSON (Jackson/Gson) formats.
If you use Jackson, ensure you do not enable “Polymorphic Type Handling” (enableDefaultTyping) globally unless strictly necessary and whitelisted, as this allows attackers to instantiate arbitrary classes.
// SAFER JACKSON CONFIG
ObjectMapper mapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// Avoid mapper.enableDefaultTyping(); <-- DANGEROUS
.build();9. Security Logging and Monitoring Failures (A09) #
Breaches typically take 200+ days to detect. Why? Because logs are either missing or flooded with noise. Conversely, logging too much (like passwords or credit cards) creates a data leak.
Code: Masking Sensitive Data with Logback #
Never log raw user objects. Use a custom converter or JSON logging with masking.
Here is a simple example using Logback pattern replacement in logback-spring.xml to mask credit card patterns:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'\b(?:\d[ -]*?){13,16}\b', '****-****-****-****'}%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>This RegEx replacement ensures that if a developer accidentally logs a credit card number, it gets masked in the console/file output.
10. Server-Side Request Forgery (SSRF) (A10) #
SSRF occurs when a web application is fetching a remote resource without validating the user-supplied URL. Attackers can abuse this to port scan your internal network or access cloud metadata services (like AWS Instance Metadata Service).
Vulnerable Code #
// DANGEROUS
public void fetchImage(String url) {
new URL(url).openStream(); // User can pass "http://localhost:8080/admin"
}Prevention Strategy #
- Allowlist: Only allow specific domains.
- Network Layer: Disable HTTP redirects.
- Validation: Ensure the IP is not a private/internal IP.
public InputStream safeFetch(String urlString) throws IOException {
URL url = new URL(urlString);
String host = url.getHost();
// 1. Domain Whitelist Check
if (!host.endsWith("trusted-image-provider.com")) {
throw new SecurityException("Untrusted domain");
}
// 2. Resolve IP and check for Internal Network
InetAddress address = InetAddress.getByName(host);
if (address.isSiteLocalAddress() || address.isLoopbackAddress() || address.isLinkLocalAddress()) {
throw new SecurityException("Access to internal network denied");
}
return url.openStream();
}Emerging Threats: AI and LLM Injection #
As we look toward the latter half of 2025, we must acknowledge LLM (Large Language Model) Injection. If your Java application integrates with OpenAI or local LLMs:
- Treat LLM Output as Untrusted: Apply the same sanitization (A03) to text generated by AI as you would to user input.
- Prompt Injection: Users may try to override your system prompts. Limit the context and permissions the LLM agent has.
Summary #
Securing a Java application is a continuous process. By addressing the OWASP Top 10, you eliminate the most statistical risks.
Key Takeaways:
- Shift Left: Integrate security plugins (SpotBugs, OWASP Dependency Check) into your build pipeline.
- Use Framework Features: Leverage Spring Security’s
SecurityFilterChainand Method Security rather than writing custom filters. - Validate Everything: Inputs, outputs, and dependencies.
- Stay Updated: A secure library today may be vulnerable tomorrow.
By implementing these strategies, you ensure your application remains robust, compliant, and trustworthy in the demanding environment of 2025.
Further Reading:
Found this article helpful? Share it with your team and subscribe to Java DevPro for more architectural deep dives.