Skip to content

CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) - Java

Overview

Use of Cryptographically Weak PRNG in Java applications occurs when developers use non-cryptographic random number generators like java.util.Random or Math.random() for security-sensitive operations. These PRNGs are predictable and unsuitable for cryptographic purposes. Attackers can exploit this weakness to predict session tokens, encryption keys, passwords, or other sensitive values.

Primary Defence: Use java.security.SecureRandom for all security-sensitive random number generation including session tokens, CSRF tokens, encryption keys, and password reset tokens.

Common Vulnerable Patterns

Using Random for Session Tokens

import java.util.Random;
import javax.ws.rs.*;
import javax.ws.rs.core.*;

@Path("/api/auth")
public class VulnerableAuthResource {
    private static final Random random = new Random();
    private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    private String generateSessionToken() {
        // VULNERABLE - Using java.util.Random for session tokens
        StringBuilder token = new StringBuilder(32);
        for (int i = 0; i < 32; i++) {
            token.append(CHARS.charAt(random.nextInt(CHARS.length())));
        }
        return token.toString();
    }

    @POST
    @Path("/login")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(LoginRequest request) {
        // Authenticate user
        if (authenticateUser(request.getUsername(), request.getPassword())) {
            // VULNERABLE - Predictable session token
            String sessionToken = generateSessionToken();

            // Store in session
            SessionManager.createSession(sessionToken, request.getUsername());

            return Response.ok()
                .entity(new LoginResponse(sessionToken))
                .build();
        }

        return Response.status(Response.Status.UNAUTHORIZED)
            .entity("Invalid credentials")
            .build();
    }

    private boolean authenticateUser(String username, String password) {
        // Authentication logic
        return true;
    }
}

Why this is vulnerable:

  • Random uses linear congruential generator (predictable)
  • Attacker can predict future tokens after observing outputs
  • Session hijacking becomes possible
  • PRNG state can be recovered from consecutive outputs

Math.random() for Cryptographic Keys

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/api/keys")
public class VulnerableKeyController {

    private String generateAPIKey() {
        // VULNERABLE - Math.random() is not cryptographically secure
        StringBuilder key = new StringBuilder("sk_");

        for (int i = 0; i < 32; i++) {
            int value = (int) (Math.random() * 36);
            char c = value < 10 ? 
                (char) ('0' + value) : 
                (char) ('A' + value - 10);
            key.append(c);
        }

        return key.toString();
    }

    @PostMapping
    public ResponseEntity<APIKeyResponse> createAPIKey(@RequestBody APIKeyRequest request) {
        // VULNERABLE - Weak API key generation
        String apiKey = generateAPIKey();

        // Store in database
        apiKeyRepository.save(new APIKey(
            request.getUserId(),
            apiKey,
            request.getDescription()
        ));

        return ResponseEntity.ok(new APIKeyResponse(apiKey));
    }
}

Why this is vulnerable:

  • Math.random() uses Random internally (weak PRNG)
  • API keys are predictable
  • Attacker can predict keys for other users
  • Enables unauthorized API access

Time-based Seeding

import java.util.Random;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/reset-password")
public class VulnerablePasswordResetServlet extends HttpServlet {

    private String generateResetToken() {
        // VULNERABLE - Time-based seeding is predictable
        Random random = new Random(System.currentTimeMillis());

        long token = random.nextLong();
        return Long.toHexString(token);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
        String email = request.getParameter("email");

        // VULNERABLE - Predictable reset token
        String resetToken = generateResetToken();

        // Store token
        resetTokenRepository.save(email, resetToken);

        // Send email
        emailService.sendPasswordReset(email, resetToken);

        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().write("Reset email sent");
    }
}

Why this is vulnerable:

  • Time-based seeding is highly predictable
  • Attackers can synchronize clocks and predict tokens
  • Window of attack is just milliseconds
  • Password reset tokens can be guessed

ThreadLocalRandom for Security

import java.util.concurrent.ThreadLocalRandom;
import org.springframework.stereotype.Service;

@Service
public class VulnerableTokenService {

    public String generateCSRFToken() {
        // VULNERABLE - ThreadLocalRandom is NOT cryptographically secure
        StringBuilder token = new StringBuilder();

        for (int i = 0; i < 32; i++) {
            int value = ThreadLocalRandom.current().nextInt(16);
            token.append(Integer.toHexString(value));
        }

        return token.toString();
    }

    public byte[] generateSalt() {
        // VULNERABLE - Weak salt generation
        byte[] salt = new byte[16];
        ThreadLocalRandom.current().nextBytes(salt);
        return salt;
    }

