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 cannot be overwritten in place; string literals and interned strings can also persist in the string pool. Use char[] or byte[] for sensitive values where the surrounding APIs support them, clear arrays explicitly, and avoid unnecessary copies.
Primary Defence: Use char[] or byte[] for passwords and keys with explicit Arrays.fill(...) in finally blocks where possible, implement AutoCloseable with try-with-resources for deterministic cleanup, and treat Cleaner or finalization-style cleanup only as a best-effort safety net rather than a guarantee against heap-dump exposure.
Common Vulnerable Patterns
Storing password as String
import java.util.Scanner;
// VULNERABLE - String is immutable and cannot be cleared in place
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 until garbage collection; interned strings can persist longer
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- Reduces password persistence: Clearing removes the contents of that specific array from later memory dumps and debugger inspection, but earlier copies may still exist
finallyensures cleanup: Password cleared even if authentication throws exception- Avoids immutable-string issues: Strings cannot be overwritten in place, and interned strings can persist longer than ordinary heap objects
- 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;
}
}
}
// 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 - No finalizer dependency: Cleanup is tied to explicit
close()instead of relying on deprecated, non-deterministic finalization
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 safety net: Java 9+
Cleanercan register aRunnableto zero credential bytes when the object becomes phantom-reachable, but timing still depends on garbage collection - 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() - Prefer explicit close:
Cleaneravoids somefinalize()problems, but explicitclose()remains the control that gives predictable cleanup timing
Spring Security with char[] passwords (compromise)
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 practical compromise when using BCrypt libraries that only acceptStringparameters. For stricter memory-handling requirements, consider password-hashing APIs that acceptbyte[]or implement PBKDF2/Argon2 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). The String object created by new String(password) cannot be cleared and may appear in heap dumps until garbage collection and memory reuse occur. Clearing the char[] removes one copy while the String copy remains. Spring Security's BCrypt implementation still provides strong password hashing, but it does not fully solve CWE-316 memory-residency concerns. For applications with strict memory-security requirements, consider password-hashing APIs that accept byte[], implement PBKDF2 or Argon2 directly with clearable byte arrays, or document that HTTP/web-framework input paths already provide passwords as String.
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 java.security.MessageDigest.isEqual(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) - Timing-resistant comparison:
MessageDigest.isEqual()avoids the early-exit behavior of ordinary array equality and is more appropriate for comparing password hashes - 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
Remediation Steps
- Locate sensitive values held in
String, static fields, caches, request objects, and long-lived service objects. - Trace input boundaries such as Swing, console, servlet, Spring MVC, and configuration loaders to identify unavoidable string copies.
- Use
char[]orbyte[]where APIs support them, and clear arrays withArrays.fill(...)infinallyblocks orAutoCloseable.close(). - Treat
Cleaneras a safety net only; use try-with-resources for deterministic cleanup. - Replace logging, exception, and debugging output that includes passwords, keys, tokens, or derived secrets.
- Re-scan and review heap-dump/crash-dump handling for the affected service.
Testing
- Normal input: verify login, token signing, encryption, and credential flows still work with the new clearable-buffer paths.
- Boundary input: test authentication failures, exceptions, request cancellation, and missing console/request fields to confirm cleanup still occurs.
- Malicious input: create a controlled heap dump in a non-production environment and search for known test secrets, confirming avoidable copies are gone.