Security Best Practices

Building Secure Java Applications

← Back to Index

Input Validation

Never trust user input. Validate all data at system boundaries.

Whitelisting Approach

// GOOD: Whitelist validation - define what IS allowed
public String sanitizeUsername(String username) {
    if (username == null || username.isBlank()) {
        throw new ValidationException("Username required");
    }

    // Only allow alphanumeric and underscore
    if (!username.matches("^[a-zA-Z0-9_]{3,20}$")) {
        throw new ValidationException("Invalid username format");
    }

    return username;
}

// Bean Validation annotations
public class UserRegistration {
    @NotBlank
    @Size(min = 3, max = 20)
    @Pattern(regexp = "^[a-zA-Z0-9_]+$")
    private String username;

    @Email
    @NotBlank
    private String email;

    @Size(min = 8, max = 100)
    private String password;
}

SQL Injection Prevention

// BAD: SQL Injection vulnerable!
public User findByUsername(String username) {
    String sql = "SELECT * FROM users WHERE username = '" + username + "'";
    // Attacker input: ' OR '1'='1' --
    // Results in: SELECT * FROM users WHERE username = '' OR '1'='1' --'
    return jdbcTemplate.queryForObject(sql, userRowMapper);
}

// GOOD: Parameterized query
public User findByUsername(String username) {
    String sql = "SELECT * FROM users WHERE username = ?";
    return jdbcTemplate.queryForObject(sql, userRowMapper, username);
}

// GOOD: JPA/Hibernate with named parameters
@Query("SELECT u FROM User u WHERE u.username = :username")
Optional<User> findByUsername(@Param("username") String username);

// GOOD: Criteria API
public List<User> findByUsername(String username) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<User> query = cb.createQuery(User.class);
    Root<User> root = query.from(User.class);
    query.where(cb.equal(root.get("username"), username));
    return em.createQuery(query).getResultList();
}

Cross-Site Scripting (XSS) Prevention

// BAD: Directly outputting user content
@GetMapping("/profile")
public String profile(Model model) {
    String bio = user.getBio();  // Could contain: <script>alert('XSS')</script>
    model.addAttribute("bio", bio);
    return "profile";  // Rendered without escaping!
}

// GOOD: Thymeleaf auto-escapes by default
// In template: <p th:text="${bio}"></p>
// Output: &lt;script&gt;alert('XSS')&lt;/script&gt;

// For APIs - encode output
public String sanitizeForHtml(String input) {
    return HtmlUtils.htmlEscape(input);
}

// Content Security Policy header
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.headers(headers -> headers
        .contentSecurityPolicy(csp -> csp
            .policyDirectives("default-src 'self'; script-src 'self'")
        )
    );
    return http.build();
}

Password Security

// NEVER store plain text passwords!

// GOOD: Use BCrypt (or Argon2)
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // Cost factor
}

@Service
public class UserService {
    private final PasswordEncoder passwordEncoder;

    public void register(UserRegistration registration) {
        User user = new User();
        user.setUsername(registration.getUsername());
        // Hash password before storing
        user.setPasswordHash(passwordEncoder.encode(registration.getPassword()));
        userRepository.save(user);
    }

    public boolean authenticate(String username, String password) {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new AuthenticationException("Invalid credentials"));

        // Compare with hashed password
        return passwordEncoder.matches(password, user.getPasswordHash());
    }
}

// Password requirements validation
public void validatePassword(String password) {
    if (password.length() < 8) {
        throw new ValidationException("Password must be at least 8 characters");
    }
    if (!password.matches(".*[A-Z].*")) {
        throw new ValidationException("Password must contain uppercase letter");
    }
    if (!password.matches(".*[0-9].*")) {
        throw new ValidationException("Password must contain a digit");
    }
}

Secrets Management

// BAD: Hardcoded credentials
String apiKey = "sk_live_abc123xyz789";  // Never do this!

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

// GOOD: Spring externalized configuration
@Value("${api.secret-key}")
private String apiSecretKey;

