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:
Randomuses 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()usesRandominternally (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:
ThreadLocalRandomis 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:
SecureRandomuses 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
SecureRandominstance 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:
SecureRandomensures 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
SecureRandomprevents guessing even with email knowledge - One-time use: Marking
used=trueprevents 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
SecureRandomtoCipher.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()usesSecureRandominternally (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
SecureRandomselection - 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