    public String generateOTP() {
        // VULNERABLE - Predictable OTP
        int otp = ThreadLocalRandom.current().nextInt(100000, 1000000);
        return String.format("%06d", otp);
    }
}

Why this is vulnerable:

  • ThreadLocalRandom is optimized for performance, not security
  • Not suitable for cryptographic operations
  • CSRF tokens can be predicted
  • Salts lose their security properties

Weak IV Generation

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

public class VulnerableEncryption {
    private SecretKey key;
    private Random random = new Random();

    public VulnerableEncryption(SecretKey key) {
        this.key = key;
    }

    public String encrypt(String plaintext) throws Exception {
        // VULNERABLE - Weak IV generation
        byte[] iv = new byte[16];
        random.nextBytes(iv);  // Using Random instead of SecureRandom!

        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);

        byte[] ciphertext = cipher.doFinal(plaintext.getBytes());

        // Combine IV and ciphertext
        byte[] combined = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);

        return Base64.getEncoder().encodeToString(combined);
    }
}

// Spring Boot service using vulnerable encryption
import org.springframework.stereotype.Service;

@Service
public class DataEncryptionService {
    private final VulnerableEncryption encryption;

    public DataEncryptionService(SecretKey key) {
        this.encryption = new VulnerableEncryption(key);
    }

    public String encryptSensitiveData(String data) throws Exception {
        // VULNERABLE - Weak IV compromises encryption
        return encryption.encrypt(data);
    }
}

Why this is vulnerable:

  • IV must be unpredictable for CBC mode
  • Weak IV allows plaintext recovery attacks
  • Predictable IVs compromise confidentiality
  • Enables chosen-plaintext attacks

Weak UUID Generation

import java.util.Random;
import java.util.UUID;

public class VulnerableUUIDGenerator {
    private static final Random random = new Random();

    public static UUID generateWeakUUID() {
        // VULNERABLE - Custom UUID using weak randomness
        byte[] randomBytes = new byte[16];
        random.nextBytes(randomBytes);

        // Set version and variant bits for UUID
        randomBytes[6] &= 0x0f;
        randomBytes[6] |= 0x40;  // Version 4
        randomBytes[8] &= 0x3f;
        randomBytes[8] |= 0x80;  // Variant 2

        long mostSigBits = 0;
        long leastSigBits = 0;

        for (int i = 0; i < 8; i++) {
            mostSigBits = (mostSigBits << 8) | (randomBytes[i] & 0xff);
        }
        for (int i = 8; i < 16; i++) {
            leastSigBits = (leastSigBits << 8) | (randomBytes[i] & 0xff);
        }

        return new UUID(mostSigBits, leastSigBits);
    }

    public static String generateUserId() {
        // VULNERABLE - Predictable user IDs
        return "user_" + random.nextInt(1000000);
    }
}

// JPA Entity using weak UUIDs
import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    private UUID id;

    private String username;

    @PrePersist
    public void generateId() {
        // VULNERABLE - Predictable UUID
        this.id = VulnerableUUIDGenerator.generateWeakUUID();
    }
}

Why this is vulnerable:

  • Predictable UUIDs enable user enumeration
  • Attackers can guess valid user IDs
  • Privacy violation
  • Enables unauthorized access

Weak Password Generation

import java.util.Random;

public class VulnerablePasswordGenerator {
    private static final Random random = new Random();
    private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
    private static final String DIGITS = "0123456789";
    private static final String SPECIAL = "!@#$%^&*";

    public static String generateTemporaryPassword(int length) {
        // VULNERABLE - Using Random for password generation
        String allChars = UPPERCASE + LOWERCASE + DIGITS + SPECIAL;
        StringBuilder password = new StringBuilder();

        for (int i = 0; i < length; i++) {
            int index = random.nextInt(allChars.length());
            password.append(allChars.charAt(index));
        }

        return password.toString();
    }
}

// Micronaut controller
import io.micronaut.http.annotation.*;
import io.micronaut.http.HttpResponse;

@Controller("/api/users")
public class UserController {

    @Post("/create")
    public HttpResponse<CreateUserResponse> createUser(@Body CreateUserRequest request) {
        // VULNERABLE - Weak temporary password
        String tempPassword = VulnerablePasswordGenerator.generateTemporaryPassword(12);

        // Create user with temporary password
        User user = userService.createUser(
            request.getUsername(),
            tempPassword
        );

        return HttpResponse.ok(new CreateUserResponse(
            user.getId(),
            tempPassword,
            "Please change your password on first login"
        ));
    }
}

