Use of Inner Class Containing Sensitive Data

Description

Use of Inner Class Containing Sensitive Data is a vulnerability where inner classes are used to hold or access sensitive information. Java's compiler transforms inner classes into separate peer classes with package-level access, potentially exposing code and data the programmer intended to keep private. When inner classes access private fields in their enclosing class, the compiler may convert those private fields into protected or package-private fields, creating security vulnerabilities. This occurs because Java bytecode has no concept of inner classes - they are purely a source-level construct.

Risk

Inner classes create hidden security risks because their compiled form differs significantly from their source representation. Private data accessed by inner classes becomes accessible to any class in the same package. Attackers can create classes in the same package to access supposedly private fields and methods exposed through inner class compilation. Sensitive data such as credentials, cryptographic keys, or personal information stored in inner classes or accessed by them from enclosing classes can be read or modified by malicious code. The risk is amplified because developers often assume inner class access modifiers provide security guarantees they do not actually provide.

Solution

Avoid using inner classes to store or access sensitive data. Use sealed classes where available to protect encapsulation. Make inner classes static when they don't need access to enclosing class instance members, as static inner classes have more predictable access semantics. Consider using local inner classes defined within methods when the class is only needed in a specific scope. For truly sensitive operations, use separate classes with proper encapsulation rather than relying on inner class access restrictions. Never assume that inner classes provide security isolation.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Inner class data confidentiality can be overcome, allowing attackers to read sensitive application data through the exposed package-level access.
IntegrityScope: Integrity

Modify Application Data - Attackers can modify data in inner classes or data accessed by inner classes from outer classes through the exposed access paths.

Example Code

Vulnerable Code

// Vulnerable: Inner class accessing private sensitive data
public class VulnerableBankingSystem {
    // Private field - but inner class access makes it package-accessible
    private String masterEncryptionKey = "super-secret-key-12345";
    private Map<String, Double> accountBalances = new HashMap<>();

    // Vulnerable: Non-static inner class has access to all private members
    public class TransactionProcessor {
        public void processTransaction(String accountId, double amount) {
            // Inner class can access private outer class fields
            // After compilation, masterEncryptionKey becomes package-accessible
            String encrypted = encrypt(amount, masterEncryptionKey);

            // accountBalances is also exposed
            double currentBalance = accountBalances.get(accountId);
            accountBalances.put(accountId, currentBalance + amount);
        }

        private String encrypt(double amount, String key) {
            // Encryption logic
            return "" + amount;
        }
    }
}

// Attacker in same package can exploit the exposed fields
package com.banking; // Same package as VulnerableBankingSystem

public class Attacker {
    public void exploit() {
        VulnerableBankingSystem system = new VulnerableBankingSystem();

        // Due to inner class compilation, private fields are now accessible
        // through reflection or synthetic accessor methods
        try {
            Field keyField = VulnerableBankingSystem.class
                .getDeclaredField("masterEncryptionKey");
            keyField.setAccessible(true);  // Made easier by inner class access
            String key = (String) keyField.get(system);
            System.out.println("Stolen key: " + key);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// Vulnerable: Applet with inner class exposing sensitive data
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;

public class VulnerableApplet extends Applet {
    // Private sensitive data
    private String userPassword;
    private String sessionToken;

    // Vulnerable: Anonymous inner class for event handling
    // accesses private fields, exposing them at bytecode level
    public void init() {
        Button submitButton = new Button("Submit");

        // Vulnerable: Anonymous inner class
        submitButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // Accessing private field exposes it in bytecode
                authenticateUser(userPassword);
                // sessionToken also becomes accessible
                startSession(sessionToken);
            }
        });

        add(submitButton);
    }

    private void authenticateUser(String password) {
        // Authentication logic
    }

    private void startSession(String token) {
        // Session logic
    }
}

// Vulnerable: Class with sensitive inner class data
public class VulnerableUserManager {

    // Vulnerable: Inner class holding sensitive user data
    private class UserCredentials {
        String username;
        String password;
        String ssn;
        String creditCardNumber;

        UserCredentials(String user, String pass, String ssn, String cc) {
            this.username = user;
            this.password = pass;
            this.ssn = ssn;
            this.creditCardNumber = cc;
        }
    }

    private UserCredentials currentUser;

    public void login(String user, String pass, String ssn, String cc) {
        // Vulnerable: Sensitive data in inner class
        // is more accessible than it appears in source
        currentUser = new UserCredentials(user, pass, ssn, cc);
    }
}

Fixed Code

