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;
}
}
Why this is vulnerable: String exposes no way to overwrite its contents, so the value stays readable until the object is collected and the memory happens to be reused - a delay measured in whatever the collector decides, not in what the code does. Worse, a moving collector copies live objects during compaction, so one logical password can leave several physical copies at addresses the program never held a reference to. Nothing the application does afterwards can reach them.
The API choice is upstream of the problem. Scanner.nextLine() can only return a String; System.console().readPassword() returns a char[] for exactly this reason, and reading through it means there is something to clear.
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();
}
}
Why this is vulnerable: A field keeps a strong reference for the object's whole lifetime, so a long-lived client holds the key for the life of the process and no collection cycle will touch it.
The realistic disclosure path is not an exploit. It is jmap, a container OOM with -XX:+HeapDumpOnOutOfMemoryError set, or an APM agent's snapshot - routine diagnostic artefacts that get attached to tickets, copied to workstations and kept far longer than the process ran. A heap dump is a plaintext search of every String on the heap, and it needs no vulnerability to obtain.
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);
}
}
Why this is vulnerable: The parameterized {} form is the one SLF4J recommends, because it skips building the message when the level is disabled - which is exactly what makes this look like the careful version rather than the careless one. The deferral is only of formatting. The password is still evaluated, still passed, and still retained by the varargs array until the call returns, and if any environment turns debug on the string is written out.
What follows the write is the larger problem. Log files are shipped to an aggregator, indexed, replicated and retained under a policy chosen for operations rather than for secrets, so a credential written once at debug outlives both the request and, usually, the password.
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;
}
}
Why this is vulnerable: JPasswordField.getPassword() returns a fresh array on every call, and its javadoc says outright to zero it after use - the API was designed around clearing, and this code drops the array on the floor instead.
The line above it undoes the point anyway. new String(password) copies the characters into an immutable object that cannot be cleared, so even a correct Arrays.fill() afterwards would leave the plaintext behind in the String. A char[] is only worth using if nothing converts it.
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');
}
}
Why this is vulnerable: After the conversion there are two copies, one of which is beyond reach, so clearing the array is theatre. The interesting question is why the conversion is there at all, and usually the answer is that some API downstream demanded it - PreparedStatement.setString() and String.getBytes() both do.
That makes the fix an API-selection problem rather than a memory-hygiene one. Prefer consumers that accept char[] or CharSequence so the conversion never has to happen, and where the ecosystem offers nothing that does, say so in the finding: an exposure that cannot be closed is worth recording accurately rather than papering over with a clear that runs too late.
Secure Patterns
Using char[] for passwords with explicit clearing
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
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) {
// Encode with a real charset. Casting each char to a byte would
// truncate anything outside Latin-1 - see the note below.
byte[] passwordBytes = toUtf8Bytes(password);
try {
// Hash and verify
byte[] hash = hashPassword(passwordBytes);
return compareHashes(hash, getStoredHash(username));
} finally {
// Clear temporary byte array
Arrays.fill(passwordBytes, (byte) 0);
}
}
/** char[] to UTF-8 bytes without an intermediate String, clearing the buffer. */
private static byte[] toUtf8Bytes(char[] password) {
ByteBuffer buffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(password));
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// The encoder's backing array holds a copy; zero it before releasing
Arrays.fill(buffer.array(), (byte) 0);
return bytes;
}
}
Why this works:
char[]is mutable and clearable:Arrays.fill()zeroes the contents in place- 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
- The API was designed for it:
JPasswordField.getPassword()has returnedchar[]rather thanStringsince JDK 1.2
Do not convert with (byte) password[i]. It is the obvious way to get bytes
without building a String, and it silently truncates every character above
U+00FF to its low byte. Two different passwords then hash identically:
"café" -> [99, 97, 102, -23]
"cafǩ" -> [99, 97, 102, -23] same bytes, so either password authenticates
StandardCharsets.UTF_8.encode(CharBuffer.wrap(password)) avoids the
truncation and still avoids the String, which is the whole point of holding a
char[]. Its backing array is a copy of the secret, so zero it before letting
it go, as toUtf8Bytes does above.
Secure key management with AutoCloseable
import java.nio.ByteBuffer;
import java.security.Key;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
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");
// Generate an explicit 12-byte IV. Calling init() without a GCMParameterSpec
// lets the provider pick one internally; it is then only reachable via
// cipher.getIV(), and if it is not returned with the ciphertext the output
// can never be decrypted.
byte[] iv = new byte[12];
SecureRandom.getInstanceStrong().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(128, iv));
byte[] ciphertext = cipher.doFinal(plaintext);
// Prepend the IV so the caller can decrypt. The GCM tag is already appended
// to the ciphertext by doFinal(), so it does not need separate handling here.
return ByteBuffer.allocate(iv.length + ciphertext.length)
.put(iv)
.put(ciphertext)
.array();
}
@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:
AutoCloseableties cleanup to the block: The key is cleared even whenencrypt()throws- Deterministic cleanup timing: The key is zeroed when the block exits, not whenever the collector next runs
- Mutable
byte[]storage:Arrays.fill()zeroes the key bytes in place clearedflag fails fast: Using the key afterclose()throwsIllegalStateException- 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()returnschar[], notString: The array can be zeroed after use- Prevents shoulder-surfing:
readPassword()disables echo, so characters do not 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) - Recommended by Oracle: The Secure Coding Guidelines name
readPassword()as the way to take a password at the command line
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: A
Stringcopy of the password ends up on the heap, where it cannot be cleared. That is unavoidable here, but not for the reason it is usually given:PasswordEncoderaccepts aCharSequence, not aString-encode(CharSequence)andmatches(CharSequence, String)- so wrapping the array withCharBuffer.wrap(password)compiles and looks like it solves the problem. It does not.BCryptPasswordEncodercallsrawPassword.toString()internally before hashing, so theStringis created inside Spring Security whether or not your code creates one. Wrapping moves the allocation, it does not remove it. For stricter memory-handling requirements the answer is a different hashing API - one takingbyte[], or PBKDF2/Argon2 driven directly with clearable arrays - not a different way of calling this one.
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: The char[] is cleared with Arrays.fill(), but a String copy of the password reaches the heap and cannot be cleared - it may appear in heap dumps until garbage collection and memory reuse happen to overwrite it. The example creates that String explicitly so the cost is visible at the call site rather than hidden inside BCryptPasswordEncoder. Clearing the array still removes one copy and is worth doing. BCrypt remains strong password hashing; it does not solve the memory-residency half of the problem. For applications with strict memory-security requirements, use a hashing API that accepts byte[], drive PBKDF2 or Argon2 directly with clearable arrays, or record that the HTTP framework already delivered the password as a String before your code saw it - in which case this whole exercise is protecting a copy that was never the only one.
JWT token handling with secure storage
// jjwt 0.12+ API. The 0.11 spellings do not compile here: parserBuilder() was
// removed outright, and setSubject()/signWith(Key, SignatureAlgorithm) are
// deprecated in favour of subject() and Jwts.SIG.
import io.jsonwebtoken.Jwts;
import java.util.Arrays;
import javax.crypto.SecretKey;
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. HS256 requires at least 32 bytes;
// a shorter one is rejected at signing time, not at construction.
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
SecretKey key = new SecretKeySpec(secretKey, "HmacSHA256");
// Generate JWT
return Jwts.builder()
.subject(subject)
.signWith(key, Jwts.SIG.HS256)
.compact();
}
public String validateToken(String token) {
if (cleared) {
throw new IllegalStateException("Secret key has been cleared");
}
SecretKey key = new SecretKeySpec(secretKey, "HmacSHA256");
// verifyWith() fixes the key before the token is read, which rejects an
// unsecured token (alg: none) and one signed with a different key type.
// It does not pin the exact MAC algorithm - see the note below.
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload()
.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:
signWith(key, Jwts.SIG.HS256)names the algorithm in code, andverifyWith(key)fixes the key before the token is parsed. Measured on jjwt 0.12.6 and 0.13.0, analg: nonetoken is refused withUnsupportedJwtExceptionand one signed with a different key withSignatureException - Key size is checked at signing: jjwt refuses to produce an HS256 signature with a key under 256 bits (RFC 7518 section 3.2), so a truncated or misconfigured secret fails loudly rather than producing a weak token
- Fail-fast enforcement:
clearedflag throws exceptions if tokens created/validated after clearing; outerfinallyclears original secret for defense-in-depth
What verifyWith(key) does not do is pin the exact MAC algorithm. jjwt takes
the algorithm from the token's own alg header and only requires that the key
suits it, so on the same secret a token minted as HS512 verifies against this
handler and getSubject() returns the attacker's subject - measured on both
0.12.6 and 0.13.0. It is a narrow gap, since forging the token still needs the
secret, but it means "the signature verified" is not the same statement as "the
token was issued the way we issue tokens". Where that distinction matters, keep
the parsed Jws<Claims> and assert on it before trusting the claims:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
Jws<Claims> jws = Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
if (!"HS256".equals(jws.getHeader().getAlgorithm())) {
throw new SecurityException("Unexpected token algorithm");
}
return jws.getPayload().getSubject();
Servlet authentication with secure password handling
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.UUID;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
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 static final int ITERATIONS = 600_000; // OWASP Password Storage Cheat Sheet
private static final int KEY_BITS = 256;
// A real stored record for a user nobody knows, verified against when the
// username does not exist so both branches cost the same.
private static final StoredCredential DUMMY =
newCredential(UUID.randomUUID().toString().toCharArray());
record StoredCredential(byte[] salt, byte[] hash) {}
private boolean authenticateUser(String username, char[] password) {
// null when the user does not exist
StoredCredential stored = getUserCredential(username);
StoredCredential target = (stored != null) ? stored : DUMMY;
// Derive unconditionally. Returning early on a missing user would
// answer in microseconds where a real derivation takes ~160 ms.
byte[] inputHash = pbkdf2(password, target.salt());
try {
boolean matched = java.security.MessageDigest.isEqual(inputHash, target.hash());
return stored != null && matched;
} finally {
// Clear temporary hash
Arrays.fill(inputHash, (byte) 0);
}
}
/** PBKDF2 is one of the few JDK APIs that takes a char[] - see Considerations. */
private static byte[] pbkdf2(char[] password, byte[] salt) {
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_BITS);
try {
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
.generateSecret(spec)
.getEncoded();
} catch (java.security.GeneralSecurityException e) {
throw new IllegalStateException("PBKDF2 unavailable", e);
} finally {
// PBEKeySpec copies the array in its constructor, so this clears
// its copy. The caller still has to clear the array it passed in.
((PBEKeySpec) spec).clearPassword();
}
}
private static StoredCredential newCredential(char[] password) {
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
return new StoredCredential(salt, pbkdf2(password, salt));
}
}
Why this works:
- Immediate clearing: Converts password parameter to
char[]viatoCharArray()and clears infinally, minimizing cleartext time (original string in request object may persist) PBEKeySpecis the API that makes thechar[]worth holding: it is one of the few JDK entry points that accepts achar[]rather than aString, so the password reaches the KDF without ever being converted.clearPassword()zeroes the copyPBEKeySpecmade; the servlet's ownfinallyzeroes the one it passed in. Two owners, two clears- A password KDF with a per-user salt, not a bare digest: the salt is stored with the hash rather than fixed in code, so identical passwords hash differently. A single unsalted
MessageDigestpass over the password would be fast enough to attack offline at GPU speed, which is the property a KDF exists to remove - Timing-resistant comparison:
MessageDigest.isEqual()avoids the early-exit behavior of ordinary array equality - The unknown user costs the same as the known one: deriving against
DUMMYrather than returning early keeps both branches at the same ~160 ms, and also removes theNullPointerExceptiona missing record would otherwise produce - which would answer 500 where a real failure answers 401, the loudest possible enumeration oracle. Rate-limit the endpoint, since every attempt now runs the KDF - Servlet/JSP context: Important for applications where passwords arrive as HTTP POST parameters (strings) but should convert to clearable arrays immediately
Considerations
This is a mitigation, not an elimination, and the difference matters when deciding how far to go. A managed runtime gives you no way to guarantee a secret is gone: the garbage collector copies values as it compacts, immutable strings cannot be overwritten at all, pages may be written to swap, and a crash dump captures whatever happens to be resident. Clearing buffers shortens the window an attacker with memory access must hit. It does not close it. Say which you are buying before spending much effort.
The boundary is the API you have to call. Holding a credential in a mutable buffer only helps if everything downstream accepts one. The moment a library requires a string, the conversion creates a copy you cannot clear, and the care taken upstream buys almost nothing. Judge by whether the whole path can avoid the conversion; if it cannot, spend the effort on the operational controls instead.
Whether char[] helps depends entirely on the consuming API. It is genuinely
useful where the library takes one - PBEKeySpec and KeyStore.load both do,
and both let you clear the array afterwards. It buys nothing where the API takes
a String, which includes JDBC's DriverManager.getConnection: the conversion
creates an immutable copy that lives until collected. Check the signature you
have to call before restructuring the code above it.
String literals are never collected. A credential written as a literal in source is interned in the constant pool for the life of the JVM, so no amount of careful handling downstream affects it. That is a hard-coded-credential problem (CWE-798) wearing this CWE's clothes, and the fix is to remove the literal.
Most of the real exposure is operational rather than in the code. Whether process dumps are enabled, whether swap is encrypted, whether the host is shared, how long worker processes live, and whether debuggers can attach in production will usually change the risk more than any in-process buffer handling. If you can only do one thing, restricting dump generation and shortening process lifetime tends to beat clearing arrays.
The strongest version of this fix is not holding the secret at all. Fetching a credential from a vault at the point of use, keeping it for the shortest span the operation needs, and letting the platform hold anything long-lived removes the question rather than managing it.
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.