Why this is vulnerable:

  • Temporary passwords are predictable
  • Attacker can guess passwords for new users
  • Compromises account security
  • Violates password security best practices

Sequential Random Numbers

import java.util.Random;
import org.springframework.stereotype.Service;

@Service
public class VulnerableOrderService {
    private Random random = new Random();

    public String generateOrderId() {
        // VULNERABLE - Sequential random numbers are predictable
        long orderId = Math.abs(random.nextLong()) % 1000000000L;
        return String.format("ORD%09d", orderId);
    }

    public String generateInvoiceNumber() {
        // VULNERABLE - Predictable invoice numbers
        int invoiceNum = random.nextInt(900000) + 100000;
        return "INV-" + invoiceNum;
    }

    public String generateConfirmationCode() {
        // VULNERABLE - Predictable confirmation codes
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            code.append(random.nextInt(10));
        }
        return code.toString();
    }
}

Why this is vulnerable:

  • Order IDs can be enumerated
  • Privacy violation (reveals order volume)
  • Enables unauthorized access to orders
  • Business intelligence leakage

Secure Patterns

Using SecureRandom for Tokens

import java.security.SecureRandom;
import java.util.Base64;
import javax.ws.rs.*;
import javax.ws.rs.core.*;

@Path("/api/auth")
public class SecureAuthResource {
    private static final SecureRandom secureRandom = new SecureRandom();

    private String generateSessionToken() {
        // SECURE - Using SecureRandom for session tokens
        byte[] randomBytes = new byte[32];  // 256 bits
        secureRandom.nextBytes(randomBytes);

        // Base64 URL-safe encoding
        return Base64.getUrlEncoder().withoutPadding()
            .encodeToString(randomBytes);
    }

    private String generateSessionTokenHex() {
        // Alternative: Hex encoding
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);

        StringBuilder hexString = new StringBuilder();
        for (byte b : randomBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    @POST
    @Path("/login")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(LoginRequest request) {
        if (authenticateUser(request.getUsername(), request.getPassword())) {
            // SECURE - Cryptographically strong session token
            String sessionToken = generateSessionToken();

            // Store in database with expiration
            sessionRepository.createSession(
                sessionToken,
                request.getUsername(),
                Instant.now().plus(24, ChronoUnit.HOURS)
            );

            return Response.ok()
                .entity(new LoginResponse(sessionToken))
                .build();
        }

        return Response.status(Response.Status.UNAUTHORIZED)
            .entity("Invalid credentials")
            .build();
    }

    @POST
    @Path("/verify")
    @Produces(MediaType.APPLICATION_JSON)
    public Response verifySession(@HeaderParam("Authorization") String authHeader) {
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }

        String token = authHeader.substring(7);
        Session session = sessionRepository.findByToken(token);

        if (session == null || session.getExpiresAt().isBefore(Instant.now())) {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }

        return Response.ok()
            .entity(new SessionInfo(session.getUsername()))
            .build();
    }

    private boolean authenticateUser(String username, String password) {
        // Authentication logic
        return true;
    }
}

Why this works:

  • OS-level CSPRNG: SecureRandom uses system entropy sources (/dev/urandom on Linux, CryptGenRandom on Windows) for unpredictable output
  • 256-bit entropy: 32-byte tokens provide 2^256 possible values, making brute-force computationally impossible
  • URL-safe encoding: Base64.getUrlEncoder().withoutPadding() enables safe transmission in URLs/headers/cookies
  • Thread-safe: Static SecureRandom instance amortizes initialization cost across calls
  • Security-critical applications: Suitable for session tokens, CSRF tokens, and any identifier requiring cryptographic unpredictability

Secure API Key Generation

import java.security.SecureRandom;
import java.security.MessageDigest;
import java.util.Base64;
import org.springframework.stereotype.Service;

@Service
public class SecureAPIKeyService {
    private static final SecureRandom secureRandom = new SecureRandom();

    public APIKey generateAPIKey(Long userId, String description) throws Exception {
        // SECURE - Generate cryptographically strong API key
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);

        String apiKey = "sk_live_" + Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(randomBytes);

        // Hash API key for storage (never store plaintext)
        String keyHash = hashAPIKey(apiKey);

        // Store in database
        APIKey key = new APIKey();
        key.setUserId(userId);
        key.setKeyHash(keyHash);
        key.setDescription(description);
        key.setCreatedAt(Instant.now());

