security-audit

Security checklist for Java applications based on OWASP Top 10 and secure coding practices.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "security-audit" with this command: npx skills add decebals/claude-code-java/decebals-claude-code-java-security-audit

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.

// ✅ GOOD: Validate at boundary 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;

}

// Controller/Resource - trigger validation public Response createUser(@Valid CreateUserRequest request) { // request is already validated }

Custom Validators

// Custom annotation @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 {}; }

// Validator implementation public class SafeHtmlValidator implements ConstraintValidator<SafeHtml, String> {

private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
    "&#x3C;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

// ❌ BAD: Blocklist (attackers find bypasses) if (input.contains("<script>")) { throw new ValidationException("Invalid input"); }

// ✅ GOOD: Allowlist (only permit known-good) 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)

// ✅ GOOD: Parameterized queries @Query("SELECT u FROM User u WHERE u.email = :email") Optional<User> findByEmail(@Param("email") String email);

// ✅ GOOD: Criteria API 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)); // Safe

// ✅ GOOD: Named parameters TypedQuery<User> query = entityManager.createQuery( "SELECT u FROM User u WHERE u.status = :status", User.class); query.setParameter("status", status); // Safe

// ❌ BAD: String concatenation String jpql = "SELECT u FROM User u WHERE u.email = '" + email + "'"; // VULNERABLE!

Native Queries

// ✅ GOOD: Parameterized native query @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true) User findByEmailNative(String email);

// ❌ BAD: Concatenated native query String sql = "SELECT * FROM users WHERE email = '" + email + "'"; // VULNERABLE!

JDBC (Plain Java)

// ✅ GOOD: PreparedStatement 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(); }

// ❌ BAD: Statement with concatenation String sql = "SELECT * FROM users WHERE email = '" + email + "'"; // VULNERABLE! Statement stmt = connection.createStatement(); stmt.executeQuery(sql);

XSS Prevention

Output Encoding

// ✅ GOOD: Use templating engine's auto-escaping

// Thymeleaf - auto-escapes by default <p th:text="${userInput}">...</p> // Safe

// To display HTML (dangerous, use carefully): <p th:utext="${trustedHtml}">...</p> // Only for trusted content!

// ✅ GOOD: Manual encoding when needed 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

// Add CSP header to prevent inline scripts

// Spring Boot @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();
}

}

// Servlet Filter (works everywhere) @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

// CSRF enabled by default for browser clients @Configuration public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // For REST APIs with JWT (stateless) - can disable CSRF
        .csrf(csrf -> csrf.disable())

        // For browser apps with sessions - keep CSRF enabled
        .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

// ✅ GOOD: Use BCrypt or Argon2 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;

// BCrypt (widely supported) PasswordEncoder encoder = new BCryptPasswordEncoder(12); // strength 12 String hash = encoder.encode(rawPassword); boolean matches = encoder.matches(rawPassword, hash);

// Argon2 (recommended for new projects) PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); String hash = encoder.encode(rawPassword);

// ❌ BAD: MD5, SHA1, SHA256 without salt String hash = DigestUtils.md5Hex(password); // NEVER for passwords!

Authorization Checks

// ✅ GOOD: Check authorization at service layer @Service public class DocumentService {

public Document getDocument(Long documentId, User currentUser) {
    Document doc = documentRepository.findById(documentId)
        .orElseThrow(() -> new NotFoundException("Document not found"));

    // Authorization check
    if (!doc.getOwnerId().equals(currentUser.getId()) &#x26;&#x26;
        !currentUser.hasRole("ADMIN")) {
        throw new AccessDeniedException("Not authorized to access this document");
    }

    return doc;
}

}

// ❌ BAD: Only check at controller level, trust user input @GetMapping("/documents/{id}") public Document getDocument(@PathVariable Long id) { return documentRepository.findById(id).orElseThrow(); // No auth check! }

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

// ❌ BAD: Hardcoded secrets private static final String API_KEY = "sk-1234567890abcdef"; private static final String DB_PASSWORD = "admin123";

// ✅ GOOD: Environment variables String apiKey = System.getenv("API_KEY");

// ✅ GOOD: External configuration @Value("${api.key}") private String apiKey;

// ✅ GOOD: Secrets manager @Autowired private SecretsManager secretsManager; String apiKey = secretsManager.getSecret("api-key");

Configuration Files

✅ GOOD: Reference environment variables

spring: datasource: password: ${DB_PASSWORD}

api: key: ${API_KEY}

❌ BAD: Hardcoded in application.yml

spring: datasource: password: admin123 # NEVER!

.gitignore

Never commit these

.env *.pem *.key credentials secret application-local.yml

Secure Deserialization

Avoid Java Serialization

// ❌ DANGEROUS: Java ObjectInputStream ObjectInputStream ois = new ObjectInputStream(untrustedInput); Object obj = ois.readObject(); // Remote Code Execution risk!

// ✅ GOOD: Use JSON with Jackson ObjectMapper mapper = new ObjectMapper(); // Disable dangerous features mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL ); // Be careful with polymorphic types!

User user = mapper.readValue(json, User.class);

Jackson Security

// ✅ Configure Jackson safely @Configuration public class JacksonConfig {

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();

    // Prevent unknown properties exploitation
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    // Don't allow class type in JSON (prevents gadget attacks)
    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> <!-- Fail on high severity --> </configuration> </plugin>

Run:

mvn dependency-check:check

Report: target/dependency-check-report.html

Keep Dependencies Updated

Check for updates

mvn versions:display-dependency-updates

Update to latest

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 security-relevant 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));

// ❌ NEVER log sensitive data log.info("Login: user={}, password={}", username, password); // NEVER! log.debug("Request body: {}", requestWithCreditCard); // NEVER!

Security Checklist

Code Review

  • Input validated with allowlist patterns

  • SQL queries use parameters (no concatenation)

  • Output encoded for context (HTML, JS, URL)

  • Authorization checked at service layer

  • No hardcoded secrets

  • Passwords hashed with BCrypt/Argon2

  • Sensitive data not logged

  • CSRF protection enabled (for browser apps)

Configuration

  • HTTPS enforced

  • Security headers configured

  • Debug/dev features disabled in production

  • Default credentials changed

  • Error messages don't leak internal details

Dependencies

  • No known vulnerabilities (OWASP check)

  • Dependencies up to date

  • Unnecessary dependencies removed

Related Skills

  • java-code-review

  • General code review

  • maven-dependency-audit

  • Dependency vulnerability scanning

  • logging-patterns

  • Secure logging practices

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

maven-dependency-audit

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

java-code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clean-code

No summary provided by upstream source.

Repository SourceNeeds Review