CWE-316: Cleartext Storage of Sensitive Information in Memory - Java
Overview
Storing sensitive data (passwords, cryptographic keys, tokens) in memory as cleartext in Java exposes it to heap dumps, debuggers, and memory disclosure vulnerabilities. Java strings are immutable and persist in the string pool, making them particularly dangerous for sensitive data. Use char[] for passwords, clear arrays explicitly, and leverage SecureString alternatives.
Primary Defence: Use char[] for passwords with explicit Arrays.fill(password, '\0') in finally blocks to enable clearing (unlike immutable strings), implement AutoCloseable with try-with-resources for automatic cleanup of sensitive resources, and use Java's Cleaner API (Java 9+) or custom memory-locking solutions for cryptographic keys to prevent cleartext persistence in heap dumps and enable deterministic memory clearing.
Common Vulnerable Patterns
Storing password as String
import java.util.Scanner;
// VULNERABLE - String is immutable, persists in string pool
public class InsecureAuth {
public boolean authenticate(String username) {
Scanner scanner = new Scanner(System.in);
// Password stored as immutable String
// Cannot be cleared from memory
String password = scanner.nextLine();
boolean result = verifyPassword(username, password);
// Password remains in memory and string pool
return result;
}
}
Storing API keys as String fields
// VULNERABLE - API keys persist in memory
public class APIClient {
private String apiKey;
private String apiSecret;
public APIClient(String key, String secret) {
// Immutable strings - cannot be cleared
this.apiKey = key;
this.apiSecret = secret;
}
public Response makeRequest(String endpoint) {
// API key visible in heap dumps
return httpClient.get(endpoint)
.header("Authorization", "Bearer " + apiKey)
.execute();
}
}
Logging sensitive data
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// VULNERABLE - Password logged to files
public class LoginService {
private static final Logger logger = LoggerFactory.getLogger(LoginService.class);
public void login(String username, String password) {
logger.debug("Login attempt: user={}, password={}", username, password);
// Password now in log files and log string objects
boolean success = authenticate(username, password);
logger.info("Login result: {}", success);
}
}
Not clearing char arrays
import javax.swing.JPasswordField;
// VULNERABLE - Password char array not cleared
public class PasswordForm {
public boolean submit(JPasswordField passwordField) {
char[] password = passwordField.getPassword();
// Use password
boolean result = authenticate(new String(password));
// char[] never cleared - remains in memory
return result;
}
}
Converting char[] to String
// VULNERABLE - Defeats purpose of char[]
public class PasswordHandler {
public void processPassword(char[] password) {
// Converting to String creates immutable copy
String passwordString = new String(password);
// Now password exists in both char[] and String
processCredential(passwordString);
// Even if we clear char[], String remains
Arrays.fill(password, '\0');
}
}
Secure Patterns
Using char[] for passwords with explicit clearing
import java.util.Arrays;
import javax.swing.JPasswordField;
public class SecureAuth {
public boolean authenticate(String username, JPasswordField passwordField) {
// Get password as char[] (mutable)
char[] password = passwordField.getPassword();
try {
// Use password for authentication
// Pass char[] directly, don't convert to String
return verifyPassword(username, password);
} finally {
// Always clear password from memory
Arrays.fill(password, '\0');
}
}
private boolean verifyPassword(String username, char[] password) {
// Work with char[] directly
byte[] passwordBytes = new byte[password.length];
for (int i = 0; i < password.length; i++) {
passwordBytes[i] = (byte) password[i];
}
try {
// Hash and verify
byte[] hash = hashPassword(passwordBytes);
return compareHashes(hash, getStoredHash(username));
} finally {
// Clear temporary byte array
Arrays.fill(passwordBytes, (byte) 0);
}
}
}
Why this works:
char[]is mutable and clearable: Unlike immutable strings, arrays can be zeroed withArrays.fill()to remove from memory- Prevents password persistence: Clearing eliminates data from memory dumps and debugger inspection
finallyensures cleanup: Password cleared even if authentication throws exception- Avoids string pooling issues: Strings are immutable and pooled, making them impossible to clear
- Java security best practice:
JPasswordField.getPassword()returnschar[]for this reason since JDK 1.2
Secure key management with AutoCloseable
import java.security.Key;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class SecureKeyManager implements AutoCloseable {
private byte[] keyBytes;
private boolean cleared = false;
public SecureKeyManager(byte[] key) {
// Store key in mutable byte array
this.keyBytes = Arrays.copyOf(key, key.length);
}
public byte[] encrypt(byte[] plaintext) throws Exception {
if (cleared) {
throw new IllegalStateException("Key has been cleared");
}
// Create key from bytes
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(plaintext);
}
@Override
public void close() {
// Clear key from memory
if (!cleared && keyBytes != null) {
Arrays.fill(keyBytes, (byte) 0);
cleared = true;
}
}
@Override
protected void finalize() throws Throwable {
try {
close();
} finally {
super.finalize();
}
}
}
// Usage with try-with-resources
public void processData(byte[] key, byte[] data) throws Exception {
try (SecureKeyManager keyManager = new SecureKeyManager(key)) {
byte[] encrypted = keyManager.encrypt(data);
// Use encrypted data
}
// Key automatically cleared after try block
}
Why this works:
AutoCloseableenables automatic cleanup: Try-with-resources ensures keys are cleared even on exceptions- Deterministic cleanup timing: Unlike GC which is non-deterministic, try-with-resources clears immediately
- Mutable
byte[]allows in-place clearing:Arrays.fill()zeroes key bytes from memory clearedflag prevents use-after-free: Fail-fast behavior throws exceptions if key used after clearing- Creates
SecretKeySpecper operation: Avoids long-lived Key objects that may not clear internal state
Console password reading with clearing
import java.io.Console;
import java.util.Arrays;
public class SecureConsoleAuth {
public void authenticateFromConsole() {
Console console = System.console();
if (console == null) {
throw new IllegalStateException("No console available");
}
// readPassword() returns char[] instead of String
char[] password = console.readPassword("Enter password: ");
try {
// Use password
boolean success = authenticate(password);
if (success) {
System.out.println("Authentication successful");
} else {
System.out.println("Authentication failed");
}
} finally {
// Always clear password
Arrays.fill(password, '\0');
}
}
}
Why this works:
readPassword()returns clearablechar[]: UnlikeString, arrays can be zeroed after use- Echo disabled prevents shoulder-surfing: Characters don't appear on screen during entry
finallyensures cleanup: Password cleared even if authentication fails or throws exception- Null check prevents crashes:
System.console()returns null when stdin redirected (IDE, scripts) - Oracle security best practice: Recommended approach for CLI password input in Secure Coding Guidelines
Secure credential holder with zero-on-GC
import java.lang.ref.Cleaner;
import java.util.Arrays;
public class SecureCredential implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private byte[] credentials;
private final Cleaner.Cleanable cleanable;
private boolean closed = false;
// State class for cleanup
private static class State implements Runnable {
private byte[] credentials;
State(byte[] credentials) {
this.credentials = credentials;
}
@Override
public void run() {
if (credentials != null) {
Arrays.fill(credentials, (byte) 0);
credentials = null;
}
}
}
public SecureCredential(byte[] credentials) {
this.credentials = Arrays.copyOf(credentials, credentials.length);
// Register cleanup action
State state = new State(this.credentials);
this.cleanable = CLEANER.register(this, state);
}
public byte[] getCredentials() {
if (closed) {
throw new IllegalStateException("Credentials have been cleared");
}
return credentials;
}
@Override
public void close() {
if (!closed) {
closed = true;
// Explicitly clear
if (credentials != null) {
Arrays.fill(credentials, (byte) 0);
credentials = null;
}
// Invoke cleanup
cleanable.clean();
}
}
}
// Usage
try (SecureCredential cred = new SecureCredential(secretBytes)) {
processCredential(cred.getCredentials());
}
// Automatically cleared
Why this works:
- Cleaner API reliability: Java 9+
Cleaner(replacesfinalize()) registersRunnablezeroing credential bytes when object phantom-reachable (eligible for GC) - Separate state management:
Stateclass holds credential bytes, registered separately from main object; cleanup action runs even afterSecureCredentialcollected - Dual cleanup: Explicit
close()for deterministic try-with-resources cleanup; cleaner provides safety net ifclose()forgotten;cleanable.clean()ensures single execution - Clearable storage:
byte[](copied withArrays.copyOf()to prevent external modification) enables in-place clearing withArrays.fill() - Superior to finalize():
Cleaneractions execute on dedicated thread with better guarantees vsfinalize()which had no run guarantees
COMPROMISE WARNING: Spring Security with char[] passwords
** Security Limitation: This pattern converts
char[]toStringfor BCrypt, which undermines the memory security benefit** of usingchar[]because the immutableStringpersists on the garbage-collected heap and cannot be cleared. This is a necessary compromise when using BCrypt libraries that only acceptStringparameters. For maximum security, consider using bcrypt implementations that acceptbyte[]or implement custom password hashing with PBKDF2/Argon2 that works directly with clearable byte arrays.
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
public class SecureAuthenticationService {
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public boolean authenticateUser(String username, char[] password) {
// Get stored hash from database
String storedHash = userRepository.findHashByUsername(username);
// LIMITATION: Converting to String creates immutable copy in heap
// that cannot be cleared and will persist until GC runs
String passwordString = new String(password);
try {
// Verify password
return passwordEncoder.matches(passwordString, storedHash);
} finally {
// Clear char array (but String still exists on heap)
Arrays.fill(password, '\0');
}
}
public void registerUser(String username, char[] password) {
// LIMITATION: String conversion defeats char[] security benefit
String passwordString = new String(password);
try {
// Hash password (BCrypt salts automatically)
String hashedPassword = passwordEncoder.encode(passwordString);
// Store hash only
userRepository.save(new User(username, hashedPassword));
} finally {
// Clear char array (but String still exists on heap)
Arrays.fill(password, '\0');
}
}
}
Why this is a compromise: While this pattern uses char[] for password input and clears it with Arrays.fill(), it must convert to an immutable String to work with Spring Security's BCryptPasswordEncoder (which only accepts String parameters), defeating the primary security benefit of using char[]. The String object created by new String(password) is immutable and will persist on the garbage-collected heap until GC runs, potentially appearing in memory dumps for an extended period. Clearing the char[] only removes one copy while the String copy remains. This is a necessary evil when using BCrypt libraries that don't accept byte arrays. Spring Security's BCrypt implementation does provide strong password hashing (automatic random salts, configurable work factor defaulting to 2^10 = 1024 iterations, constant-time comparison in matches() to prevent timing attacks), but the memory security is compromised. For applications with strict memory security requirements (PCI-DSS, HIPAA, high-security government systems), consider alternatives: (1) Use bcrypt implementations that accept byte[] parameters, (2) Implement PBKDF2 or Argon2 hashing directly with byte[] (see "Secure password comparison" pattern earlier in this document), or (3) Accept that web frameworks typically receive passwords as String from HTTP parameters anyway, making the char[] conversion largely symbolic unless you control the entire input pipeline.
JWT token handling with secure storage
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.security.Key;
import java.util.Arrays;
import javax.crypto.spec.SecretKeySpec;
public class SecureJWTHandler implements AutoCloseable {
private byte[] secretKey;
private boolean cleared = false;
public SecureJWTHandler(byte[] secret) {
// Store secret in mutable byte array
this.secretKey = Arrays.copyOf(secret, secret.length);
}
public String createToken(String subject) {
if (cleared) {
throw new IllegalStateException("Secret key has been cleared");
}
// Create key from bytes
Key key = new SecretKeySpec(secretKey, SignatureAlgorithm.HS256.getJcaName());
// Generate JWT
return Jwts.builder()
.setSubject(subject)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public String validateToken(String token) {
if (cleared) {
throw new IllegalStateException("Secret key has been cleared");
}
Key key = new SecretKeySpec(secretKey, SignatureAlgorithm.HS256.getJcaName());
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
@Override
public void close() {
if (!cleared && secretKey != null) {
Arrays.fill(secretKey, (byte) 0);
secretKey = null;
cleared = true;
}
}
}
// Usage
byte[] secret = loadSecretKey();
try (SecureJWTHandler jwtHandler = new SecureJWTHandler(secret)) {
String token = jwtHandler.createToken("user123");
// Use token
} finally {
// Clear original secret
Arrays.fill(secret, (byte) 0);
}
Why this works:
- Clearable secret: Stores JWT signing secret in mutable
byte[]that can be explicitly cleared withArrays.fill(secretKey, (byte) 0) - Deterministic cleanup:
AutoCloseablewith try-with-resources ensures cleanup even if exceptions occur during token creation/validation - Transient Key objects: Creates
SecretKeySpecper operation (not stored) - only clearable byte array persists - HMAC-SHA256 security: JJWT library's
signWith()generates tokens with cryptographic integrity; validation verifies signatures automatically - Fail-fast enforcement:
clearedflag throws exceptions if tokens created/validated after clearing; outerfinallyclears original secret for defense-in-depth
Servlet authentication with secure password handling
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
public class SecureLoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String username = request.getParameter("username");
String passwordParam = request.getParameter("password");
// Convert to char[] immediately
char[] password = passwordParam.toCharArray();
try {
// Authenticate using char[]
boolean authenticated = authenticateUser(username, password);
if (authenticated) {
request.getSession().setAttribute("user", username);
response.sendRedirect("/dashboard");
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
} finally {
// Always clear password
Arrays.fill(password, '\0');
}
}
private boolean authenticateUser(String username, char[] password) {
// Get stored hash
String storedHash = getUserHash(username);
// Hash input password
byte[] inputHash = hashPassword(password);
try {
byte[] storedHashBytes = Base64.getDecoder().decode(storedHash);
return Arrays.equals(inputHash, storedHashBytes);
} finally {
// Clear temporary hash
Arrays.fill(inputHash, (byte) 0);
}
}
}
Why this works:
- Immediate clearing: Converts password parameter to
char[]viatoCharArray()and clears infinally, minimizing cleartext time (original string in request object may persist) - Constant-time comparison:
Arrays.equals()prevents timing attacks that could leak password correctness via response time differences - No intermediate strings: Converts
char[]tobyte[]for hashing without creating string copies - Session-based auth: Creates session after successful auth to avoid password retransmission; generic error messages prevent username enumeration
- Servlet/JSP context: Important for applications where passwords arrive as HTTP POST parameters (strings) but should convert to clearable arrays immediately
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