        apiKeyRepository.save(key);

        // Return plaintext key only once
        key.setPlaintextKey(apiKey);  // Transient field
        return key;
    }

    private String hashAPIKey(String apiKey) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(apiKey.getBytes());
        return Base64.getEncoder().encodeToString(hash);
    }

    public boolean verifyAPIKey(String apiKey) throws Exception {
        String keyHash = hashAPIKey(apiKey);

        APIKey key = apiKeyRepository.findByKeyHash(keyHash);

        if (key == null || key.getRevokedAt() != null) {
            return false;
        }

        // Update last used timestamp
        key.setLastUsed(Instant.now());
        apiKeyRepository.save(key);

        return true;
    }
}

// Spring Boot REST controller
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/api/keys")
public class APIKeyController {
    private final SecureAPIKeyService apiKeyService;

    public APIKeyController(SecureAPIKeyService apiKeyService) {
        this.apiKeyService = apiKeyService;
    }

    @PostMapping
    public ResponseEntity<APIKeyResponse> createAPIKey(@RequestBody APIKeyRequest request) {
        try {
            APIKey key = apiKeyService.generateAPIKey(
                request.getUserId(),
                request.getDescription()
            );

            return ResponseEntity.ok(new APIKeyResponse(
                key.getPlaintextKey(),
                "Store this key securely. It will not be shown again."
            ));
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }
}

Why this works:

  • 256-bit cryptographic entropy: SecureRandom ensures keys are unpredictable and unique, preventing guessing/enumeration
  • Hash before storage: SHA-256 hashing means compromised databases yield useless hashes, not working keys
  • One-time display: Transient field returns plaintext key only during creation, enforcing secure user storage
  • Industry-standard prefix: sk_live_ prefix (Stripe convention) enables automated scanning for accidental commits and distinguishes key types
  • Defense-in-depth: Strong generation + secure hashing + single display provides layered security

Secure Password Reset Tokens

import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import org.springframework.stereotype.Service;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;

@Service
public class SecurePasswordResetService {
    private static final SecureRandom secureRandom = new SecureRandom();
    private final PasswordResetTokenRepository tokenRepository;
    private final JavaMailSender mailSender;

    public SecurePasswordResetService(
            PasswordResetTokenRepository tokenRepository,
            JavaMailSender mailSender) {
        this.tokenRepository = tokenRepository;
        this.mailSender = mailSender;
    }

    public void initiatePasswordReset(String email) {
        // SECURE - Generate cryptographically strong reset token
        String resetToken = generateResetToken();

        // Store token with expiration
        PasswordResetToken token = new PasswordResetToken();
        token.setEmail(email);
        token.setToken(resetToken);
        token.setExpiresAt(Instant.now().plus(1, ChronoUnit.HOURS));
        token.setUsed(false);

        tokenRepository.save(token);

        // Send email
        sendResetEmail(email, resetToken);
    }

    private String generateResetToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding()
            .encodeToString(randomBytes);
    }

    public boolean validateAndResetPassword(String token, String newPassword) {
        PasswordResetToken resetToken = tokenRepository.findByToken(token);

        if (resetToken == null || 
            resetToken.isUsed() ||
            resetToken.getExpiresAt().isBefore(Instant.now())) {
            return false;
        }

        // Reset password
        User user = userRepository.findByEmail(resetToken.getEmail());
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);

        // Mark token as used
        resetToken.setUsed(true);
        tokenRepository.save(resetToken);

        return true;
    }

    private void sendResetEmail(String email, String token) {
        String resetUrl = "https://example.com/reset-password?token=" + token;

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(email);
        message.setSubject("Password Reset Request");
        message.setText("Click here to reset your password: " + resetUrl +
            "\n\nThis link expires in 1 hour.");

        mailSender.send(message);
    }
}

Why this works:

  • Cryptographic unpredictability: 256-bit entropy from SecureRandom prevents guessing even with email knowledge
  • One-time use: Marking used=true prevents replay attacks from intercepted reset emails
  • Time-limited window: Short expiration (typically 1 hour) makes tokens useless quickly if intercepted
  • Automatic validation: Database timestamp check rejects expired tokens without manual cleanup
  • Proof of email ownership: More secure than temporary passwords - requires email access to complete reset
  • Defense-in-depth: Cryptographic randomness + single-use + expiration creates multiple protective layers

Secure IV and Salt Generation

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

public class SecureEncryption {
    private static final SecureRandom secureRandom = new SecureRandom();
    private final SecretKey key;

