Skip to main content
Java advanced Lesson 46 of 58

Java Security

Secure Java coding practices — input validation, OWASP Top 10 for Java, KeyStore, TLS/HTTPS setup, and protecting against common vulnerabilities.

Input Validation

All data from external sources — HTTP parameters, headers, database results, file contents, environment variables — is untrusted:

import java.util.regex.*;

public class InputValidator {

    // Whitelist validation — only allow known-good characters
    private static final Pattern ALPHANUMERIC = Pattern.compile("^[a-zA-Z0-9_\\-]{1,50}$");
    private static final Pattern EMAIL = Pattern.compile(
        "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$"
    );

    public static String requireValidUsername(String input) {
        if (input == null || !ALPHANUMERIC.matcher(input).matches()) {
            throw new IllegalArgumentException("Invalid username format");
        }
        return input;
    }

    public static String requireValidEmail(String input) {
        if (input == null || !EMAIL.matcher(input.trim()).matches()) {
            throw new IllegalArgumentException("Invalid email format");
        }
        return input.trim().toLowerCase();
    }

    // Length limits — prevent resource exhaustion
    public static String requireLength(String input, int minLen, int maxLen, String fieldName) {
        Objects.requireNonNull(input, fieldName + " must not be null");
        if (input.length() < minLen || input.length() > maxLen) {
            throw new IllegalArgumentException(
                fieldName + " must be between " + minLen + " and " + maxLen + " characters"
            );
        }
        return input;
    }

    // Numeric range validation
    public static int requireInRange(int value, int min, int max, String fieldName) {
        if (value < min || value > max) {
            throw new IllegalArgumentException(
                fieldName + " must be between " + min + " and " + max
            );
        }
        return value;
    }
}

SQL Injection Prevention

import java.sql.*;

public class UserRepository {

    // NEVER do this — SQL injection vulnerability
    public User findByUsernameInsecure(Connection conn, String username) throws SQLException {
        // If username is: ' OR '1'='1  → returns all users
        String sql = "SELECT * FROM users WHERE username = '" + username + "'";
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql); // DANGEROUS
        // ...
    }

    // ALWAYS use PreparedStatement with parameter binding
    public Optional<User> findByUsername(Connection conn, String username) throws SQLException {
        String sql = "SELECT id, username, email FROM users WHERE username = ?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, username); // safely bound — no injection possible
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(new User(rs.getLong("id"),
                                                rs.getString("username"),
                                                rs.getString("email")));
                }
                return Optional.empty();
            }
        }
    }

    // Dynamic queries — still use PreparedStatement
    public List<User> findUsers(Connection conn, String role, boolean activeOnly)
            throws SQLException {
        StringBuilder sql = new StringBuilder(
            "SELECT id, username, email FROM users WHERE role = ?");
        if (activeOnly) sql.append(" AND active = TRUE");
        sql.append(" ORDER BY username");

        try (PreparedStatement ps = conn.prepareStatement(sql.toString())) {
            ps.setString(1, role);
            // ...
        }
    }
}

Password Hashing

// Using Spring Security's BCryptPasswordEncoder
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

public class PasswordService {

    // BCrypt with strength 12 (default is 10; each +1 doubles the work)
    private final PasswordEncoder encoder = new BCryptPasswordEncoder(12);

    public String hashPassword(String rawPassword) {
        // BCrypt auto-generates a unique salt
        return encoder.encode(rawPassword);
        // Result: $2a$12$... (60-char string containing salt + hash)
    }

    public boolean verifyPassword(String rawPassword, String storedHash) {
        return encoder.matches(rawPassword, storedHash);
    }
}

// Using Java's built-in PBKDF2 (no external dependency)
import javax.crypto.spec.*;
import javax.crypto.*;
import java.security.*;

public class Pbkdf2PasswordService {

    private static final int ITERATIONS   = 310_000; // OWASP 2023 recommendation
    private static final int KEY_LENGTH   = 256;
    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";

    public byte[] hash(char[] password, byte[] salt) throws Exception {
        var spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
        var factory = SecretKeyFactory.getInstance(ALGORITHM);
        return factory.generateSecret(spec).getEncoded();
    }

    public byte[] generateSalt() {
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);
        return salt;
    }
}

Secure Random and Token Generation

import java.security.SecureRandom;
import java.util.Base64;
import java.util.HexFormat;

public class TokenGenerator {

    // NEVER use java.util.Random for security-sensitive values
    // Math.random() and new Random() are NOT cryptographically secure

    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    // Generate a URL-safe token (e.g., password reset, session ID)
    public static String generateToken(int byteLength) {
        byte[] bytes = new byte[byteLength];
        SECURE_RANDOM.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    // 32 bytes = 256 bits = 43 base64 characters
    public static String generateSessionToken() {
        return generateToken(32);
    }

    // Generate a numeric OTP
    public static String generateOtp(int digits) {
        long max = (long) Math.pow(10, digits);
        long otp = Math.abs(SECURE_RANDOM.nextLong()) % max;
        return String.format("%0" + digits + "d", otp);
    }
}

// Usage
String resetToken = TokenGenerator.generateToken(32);
// "Xk2p_Rz8vQ3mNj7LwEi1Oa5Yh6Fc9Ub4Td0Sv2" (example — unpredictable)

Encryption with JCA

import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.util.Base64;

public class AesEncryption {

    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int KEY_BITS    = 256;
    private static final int IV_BYTES    = 12;  // 96-bit IV for GCM
    private static final int TAG_BITS    = 128; // GCM authentication tag

    public static SecretKey generateKey() throws Exception {
        KeyGenerator gen = KeyGenerator.getInstance("AES");
        gen.init(KEY_BITS, new SecureRandom());
        return gen.generateKey();
    }