// GOOD: Spring Cloud Vault integration
@Configuration
public class VaultConfig {
    @Value("${vault.database.password}")
    private String dbPassword;
}

// Sensitive data in logs
// BAD
log.info("User {} logged in with password {}", username, password);

// GOOD: Never log sensitive data
log.info("User {} logged in", username);

// Clear sensitive data from memory when done
char[] password = getPassword();
try {
    authenticate(password);
} finally {
    Arrays.fill(password, '\0');  // Overwrite password
}

CSRF Protection

// Spring Security CSRF protection (enabled by default)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // CSRF is enabled by default
        // For APIs using JWT/tokens, you might disable it:
        // .csrf(csrf -> csrf.disable())

        // Configure for SPA with cookie-based CSRF
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        );

    return http.build();
}

// In Thymeleaf forms - token included automatically
// <form th:action="@{/submit}" method="post">
//     <!-- Hidden CSRF token field added automatically -->
// </form>

Secure Random Numbers

// BAD: Predictable random - NOT for security
Random random = new Random();
String token = String.valueOf(random.nextLong());  // Predictable!

// GOOD: Cryptographically secure random
SecureRandom secureRandom = new SecureRandom();
byte[] tokenBytes = new byte[32];
secureRandom.nextBytes(tokenBytes);
String token = Base64.getUrlEncoder().encodeToString(tokenBytes);

// For session IDs, tokens, etc.
public String generateSecureToken() {
    byte[] bytes = new byte[32];
    new SecureRandom().nextBytes(bytes);
    return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}

Secure File Handling

// BAD: Path traversal vulnerability
public File getFile(String filename) {
    // Attacker input: "../../../etc/passwd"
    return new File("/uploads/" + filename);
}

// GOOD: Validate and normalize path
public Path getFile(String filename) {
    Path basePath = Paths.get("/uploads").toAbsolutePath().normalize();
    Path filePath = basePath.resolve(filename).normalize();

    // Ensure file is within base directory
    if (!filePath.startsWith(basePath)) {
        throw new SecurityException("Invalid file path");
    }

    return filePath;
}

// Validate file uploads
public void handleUpload(MultipartFile file) {
    // Check file type by content, not just extension
    String contentType = file.getContentType();
    if (!ALLOWED_TYPES.contains(contentType)) {
        throw new ValidationException("File type not allowed");
    }

    // Limit file size
    if (file.getSize() > MAX_FILE_SIZE) {
        throw new ValidationException("File too large");
    }

    // Generate new filename
    String safeFilename = UUID.randomUUID() + getExtension(file);
}

Security Headers

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.headers(headers -> headers
        // Prevent clickjacking
        .frameOptions(frame -> frame.deny())

        // XSS protection (mostly for older browsers)
        .xssProtection(xss -> xss.enable())

        // Prevent MIME type sniffing
        .contentTypeOptions(content -> {})

        // HTTP Strict Transport Security
        .httpStrictTransportSecurity(hsts -> hsts
            .maxAgeInSeconds(31536000)
            .includeSubDomains(true)
        )

        // Content Security Policy
        .contentSecurityPolicy(csp -> csp
            .policyDirectives("default-src 'self'; script-src 'self'")
        )
    );

    return http.build();
}

Security Checklist

OWASP Top 10 Quick Reference
  • Injection - Use parameterized queries
  • Broken Authentication - Strong passwords, MFA, secure sessions
  • Sensitive Data Exposure - Encrypt at rest and in transit
  • XML External Entities - Disable DTDs, use safe parsers
  • Broken Access Control - Check authorization on every request
  • Security Misconfiguration - Secure defaults, remove unused features
  • Cross-Site Scripting - Escape output, use CSP
  • Insecure Deserialization - Avoid Java serialization for untrusted data
  • Components with Vulnerabilities - Keep dependencies updated
  • Insufficient Logging - Log security events, monitor for attacks