    public SecureEncryption(SecretKey key) {
        this.key = key;
    }

    public String encrypt(String plaintext) throws Exception {
        // SECURE - Generate cryptographically strong IV
        byte[] iv = new byte[16];
        secureRandom.nextBytes(iv);

        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));

        byte[] ciphertext = cipher.doFinal(plaintext.getBytes());

        // Combine IV and ciphertext
        byte[] combined = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);

        return Base64.getEncoder().encodeToString(combined);
    }

    public String decrypt(String encryptedData) throws Exception {
        byte[] combined = Base64.getDecoder().decode(encryptedData);

        // Extract IV and ciphertext
        byte[] iv = new byte[16];
        byte[] ciphertext = new byte[combined.length - 16];
        System.arraycopy(combined, 0, iv, 0, iv.length);
        System.arraycopy(combined, iv.length, ciphertext, 0, ciphertext.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));

        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext);
    }

    public static byte[] generateSalt() {
        // SECURE - Generate cryptographically strong salt
        byte[] salt = new byte[32];  // 256 bits
        secureRandom.nextBytes(salt);
        return salt;
    }

    public static SecretKey generateKey() throws Exception {
        // SECURE - Generate cryptographically strong key
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256, secureRandom);  // Explicitly use SecureRandom
        return keyGen.generateKey();
    }
}

Why this works:

  • Unique random IVs/nonces: SecureRandom.nextBytes() for 96-bit GCM nonce ensures uniqueness; reusing IV with same key catastrophically breaks confidentiality via XOR analysis
  • Authenticated encryption: AES-GCM encrypts and generates authentication tag detecting tampering, preventing ciphertext manipulation
  • 256-bit salt uniqueness: 32-byte salts ensure identical passwords produce different hashes, preventing rainbow table attacks
  • Explicit CSPRNG: Passing SecureRandom to Cipher.init() ensures all cipher randomness uses cryptographic-quality entropy
  • Complete cryptographic workflow: Proper random usage for key derivation (salt), encryption (IV/nonce), and authentication (GCM)

UUID.randomUUID() (Uses SecureRandom)

import java.util.UUID;
import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(
        name = "UUID",
        strategy = "org.hibernate.id.UUIDGenerator"
    )
    private UUID id;

    private String username;
    private String email;

    // UUID.randomUUID() uses SecureRandom internally
    public static UUID generateSecureId() {
        return UUID.randomUUID();
    }
}

// Service using secure UUIDs
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public User createUser(String username, String email) {
        User user = new User();
        user.setId(UUID.randomUUID());  // SECURE - Uses SecureRandom
        user.setUsername(username);
        user.setEmail(email);

        return userRepository.save(user);
    }

    public String generateInvitationCode() {
        // SECURE - UUID for invitation codes
        return UUID.randomUUID().toString();
    }
}

Why this works:

  • 122 bits of randomness: UUID.randomUUID() uses SecureRandom internally (since Java 8+), providing 2^122 possible values (6 bits for version/variant)
  • Collision resistance: Sufficient entropy ensures trillions of UUIDs won't collide; brute-force remains computationally infeasible
  • Prevents enumeration: Unpredictability stops attackers from discovering resources via sequential ID incrementing
  • Public identifier use: Ideal for REST APIs, database primary keys in URLs, file names where sequential patterns leak information
  • Security consideration: For session tokens/API keys, prefer explicit 256-bit tokens via SecureRandom
  • Balance: UUIDs provide security, global uniqueness, and cross-system standardization

Secure OTP Generation

import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class SecureOTPService {
    private static final SecureRandom secureRandom = new SecureRandom();

    public String generateNumericOTP(int length) {
        // SECURE - Generate cryptographically strong numeric OTP
        StringBuilder otp = new StringBuilder();

        for (int i = 0; i < length; i++) {
            // SecureRandom for each digit
            otp.append(secureRandom.nextInt(10));
        }

        return otp.toString();
    }

    public String generateAlphanumericOTP(int length) {
        // SECURE - Alphanumeric OTP
        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";  // Exclude ambiguous chars
        StringBuilder otp = new StringBuilder();

        for (int i = 0; i < length; i++) {
            int index = secureRandom.nextInt(chars.length());
            otp.append(chars.charAt(index));
        }

        return otp.toString();
    }

    public OTP createOTP(String identifier, OTPType type) {
        String code = type == OTPType.NUMERIC ? 
            generateNumericOTP(6) : 
            generateAlphanumericOTP(8);

        OTP otp = new OTP();
        otp.setIdentifier(identifier);
        otp.setCode(code);
        otp.setType(type);
        otp.setExpiresAt(Instant.now().plus(10, ChronoUnit.MINUTES));
        otp.setUsed(false);

        return otpRepository.save(otp);
    }

    public boolean verifyOTP(String identifier, String code) {
        OTP otp = otpRepository.findByIdentifierAndCode(identifier, code);

        if (otp == null || 
            otp.isUsed() ||
            otp.getExpiresAt().isBefore(Instant.now())) {
            return false;
        }

        // Mark as used
        otp.setUsed(true);
        otpRepository.save(otp);

        return true;
    }
}

