| name | security-audit |
| description | Java security checklist covering OWASP Top 10, input validation, injection prevention, and secure coding. Works with Spring, Quarkus, Jakarta EE, and plain Java. Use when reviewing code security, before releases, or when user asks about vulnerabilities. |
Security Audit Skill
Security checklist for Java applications based on OWASP Top 10 and secure coding practices.
When to Use
- Security code review
- Before production releases
- User asks about "security", "vulnerability", "OWASP"
- Reviewing authentication/authorization code
- Checking for injection vulnerabilities
OWASP Top 10 Quick Reference
| # | Risk | Java Mitigation |
|---|
| A01 | Broken Access Control | Role-based checks, deny by default |
| A02 | Cryptographic Failures | Use strong algorithms, no hardcoded secrets |
| A03 | Injection | Parameterized queries, input validation |
| A04 | Insecure Design | Threat modeling, secure defaults |
| A05 | Security Misconfiguration | Disable debug, secure headers |
| A06 | Vulnerable Components | Dependency scanning, updates |
| A07 | Authentication Failures | Strong passwords, MFA, session management |
| A08 | Data Integrity Failures | Verify signatures, secure deserialization |
| A09 | Logging Failures | Log security events, no sensitive data |
| A10 | SSRF | Validate URLs, allowlist domains |
Input Validation (All Frameworks)
Bean Validation (JSR 380)
Works in Spring, Quarkus, Jakarta EE, and standalone.
public class CreateUserRequest {
@NotNull(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be 3-50 characters")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Username can only contain letters, numbers, underscore")
private String username;
@NotNull
@Email(message = "Invalid email format")
private String email;
@NotNull
@Size(min = 8, max = 100)
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$",
message = "Password must contain uppercase, lowercase, and number")
private String password;
@Min(value = 0, message = "Age cannot be negative")
@Max(value = 150, message = "Invalid age")
private Integer age;
}
public Response createUser(@Valid CreateUserRequest request) {
}
Custom Validators
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeHtmlValidator.class)
public @interface SafeHtml {
String message() default "Contains unsafe HTML";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SafeHtmlValidator implements ConstraintValidator<SafeHtml, String> {
private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
"<script|javascript:|on\\w+\\s*=", Pattern.CASE_INSENSITIVE
);
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
return !DANGEROUS_PATTERN.matcher(value).find();
}
}
Allowlist vs Blocklist
if (input.contains("<script>")) {
throw new ValidationException("Invalid input");
}
private static final Pattern SAFE_NAME = Pattern.compile("^[a-zA-Z\\s'-]{1,100}$");
if (!SAFE_NAME.matcher(input).matches()) {
throw new ValidationException("Invalid name format");
}
SQL Injection Prevention
JPA/Hibernate (All Frameworks)
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);
query.where(cb.equal(user.get("email"), email));
TypedQuery<User> query = entityManager.createQuery(
"SELECT u FROM User u WHERE u.status = :status", User.class);
query.setParameter("status", status);
String jpql = "SELECT u FROM User u WHERE u.email = '" + email + "'";
Native Queries
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
User findByEmailNative(String email);
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
JDBC (Plain Java)
String sql = "SELECT * FROM users WHERE email = ? AND status = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, email);
stmt.setString(2, status);
ResultSet rs = stmt.executeQuery();
}
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
Statement stmt = connection.createStatement();
stmt.executeQuery(sql);
XSS Prevention
Output Encoding
<p th:text="${userInput}">...</p>
<p th:utext="${trustedHtml}">...</p>
import org.owasp.encoder.Encode;
String safe = Encode.forHtml(userInput);
String safeJs = Encode.forJavaScript(userInput);
String safeUrl = Encode.forUriComponent(userInput);
Maven dependency for OWASP Encoder:
<dependency>
<groupId>org.owasp.encoder</groupId>
<artifactId>encoder</artifactId>
<version>1.2.3</version>
</dependency>
Content Security Policy
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'; style-src 'self'")
)
);
return http.build();
}
}
@WebFilter("/*")
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Content-Security-Policy", "default-src 'self'");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
chain.doFilter(req, res);
}
}
CSRF Protection
Spring Security
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}
Quarkus
# application.properties
quarkus.http.csrf.enabled=true
quarkus.http.csrf.cookie-name=XSRF-TOKEN
Authentication & Authorization
Password Storage
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hash = encoder.encode(rawPassword);
boolean matches = encoder.matches(rawPassword, hash);
PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String hash = encoder.encode(rawPassword);
String hash = DigestUtils.md5Hex(password);
Authorization Checks
@Service
public class DocumentService {
public Document getDocument(Long documentId, User currentUser) {
Document doc = documentRepository.findById(documentId)
.orElseThrow(() -> new NotFoundException("Document not found"));
if (!doc.getOwnerId().equals(currentUser.getId()) &&
!currentUser.hasRole("ADMIN")) {
throw new AccessDeniedException("Not authorized to access this document");
}
return doc;
}
}
@GetMapping("/documents/{id}")
public Document getDocument(@PathVariable Long id) {
return documentRepository.findById(id).orElseThrow();
}
Spring Security Annotations
@PreAuthorize("hasRole('ADMIN')")
public void adminOnly() { }
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public void ownDataOnly(Long userId) { }
@PreAuthorize("@authService.canAccess(#documentId, authentication)")
public Document getDocument(Long documentId) { }
Secrets Management
Never Hardcode Secrets
private static final String API_KEY = "sk-1234567890abcdef";
private static final String DB_PASSWORD = "admin123";
String apiKey = System.getenv("API_KEY");
@Value("${api.key}")
private String apiKey;
@Autowired
private SecretsManager secretsManager;
String apiKey = secretsManager.getSecret("api-key");
Configuration Files
spring:
datasource:
password: ${DB_PASSWORD}
api:
key: ${API_KEY}
spring:
datasource:
password: admin123
.gitignore
# Never commit these
.env
*.pem
*.key
*credentials*
*secret*
application-local.yml
Secure Deserialization
Avoid Java Serialization
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject();
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
User user = mapper.readValue(json, User.class);
Jackson Security
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.deactivateDefaultTyping();
return mapper;
}
}
Dependency Security
OWASP Dependency Check
Maven:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.7</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
</plugin>
Run:
mvn dependency-check:check
Keep Dependencies Updated
mvn versions:display-dependency-updates
mvn versions:use-latest-releases
Security Headers
Recommended Headers
| Header | Value | Purpose |
|---|
Content-Security-Policy | default-src 'self' | Prevent XSS |
X-Content-Type-Options | nosniff | Prevent MIME sniffing |
X-Frame-Options | DENY | Prevent clickjacking |
Strict-Transport-Security | max-age=31536000 | Force HTTPS |
X-XSS-Protection | 1; mode=block | Legacy XSS filter |
Spring Boot Configuration
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000))
.contentTypeOptions(Customizer.withDefaults())
);
return http.build();
}
Logging Security Events
log.info("User login successful", kv("userId", userId), kv("ip", clientIp));
log.warn("Failed login attempt", kv("username", username), kv("ip", clientIp), kv("attempt", attemptCount));
log.warn("Access denied", kv("userId", userId), kv("resource", resourceId), kv("action", action));
log.error("Authentication failure", kv("reason", reason), kv("ip", clientIp));
log.info("Login: user={}, password={}", username, password);
log.debug("Request body: {}", requestWithCreditCard);
Security Checklist
Code Review
Configuration
Dependencies
Related Skills
java-code-review - General code review
maven-dependency-audit - Dependency vulnerability scanning
logging-patterns - Secure logging practices