Skip to content

CWE-522: Insufficiently Protected Credentials - Java

Overview

Insufficiently Protected Credentials in Java applications occurs when passwords, API keys, secret tokens, or other authentication credentials are stored in plaintext, weakly encrypted, hardcoded in source code, checked into version control, or transmitted insecurely. Java provides robust cryptographic libraries through the JCA (Java Cryptography Architecture), but developers must actively choose and properly configure secure password hashing algorithms like BCrypt, PBKDF2, or Argon2.

Primary Defence: Use BCrypt (Spring Security BCryptPasswordEncoder), Argon2, or PBKDF2 for password hashing with appropriate cost factors, and never store passwords in plaintext.

Common Vulnerable Patterns

Storing Passwords in Plaintext

// VULNERABLE - Plaintext password storage
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;  // Plaintext password field!

    // Getters and setters
}

@RestController
public class AuthController {
    @Autowired
    private UserRepository userRepository;

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegistrationRequest request) {
        User user = new User();
        user.setUsername(request.getUsername());
        // Storing password directly without hashing!
        user.setPassword(request.getPassword());

        userRepository.save(user);
        return ResponseEntity.ok().build();
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        User user = userRepository.findByUsername(request.getUsername());

        // Direct password comparison!
        if (user != null && user.getPassword().equals(request.getPassword())) {
            return ResponseEntity.ok(generateToken(user));
        }

        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}

Why this is vulnerable:

  • Anyone with database access (DBAs, backups, SQL injection, insiders) can read user passwords directly.
  • Password reuse means a single breach can compromise accounts elsewhere.

Hardcoded Credentials

// VULNERABLE - Credentials hardcoded in source
public class DatabaseConfig {
    // Hardcoded database credentials!
    private static final String DB_URL = "jdbc:postgresql://prod-db.company.com:5432/mydb";
    private static final String DB_USER = "admin";
    private static final String DB_PASSWORD = "SuperSecret123!";  // NEVER DO THIS!

    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
    }
}

@Configuration
public class ApiConfig {
    // Hardcoded API keys!
    public static final String API_KEY = "sk-live-abc123def456ghi789";
    public static final String SECRET_KEY = "my-secret-key-12345";

Why this is vulnerable:

  • Credentials are exposed to anyone with repo access and can leak via artifacts or decompiled bytecode.
  • Secrets linger in git history and require code changes to rotate.

Hardcoded API Keys in HTTP Clients

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate template = new RestTemplate();
        template.getInterceptors().add((request, body, execution) -> {
            // Hardcoded API key in interceptor
            request.getHeaders().add("Authorization", "Bearer " + API_KEY);
            return execution.execute(request, body);
        });
        return template;
    }
}

Weak Password Hashing (MD5/SHA-1)

// VULNERABLE - Using broken hash algorithms
import java.security.MessageDigest;
import java.util.Base64;

@Service
public class WeakAuthService {

    public String hashPassword(String password) throws Exception {
        // MD5 is cryptographically broken!
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] hash = md.digest(password.getBytes());
        return Base64.getEncoder().encodeToString(hash);
    }

    public boolean verifyPassword(String password, String hash) throws Exception {
        // Even with comparison, MD5 is too weak
        return hashPassword(password).equals(hash);
    }
}

// Also vulnerable with SHA-1
public String hashPasswordSHA1(String password) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SHA-1");
    byte[] hash = md.digest(password.getBytes());
    return Base64.getEncoder().encodeToString(hash);
}

Why this is vulnerable:

  • MD5 and SHA-1 are fast and broken, making brute-force practical with GPUs.
  • No built-in salt or work factor makes large-scale cracking cheap.

Credentials in application.properties

// VULNERABLE - application.properties committed to git
# application.properties (checked into version control!)
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin

Why this is vulnerable:

  • Secrets committed to version control are exposed to all repo users and persist in history.
  • Deployed configs can be read on disk or extracted from JARs.

Logging Sensitive Credentials

spring.datasource.password=password123
jwt.secret=my-super-secret-jwt-key
api.key=sk-live-abc123def456
aws.access.key=AKIAIOSFODNN7EXAMPLE
aws.secret.key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
// Application code loading from properties
@Configuration
public class AppConfig {
    @Value("${spring.datasource.password}")
    private String dbPassword;  // Exposed if properties in git

    @Value("${jwt.secret}")
    private String jwtSecret;  // Too simple and in version control
}

Insecure JWT Implementation

// VULNERABLE - Weak JWT secret and long expiration
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Service
public class InsecureJwtService {
    // Weak, hardcoded secret!
    private static final String SECRET_KEY = "secret";
    private static final long EXPIRATION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year!

    public String generateToken(User user) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);

        return Jwts.builder()
                .setSubject(user.getUsername())
                // Including password hash in token!
                .claim("password", user.getPasswordHash())
                .claim("role", user.getRole())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                // Weak secret makes tokens easy to forge
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }
}

Why this is vulnerable:

  • Weak, hardcoded secrets make tokens forgeable and hard to rotate.
  • Long expirations and sensitive claims increase damage if tokens leak.