// Fixed: Use static inner class or separate class
public class SecureBankingSystem {
    private final String masterEncryptionKey;
    private final Map<String, Double> accountBalances;

    public SecureBankingSystem() {
        this.masterEncryptionKey = loadKeyFromSecureStorage();
        this.accountBalances = new ConcurrentHashMap<>();
    }

    // Fixed: Static inner class doesn't have implicit access to outer class
    public static class TransactionProcessor {
        private final EncryptionService encryptionService;
        private final AccountRepository accountRepository;

        public TransactionProcessor(EncryptionService encryption,
                                   AccountRepository accounts) {
            this.encryptionService = encryption;
            this.accountRepository = accounts;
        }

        public void processTransaction(String accountId, double amount) {
            // Fixed: Uses injected dependencies, not outer class fields
            String encrypted = encryptionService.encrypt(amount);
            accountRepository.updateBalance(accountId, amount);
        }
    }

    // Fixed: Factory method provides controlled access
    public TransactionProcessor createProcessor() {
        return new TransactionProcessor(
            new EncryptionService(masterEncryptionKey),
            new AccountRepository(accountBalances)
        );
    }

    private String loadKeyFromSecureStorage() {
        // Load from HSM or secure vault
        return SecureKeyVault.getKey("master-key");
    }
}
// Fixed: Use local inner class for limited scope
public class SecureApplet extends Applet {
    private char[] userPassword;  // char[] for passwords, not String

    public void init() {
        Button submitButton = new Button("Submit");

        // Fixed: Method handles authentication directly
        submitButton.addActionListener(this::handleSubmit);

        add(submitButton);
    }

    // Fixed: Implement ActionListener directly on the class
    private void handleSubmit(ActionEvent e) {
        try {
            // Process password locally
            authenticateUser(userPassword);
        } finally {
            // Clear sensitive data after use
            Arrays.fill(userPassword, '\0');
        }
    }

    private void authenticateUser(char[] password) {
        // Authentication logic
    }
}

// Alternative: Implement interface directly
public class SecureApplet2 extends Applet implements ActionListener {
    private char[] userPassword;

    public void init() {
        Button submitButton = new Button("Submit");
        submitButton.addActionListener(this);
        add(submitButton);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        // Fixed: No inner class, direct implementation
        authenticateUser(userPassword);
    }

    private void authenticateUser(char[] password) {
        // Authentication logic
    }
}
// Fixed: Separate class for sensitive data with proper encapsulation
public final class SecureUserManager {

    // Fixed: Credentials stored in separate, sealed class
    private SecureCredentials currentUser;

    public void login(String user, char[] pass) {
        // Fixed: Use properly encapsulated credential object
        currentUser = SecureCredentials.create(user, pass);
        // Clear password array after use
        Arrays.fill(pass, '\0');
    }

    public boolean isAuthenticated() {
        return currentUser != null && currentUser.isValid();
    }
}

// Fixed: Separate final class for credentials (not an inner class)
public final class SecureCredentials {
    private final String username;
    private final byte[] hashedPassword;
    private final Instant createdAt;

    private SecureCredentials(String username, byte[] hashedPassword) {
        this.username = username;
        this.hashedPassword = hashedPassword;
        this.createdAt = Instant.now();
    }

    public static SecureCredentials create(String username, char[] password) {
        byte[] hashed = hashPassword(password);
        // Clear original password
        Arrays.fill(password, '\0');
        return new SecureCredentials(username, hashed);
    }

    public boolean isValid() {
        // Check if credentials haven't expired
        return createdAt.plusSeconds(3600).isAfter(Instant.now());
    }

    public boolean verifyPassword(char[] password) {
        byte[] hashed = hashPassword(password);
        boolean matches = MessageDigest.isEqual(hashedPassword, hashed);
        Arrays.fill(hashed, (byte) 0);
        return matches;
    }

    private static byte[] hashPassword(char[] password) {
        // Use proper password hashing (bcrypt, Argon2, etc.)
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] bytes = new String(password).getBytes(StandardCharsets.UTF_8);
            byte[] hash = md.digest(bytes);
            Arrays.fill(bytes, (byte) 0);
            return hash;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the vulnerability pattern is documented in:

  • Seven Pernicious Kingdoms taxonomy
  • Java bytecode security research

References

  1. MITRE Corporation. "CWE-492: Use of Inner Class Containing Sensitive Data." https://cwe.mitre.org/data/definitions/492.html
  2. Oracle. "Secure Coding Guidelines for Java SE."
  3. McGraw, Gary and Felten, Edward. "Securing Java."