    // Returns IV + ciphertext (IV must be stored alongside the ciphertext)
    public static byte[] encrypt(byte[] plaintext, SecretKey key) throws Exception {
        byte[] iv = new byte[IV_BYTES];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
        byte[] ciphertext = cipher.doFinal(plaintext);

        // Prepend IV to ciphertext
        byte[] result = new byte[IV_BYTES + ciphertext.length];
        System.arraycopy(iv,         0, result, 0,       IV_BYTES);
        System.arraycopy(ciphertext, 0, result, IV_BYTES, ciphertext.length);
        return result;
    }

    public static byte[] decrypt(byte[] ivAndCiphertext, SecretKey key) throws Exception {
        byte[] iv         = new byte[IV_BYTES];
        byte[] ciphertext = new byte[ivAndCiphertext.length - IV_BYTES];
        System.arraycopy(ivAndCiphertext, 0,       iv,         0, IV_BYTES);
        System.arraycopy(ivAndCiphertext, IV_BYTES, ciphertext, 0, ciphertext.length);

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
        return cipher.doFinal(ciphertext); // throws AEADBadTagException if tampered
    }
}

KeyStore — Managing Keys and Certificates

import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;
import java.io.*;
import java.nio.file.*;

public class KeyStoreDemo {

    // Load a KeyStore from a PKCS12 file (.p12 / .pfx)
    public static KeyStore loadKeyStore(Path path, char[] password) throws Exception {
        KeyStore ks = KeyStore.getInstance("PKCS12");
        try (InputStream is = Files.newInputStream(path)) {
            ks.load(is, password);
        }
        return ks;
    }

    // Create an SSLContext that uses a custom keystore (e.g., mutual TLS)
    public static SSLContext createSslContext(KeyStore keyStore, char[] keyPassword,
                                              KeyStore trustStore) throws Exception {
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, keyPassword);

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
        return ctx;
    }
}

HTTPS with HttpClient

import java.net.http.*;
import java.net.URI;

public class SecureHttpClient {

    // Default HttpClient already validates TLS certificates
    private static final HttpClient CLIENT = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .followRedirects(HttpClient.Redirect.NORMAL)
        .build();

    public static String get(String url) throws Exception {
        var request = HttpRequest.newBuilder(URI.create(url))
            .GET()
            .header("Accept", "application/json")
            .build();

        var response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("Unexpected status: " + response.statusCode());
        }
        return response.body();
    }

    // Custom trust store (e.g., internal CA)
    public static HttpClient buildWithCustomTrustStore(SSLContext sslContext) {
        return HttpClient.newBuilder()
            .sslContext(sslContext)
            .build();
    }
}

Security Checklist

OWASP Top 10 for Java

#VulnerabilityJava mitigation
A01Broken Access ControlSpring Security @PreAuthorize, method-level security
A02Cryptographic FailuresUse AES-256-GCM, BCrypt; no MD5/SHA-1 for passwords
A03Injection (SQL, LDAP, OS)PreparedStatement, parameterised queries, no string concat
A04Insecure DesignThreat modelling, defence in depth
A05Security MisconfigurationDisable debug endpoints, no default credentials
A06Vulnerable Componentsmvn versions:display-dependency-updates, Dependabot
A07Auth & Session FailuresSecureRandom tokens, short session expiry, rate limiting
A08Software Integrity FailuresVerify dependency checksums, sign JARs
A09Security Logging & MonitoringLog auth events, anomaly detection
A10SSRFValidate and allowlist outbound URLs
// Path traversal prevention
public byte[] readUserFile(String filename) throws IOException {
    // Bad — attacker passes "../../etc/passwd"
    // return Files.readAllBytes(Path.of("/user-files/" + filename));

    // Good — resolve and verify the path stays inside the allowed directory
    Path base     = Path.of("/user-files").toRealPath();
    Path resolved = base.resolve(filename).normalize();

    if (!resolved.startsWith(base)) {
        throw new SecurityException("Path traversal attempt detected: " + filename);
    }
    return Files.readAllBytes(resolved);
}

// Deserialization — never deserialize untrusted data with ObjectInputStream
// Use JSON (Jackson) or Protocol Buffers instead
// If you must use Java serialization, implement a whitelist filter:
ObjectInputStream ois = new ObjectInputStream(inputStream) {
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        Set<String> allowed = Set.of("com.example.MyClass", "java.util.ArrayList");
        if (!allowed.contains(desc.getName())) {
            throw new InvalidClassException("Unauthorized deserialization: " + desc.getName());
        }
        return super.resolveClass(desc);
    }
};

Frequently Asked Questions

What is the most common security vulnerability in Java web apps?
SQL injection remains the most common and most dangerous. It occurs when user input is concatenated directly into SQL strings instead of using parameterized queries (PreparedStatement). A close second is broken authentication — weak tokens, no rate limiting, or storing passwords in plain text.
How should I store passwords in Java?
Never store plain text passwords or use MD5/SHA-1. Use a password hashing function designed for this purpose: BCrypt (via Spring Security's BCryptPasswordEncoder), Argon2 (recommended by OWASP), or PBKDF2. These are intentionally slow and include a salt automatically.
What is the difference between authentication and authorisation?
Authentication answers 'who are you?' — verifying identity (username + password, JWT, certificate). Authorisation answers 'what can you do?' — checking that the authenticated identity has permission to perform the requested action. Always authenticate before authorising.
Should I write my own cryptography code?
No. Cryptography is extremely easy to get wrong. Use well-audited libraries: Java's built-in JCA (Java Cryptography Architecture) for symmetric encryption and hashing, Bouncy Castle for advanced algorithms, and Spring Security or JJWT for JWT. Never roll your own crypto algorithms.