HTTP Credential Transmission

// VULNERABLE - No HTTPS enforcement
@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
            .and()
            .formLogin();

        // No HTTPS requirement - credentials sent over HTTP!
        return http.build();
    }
}

// Application running on HTTP

Why this is vulnerable:

  • HTTP sends credentials in cleartext.
  • Anyone on the path (WiFi sniffers, ISPs, compromised routers) can intercept them.

Secure Patterns

BCrypt Password Encoding with Spring Security

// SECURE - Proper BCrypt implementation
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCrypt with strength 12 (recommended minimum)
        return new BCryptPasswordEncoder(12);
    }
}

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String passwordHash;  // Hashed password

    // No plain password field!
}

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public User registerUser(String username, String password) {
        User user = new User();
        user.setUsername(username);

        // Hash password with BCrypt (auto-salted)
        String passwordHash = passwordEncoder.encode(password);
        user.setPasswordHash(passwordHash);

        return userRepository.save(user);
    }

    public boolean verifyPassword(String username, String password) {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            // Generic error - don't reveal if user exists
            return false;
        }

        // BCrypt handles salt automatically
        return passwordEncoder.matches(password, user.getPasswordHash());
    }
}

Why this works:

  • BCrypt uses per-password salts and a tunable work factor, making offline cracking expensive.
  • Spring Security's PasswordEncoder handles hashing and verification with standard, portable BCrypt hashes.

Spring Vault Integration

// SECURE - HashiCorp Vault integration
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.VaultResponse;

@Configuration
@EnableVaultRepositories
public class VaultConfig extends AbstractVaultConfiguration {

    @Override
    public VaultEndpoint vaultEndpoint() {
        VaultEndpoint endpoint = new VaultEndpoint();
        endpoint.setHost(System.getenv("VAULT_HOST"));
        endpoint.setPort(Integer.parseInt(System.getenv("VAULT_PORT")));
        endpoint.setScheme("https");
        return endpoint;
    }

    @Override
    public ClientAuthentication clientAuthentication() {
        // Use token authentication (token from environment)
        return new TokenAuthentication(System.getenv("VAULT_TOKEN"));
    }
}

@Service
public class SecretService {

    @Autowired
    private VaultTemplate vaultTemplate;

    public Map<String, Object> getDatabaseCredentials() {
        VaultResponse response = vaultTemplate.read("secret/data/database/postgresql");
        return response.getData();
    }

    public String getApiKey(String serviceName) {
        VaultResponse response = vaultTemplate.read("secret/data/api-keys/" + serviceName);
        return (String) response.getData().get("key");
    }
}

@Configuration
public class DataSourceConfig {

    @Autowired
    private SecretService secretService;

    @Bean
    public DataSource dataSource() {
        Map<String, Object> dbCreds = secretService.getDatabaseCredentials();

        HikariConfig config = new HikariConfig();
        config.setJdbcUrl((String) dbCreds.get("url"));
        config.setUsername((String) dbCreds.get("username"));
        config.setPassword((String) dbCreds.get("password"));

        return new HikariDataSource(config);
    }
}

Why this works:

  • Vault keeps secrets out of code and config, with encryption at rest and audit logging.
  • Token auth + Vault policies enable least-privilege access and support rotation.

AWS Secrets Manager Integration

// SECURE - AWS Secrets Manager
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

@Service
public class AWSSecretsService {

    private final SecretsManagerClient client;
    private final ObjectMapper objectMapper;

    public AWSSecretsService() {
        this.client = SecretsManagerClient.builder()
                .region(Region.US_EAST_1)
                .build();
        this.objectMapper = new ObjectMapper();
    }

    public Map<String, String> getSecret(String secretName) {
        GetSecretValueRequest request = GetSecretValueRequest.builder()
                .secretId(secretName)
                .build();

        GetSecretValueResponse response = client.getSecretValue(request);
        String secretString = response.secretString();

        try {
            return objectMapper.readValue(secretString, Map.class);
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse secret", e);
        }
    }
}

@Configuration
public class DatabaseConfig {

    @Autowired
    private AWSSecretsService secretsService;

    @Bean
    public DataSource dataSource() {
        Map<String, String> dbSecret = secretsService.getSecret("prod/database");

        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(dbSecret.get("url"));
        config.setUsername(dbSecret.get("username"));
        config.setPassword(dbSecret.get("password"));
        config.setMaximumPoolSize(10);

        return new HikariDataSource(config);
    }
}

Why this works:

  • Secrets are encrypted with KMS and fetched at runtime using IAM roles, so no embedded credentials.
  • Built-in rotation, versioning, and CloudTrail logging reduce exposure and aid auditing.

Secure JWT Implementation

// SECURE - Proper JWT with strong secret
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.UUID;

@Service
public class JwtService {

    private final SecretKey secretKey;
    private static final long ACCESS_TOKEN_VALIDITY = 3600000; // 1 hour
    private static final long REFRESH_TOKEN_VALIDITY = 2592000000L; // 30 days
    private static final String ISSUER = "https://api.example.com";
    private static final String AUDIENCE = "example-app";