Why this works:

  • Cryptographic unpredictability: SecureRandom.nextInt(10) generates 10^6 = 1 million possibilities for 6-digit OTPs (500K average brute-force attempts)
  • Layered defenses: One-time use + short expiration (5-10 min) + rate limiting (3-5 attempts) makes brute-force practically impossible
  • Prevents prediction: Cryptographic strength ensures future OTPs unpredictable from past samples, unlike weak PRNGs
  • Replay prevention: Timestamp and used status prevent intercepted OTPs from being replayed
  • Security scaling: 8-digit OTPs (100M combinations) or alphanumeric (6 chars from 36 = 2.1B) for higher security
  • Suitable use cases: Email/SMS 2FA, temporary access codes, account verification workflows

Secure Password Generation

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SecurePasswordGenerator {
    private static final SecureRandom secureRandom = new SecureRandom();
    private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
    private static final String DIGITS = "0123456789";
    private static final String SPECIAL = "!@#$%^&*()_+-=[]{}|;:,.<>?";

    public static String generatePassword(int length, boolean includeSpecial) {
        if (length < 12) {
            throw new IllegalArgumentException("Password must be at least 12 characters");
        }

        // SECURE - Build character set
        String allChars = UPPERCASE + LOWERCASE + DIGITS;
        if (includeSpecial) {
            allChars += SPECIAL;
        }

        // SECURE - Generate password with SecureRandom
        StringBuilder password = new StringBuilder(length);

        // Ensure at least one of each required character type
        password.append(UPPERCASE.charAt(secureRandom.nextInt(UPPERCASE.length())));
        password.append(LOWERCASE.charAt(secureRandom.nextInt(LOWERCASE.length())));
        password.append(DIGITS.charAt(secureRandom.nextInt(DIGITS.length())));

        if (includeSpecial) {
            password.append(SPECIAL.charAt(secureRandom.nextInt(SPECIAL.length())));
        }

        // Fill remaining characters
        for (int i = password.length(); i < length; i++) {
            password.append(allChars.charAt(secureRandom.nextInt(allChars.length())));
        }

        // Shuffle to avoid predictable pattern
        return shuffleString(password.toString());
    }

    private static String shuffleString(String input) {
        List<Character> chars = new ArrayList<>();
        for (char c : input.toCharArray()) {
            chars.add(c);
        }
        Collections.shuffle(chars, secureRandom);

        StringBuilder shuffled = new StringBuilder();
        for (char c : chars) {
            shuffled.append(c);
        }
        return shuffled.toString();
    }
}

// Service using secure password generation
import io.micronaut.http.annotation.*;

@Controller("/api/users")
public class UserController {

    @Post("/create")
    public HttpResponse<CreateUserResponse> createUser(@Body CreateUserRequest request) {
        // SECURE - Generate strong temporary password
        String tempPassword = SecurePasswordGenerator.generatePassword(16, true);

        User user = userService.createUser(
            request.getUsername(),
            tempPassword
        );

        // Send password via secure channel
        emailService.sendTemporaryPassword(user.getEmail(), tempPassword);

        return HttpResponse.ok(new CreateUserResponse(
            user.getId(),
            "Temporary password sent to email"
        ));
    }
}

Why this works:

  • Large keyspace: Character pools (uppercase, lowercase, digits, special) provide 95 possible values per character - 16-character password has 95^16 ≈ 5×10^31 combinations
  • Policy compliance: Guarantees at least one character from each required category using SecureRandom selection
  • Pattern prevention: Fisher-Yates shuffle redistributes guaranteed characters randomly throughout password, preventing predictable patterns like "Aa1!" at start
  • High entropy: 16+ character minimum provides substantial entropy even if attackers know algorithm
  • Use cases: Superior to user-created passwords (weak/predictable); useful for account provisioning, temporary access codes, password resets

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Additional Resources