Skip to content

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 with Arrays.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
  • finally ensures 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() returns char[] 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:

  • AutoCloseable enables 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
  • cleared flag prevents use-after-free: Fail-fast behavior throws exceptions if key used after clearing
  • Creates SecretKeySpec per 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 clearable char[]: Unlike String, arrays can be zeroed after use
  • Echo disabled prevents shoulder-surfing: Characters don't appear on screen during entry
  • finally ensures 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+ Cleaner can register a Runnable to zero credential bytes when the object becomes phantom-reachable, but timing still depends on garbage collection
  • Separate state management: State class holds credential bytes, registered separately from main object; cleanup action runs even after SecureCredential collected
  • Dual cleanup: Explicit close() for deterministic try-with-resources cleanup; cleaner provides safety net if close() forgotten; cleanable.clean() ensures single execution
  • Clearable storage: byte[] (copied with Arrays.copyOf() to prevent external modification) enables in-place clearing with Arrays.fill()
  • Prefer explicit close: Cleaner avoids some finalize() problems, but explicit close() remains the control that gives predictable cleanup timing

Spring Security with char[] passwords (compromise)

Security limitation: This pattern converts char[] to String for BCrypt, which undermines the memory security benefit of using char[] because the immutable String persists on the garbage-collected heap and cannot be cleared. This is a practical compromise when using BCrypt libraries that only accept String parameters. For stricter memory-handling requirements, consider password-hashing APIs that accept byte[] 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 with Arrays.fill(secretKey, (byte) 0)
  • Deterministic cleanup: AutoCloseable with try-with-resources ensures cleanup even if exceptions occur during token creation/validation
  • Transient Key objects: Creates SecretKeySpec per 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: cleared flag throws exceptions if tokens created/validated after clearing; outer finally clears 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[] via toCharArray() and clears in finally, 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[] to byte[] 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

  1. Locate sensitive values held in String, static fields, caches, request objects, and long-lived service objects.
  2. Trace input boundaries such as Swing, console, servlet, Spring MVC, and configuration loaders to identify unavoidable string copies.
  3. Use char[] or byte[] where APIs support them, and clear arrays with Arrays.fill(...) in finally blocks or AutoCloseable.close().
  4. Treat Cleaner as a safety net only; use try-with-resources for deterministic cleanup.
  5. Replace logging, exception, and debugging output that includes passwords, keys, tokens, or derived secrets.
  6. 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.

Additional Resources