    @Autowired
    public JwtService(SecretService secretService) {
        // Load secret from secure storage, not hardcoded
        String secretString = secretService.getSecret("jwt/secret-key");
        byte[] keyBytes = Base64.getDecoder().decode(secretString);
        this.secretKey = Keys.hmacShaKeyFor(keyBytes);
    }

    // Method to generate strong secret key (run once, store in vault)
    public static String generateSecretKey() {
        SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
        return Base64.getEncoder().encodeToString(key.getEncoded());
    }

    public String createAccessToken(String username, String role) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + ACCESS_TOKEN_VALIDITY);

        return Jwts.builder()
                .setSubject(username)
                .setIssuer(ISSUER)
                .setAudience(AUDIENCE)
                .setId(UUID.randomUUID().toString())
                .claim("role", role)
                // Don't include sensitive data!
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(secretKey, SignatureAlgorithm.HS512)
                .compact();
    }

    public String createRefreshToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + REFRESH_TOKEN_VALIDITY);

        return Jwts.builder()
                .setSubject(username)
                .setIssuer(ISSUER)
                .setAudience(AUDIENCE)
                .setId(UUID.randomUUID().toString())
                .claim("type", "refresh")
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(secretKey, SignatureAlgorithm.HS512)
                .compact();
    }

    public Claims validateToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .requireIssuer(ISSUER)
                    .requireAudience(AUDIENCE)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new TokenExpiredException("Token has expired");
        } catch (JwtException e) {
            throw new InvalidTokenException("Invalid token");
        }
    }
}

Why this works:

  • Strong, vault-stored HMAC keys and short-lived access tokens reduce compromise impact.
  • Issuer/audience validation plus jti enable stricter validation and revocation.

HTTPS Enforcement

// SECURE - Force HTTPS in Spring Boot
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Require HTTPS for all requests
            .requiresChannel(channel -> channel
                .anyRequest().requiresSecure()
            )
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());

        return http.build();
    }
}

// application.properties for production
# server.port=8443
# server.ssl.key-store=classpath:keystore.p12
# server.ssl.key-store-password=${SSL_KEY_STORE_PASSWORD}
# server.ssl.key-store-type=PKCS12
# server.ssl.key-alias=tomcat

// Redirect HTTP to HTTPS
@Configuration
public class HttpsRedirectConfig {

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };

        tomcat.addAdditionalTomcatConnectors(createHttpConnector());
        return tomcat;
    }

    private Connector createHttpConnector() {
        Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(8443);
        return connector;
    }
}

Why this works:

  • HTTPS encrypts credentials in transit and prevents cleartext submission.
  • Enforced redirects and container-level constraints ensure HTTP is not accepted.

Argon2 Password Encoding

// SECURE - Argon2 (most secure option)
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;

@Configuration
public class Argon2Config {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Argon2 with recommended parameters
        return new Argon2PasswordEncoder(
            16,    // salt length
            32,    // hash length
            1,     // parallelism
            65536, // memory (64 MB)
            3      // iterations
        );
    }
}

Why this works:

  • Argon2 is memory-hard and resists GPU/ASIC cracking better than legacy hashes.
  • Tunable parameters and PHC-formatted hashes support future upgrades.

Environment-Based Configuration

// SECURE - External configuration
// application.properties (safe defaults, no secrets)
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
jwt.secret=${JWT_SECRET}

// Deployment configuration (not in code)
// Set via environment variables, Kubernetes secrets, or AWS Parameter Store
# export DB_URL=jdbc:postgresql://localhost:5432/mydb
# export DB_USERNAME=appuser
# export DB_PASSWORD=<from-secrets-manager>
# export JWT_SECRET=<from-vault>

@Configuration
public class AppConfig {

    @Value("${jwt.secret}")
    private String jwtSecret;  // Loaded from environment at runtime

    // Validation on startup
    @PostConstruct
    public void validateConfig() {
        if (jwtSecret == null || jwtSecret.length() < 32) {
            throw new IllegalStateException("JWT secret must be at least 32 characters");
        }
    }
}

Why this works:

  • Secrets stay out of code and repos by using environment-injected values.
  • Deployment secret stores and startup validation reduce misconfiguration risk.

Verification

To verify credentials are properly protected:

  • Check password storage: Verify passwords in the database are hashed (should start with $2a$, $2b$, or $argon2 prefix), not plaintext
  • Confirm salt usage: Same passwords for different users should produce different hashes
  • Verify configuration: Ensure application.properties, application.yml, and other config files use environment variables or secret references (e.g., ${DB_PASSWORD}), not hardcoded values
  • Test JWT expiration: Tokens should have reasonable expiration times (typically 15-60 minutes for access tokens)
  • Review source code: Search codebase for hardcoded secrets, API keys, passwords, or connection strings
  • Test authentication: Verify login works with correct credentials and fails with incorrect ones
  • Scan with tools: Run static analysis tools to detect hardcoded credentials and weak hashing algorithms

Additional Resources