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 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 with Arrays.fill() to remove from memory
  • Prevents password persistence: Clearing eliminates data from memory dumps and debugger inspection
  • finally ensures 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() 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;
        }
    }

    @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:

  • 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

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 reliability: Java 9+ Cleaner (replaces finalize()) registers Runnable zeroing credential bytes when object phantom-reachable (eligible for GC)
  • 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()
  • Superior to finalize(): Cleaner actions execute on dedicated thread with better guarantees vs finalize() which had no run guarantees

COMPROMISE WARNING: Spring Security with char[] passwords

** 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 necessary compromise when using BCrypt libraries that only accept String parameters. For maximum security, consider using bcrypt implementations that accept byte[] 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 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 Arrays.equals(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)
  • Constant-time comparison: Arrays.equals() prevents timing attacks that could leak password correctness via response time differences
  • 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

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

Additional Resources