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
PasswordEncoderhandles 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
jtienable 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$argon2prefix), 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