Skip to content

CWE-798: Hard-coded Credentials - Java

Overview

Hard-coded credentials in source code pose severe security risks. Credentials should never be embedded in code, configuration files committed to version control, or compiled into binaries. Use environment variables, secrets managers, or configuration services instead.

Primary Defence: Store credentials in environment variables, use cloud secrets managers (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault), or use configuration frameworks like Spring Boot's externalized configuration.

Common Vulnerable Patterns

Hard-coded Database Credentials

// VULNERABLE - Credentials in source code
public class DatabaseConnection {
    private static final String DB_URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String DB_USER = "admin";
    private static final String DB_PASSWORD = "P@ssw0rd123";  // DANGEROUS!

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

Why this is vulnerable: Hard-coded database credentials are visible to anyone with repository access, remain in git history permanently, are compiled into JAR files where they can be decompiled, and cannot be rotated without recompiling and redeploying the application.

Hard-coded API Keys

// VULNERABLE - API key in code
public class ApiClient {
    private static final String API_KEY = "sk_live_51H7x8y9z10a11b12c";  // DANGEROUS!
    private static final String API_SECRET = "whsec_abcdef123456";  // DANGEROUS!

    public void makeRequest() {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + API_KEY);
        // Make API call
    }
}

Why this is vulnerable: API keys in source code are exposed to all developers with repository access, persist in version control history, are visible in decompiled bytecode, and enable unauthorized API usage or billing charges if the code is leaked or the repository is compromised.

Hard-coded Encryption Keys

// VULNERABLE - Encryption key in code
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class Encryptor {
    private static final String SECRET_KEY = "MySecretKey12345";  // DANGEROUS!

    public byte[] encrypt(String data) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        return cipher.doFinal(data.getBytes());
    }
}

Why this is vulnerable: Hard-coded encryption keys defeat the purpose of encryption since anyone with code access can decrypt the data, keys cannot be rotated without recompiling, and stolen keys compromise all historical encrypted data permanently.

Credentials in Properties Files (Committed to Git)

# VULNERABLE - application.properties committed to version control

database.url=jdbc:mysql://localhost:3306/mydb
database.username=admin
database.password=P@ssw0rd123
aws.access.key=AKIAIOSFODNN7EXAMPLE
aws.secret.key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Why this is vulnerable: Properties files committed to git expose credentials permanently in version control history, remain accessible even after deletion, are cloned by every developer and CI/CD system, and are often deployed to production where they can be read from the filesystem or extracted from deployment artifacts.

Secure Patterns

Environment Variables

// SECURE - Read from environment variables
public class DatabaseConnection {
    private final String dbUrl;
    private final String dbUser;
    private final String dbPassword;

    public DatabaseConnection() {
        this.dbUrl = System.getenv("DB_URL");
        this.dbUser = System.getenv("DB_USER");
        this.dbPassword = System.getenv("DB_PASSWORD");

        if (dbUrl == null || dbUser == null || dbPassword == null) {
            throw new IllegalStateException("Database credentials not configured");
        }
    }

    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(dbUrl, dbUser, dbPassword);
    }
}

// Set environment variables:
// export DB_URL=jdbc:mysql://localhost:3306/mydb
// export DB_USER=admin
// export DB_PASSWORD=SecurePassword123

Why this works: Environment variables decouple credentials from source code, storing them at the OS/container/platform level. Credentials never enter version control and can be managed separately per environment (dev, staging, prod). The validation (throw new IllegalStateException) ensures fail-fast behavior if credentials are missing. Environment variables can be updated without code changes, supporting credential rotation and zero-downtime deployments.

Spring @Value with External Properties

// SECURE - Spring configuration from external properties
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ApiClient {

    @Value("${api.key}")
    private String apiKey;

    @Value("${api.secret}")
    private String apiSecret;

    public void makeRequest() {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + apiKey);

        // Make API call
    }
}

// application.properties (NOT committed to version control)
// api.key=${API_KEY}
// api.secret=${API_SECRET}

// application.yml
/*
api:
  key: ${API_KEY}
  secret: ${API_SECRET}
*/

Why this works: Spring injects values from external sources (environment variables, config server, secret stores) through placeholders, so credentials never live in the compiled application or source control. The ${API_KEY} syntax defers resolution until runtime, letting you supply different secrets per environment without code changes. Because injection fails if the values are missing, the service starts fast or not at all - no silent defaults. This approach pairs naturally with Spring Cloud Config, Vault, or container env vars, enabling central rotation and least-privilege access. Keeping secrets out of application.properties prevents accidental commits and reduces blast radius if the repository is leaked.

AWS Secrets Manager

SecretsManager.java
// 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;

public class SecretsManager {
    private final SecretsManagerClient client;
    private final ObjectMapper objectMapper;

    public SecretsManager() {
        this.client = SecretsManagerClient.create();
        this.objectMapper = new ObjectMapper();
    }

    public DatabaseCredentials getDatabaseCredentials() {
        GetSecretValueRequest request = GetSecretValueRequest.builder()
            .secretId("prod/database/credentials")
            .build();

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

        try {
            return objectMapper.readValue(secretString, DatabaseCredentials.class);
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse credentials", e);
        }
    }
}
DatabaseCredentials.java
public class DatabaseCredentials {
    private String username;
    private String password;
    private String host;
    private int port;

    // getters and setters
}
Maven Dependency
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>secretsmanager</artifactId>
    <version>2.20.0</version>
</dependency>

Why this works: AWS Secrets Manager provides centralized secret storage with encryption, automatic rotation, and fine-grained access control via IAM. Secrets are retrieved at runtime, not embedded in code. The SDK automatically uses IAM roles (EC2 instance profiles, ECS task roles) for authentication - no AWS credentials in code. Supports versioning, enabling gradual rollout of rotated secrets. CloudTrail logs all secret access for audit compliance.

HashiCorp Vault

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

@Component
public class VaultService {

    private final VaultTemplate vaultTemplate;

    public VaultService(VaultTemplate vaultTemplate) {
        this.vaultTemplate = vaultTemplate;
    }

    public String getDatabasePassword() {
        VaultResponse response = vaultTemplate.read("secret/data/database");

        if (response == null || response.getData() == null) {
            throw new RuntimeException("Failed to read secret from Vault");
        }

        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) response.getData().get("data");

        return (String) data.get("password");
    }
}
application.yml
spring:
  cloud:
    vault:
      host: vault.example.com
      port: 8200
      scheme: https
      authentication: TOKEN
      token: ${VAULT_TOKEN}
Maven Dependency
<dependency>
    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-core</artifactId>
    <version>2.3.2</version>
</dependency>

Why this works: HashiCorp Vault provides dynamic secrets, automatic lease management, and encryption as a service. The VAULT_TOKEN authenticates the application via environment variable (not hardcoded). Vault supports secret versioning, dynamic database credentials (auto-generated and auto-expired), and detailed audit logs. Spring Vault integration allows seamless access through dependency injection. Secrets can be rotated centrally without application changes.

Framework-Specific Guidance

Spring Boot

application.yml
// SECURE - Spring Boot configuration
spring:
  datasource:
    url: ${DB_URL:jdbc:mysql://localhost:3306/mydb}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

aws:
  credentials:
    access-key: ${AWS_ACCESS_KEY}
    secret-key: ${AWS_SECRET_KEY}
DatabaseConfig.java
// Configuration class
@Configuration
public class DatabaseConfig {

    @Value("${spring.datasource.url}")
    private String dbUrl;

    @Value("${spring.datasource.username}")
    private String dbUsername;

    @Value("${spring.datasource.password}")
    private String dbPassword;

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(dbUrl);
        config.setUsername(dbUsername);
        config.setPassword(dbPassword);
        config.setMaximumPoolSize(10);

        return new HikariDataSource(config);
    }
}
.env
// NOT committed - add to .gitignore
DB_URL=jdbc:mysql://localhost:3306/mydb
DB_USERNAME=admin
DB_PASSWORD=SecurePassword123
AWS_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCY


// Use environment variables or external config server
// For production: export DB_PASSWORD=$(aws secretsmanager get-secret-value ...)

Java EE / Jakarta EE

// SECURE - JNDI DataSource (configured in application server)
@Resource(lookup = "java:comp/env/jdbc/MyDB")
private DataSource dataSource;

public void queryDatabase() throws SQLException {
    try (Connection conn = dataSource.getConnection()) {
        // Use connection
    }
}
Credentials configured in server.xml or similar
<Resource name="jdbc/MyDB" 
          auth="Container"
          type="javax.sql.DataSource"
          username="${DB_USERNAME}"
          password="${DB_PASSWORD}"
          driverClassName="com.mysql.jdbc.Driver"
          url="${DB_URL}"
          maxTotal="20"
          maxIdle="10"
          maxWaitMillis="10000" />

Micronaut

DatabaseService.java
// SECURE - Micronaut configuration
import io.micronaut.context.annotation.Property;
import javax.inject.Singleton;

@Singleton
public class DatabaseService {

    private final String dbUrl;
    private final String dbUsername;
    private final String dbPassword;

    public DatabaseService(
        @Property(name = "datasources.default.url") String dbUrl,
        @Property(name = "datasources.default.username") String dbUsername,
        @Property(name = "datasources.default.password") String dbPassword
    ) {
        this.dbUrl = dbUrl;
        this.dbUsername = dbUsername;
        this.dbPassword = dbPassword;
    }
}
application.yml
datasources:
  default:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

External Configuration Files

// SECURE - Load config from external file NOT in version control
import java.io.FileInputStream;
import java.util.Properties;

public class ConfigLoader {

    public static Properties loadSecrets() {
        Properties props = new Properties();

        // Load from file outside project directory
        String configPath = System.getProperty("config.path", "/etc/myapp/secrets.properties");

        try (FileInputStream fis = new FileInputStream(configPath)) {
            props.load(fis);
        } catch (IOException e) {
            throw new RuntimeException("Failed to load configuration", e);
        }

        return props;
    }
}

// Usage:
Properties secrets = ConfigLoader.loadSecrets();
String dbPassword = secrets.getProperty("database.password");

// .gitignore - MUST include:
/*
secrets.properties
*.env
application-local.properties
*/

Encryption Keys Management

EncryptionService.java
// SECURE - Key management with AWS KMS
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.*;
import software.amazon.awssdk.core.SdkBytes;

public class EncryptionService {
    private final KmsClient kmsClient;
    private final String keyId;

    public EncryptionService() {
        this.kmsClient = KmsClient.create();
        this.keyId = System.getenv("KMS_KEY_ID");
    }

    public byte[] encrypt(String plaintext) {
        SdkBytes plaintextBytes = SdkBytes.fromUtf8String(plaintext);

        EncryptRequest request = EncryptRequest.builder()
            .keyId(keyId)
            .plaintext(plaintextBytes)
            .build();

        EncryptResponse response = kmsClient.encrypt(request);
        return response.ciphertextBlob().asByteArray();
    }

    public String decrypt(byte[] ciphertext) {
        SdkBytes ciphertextBytes = SdkBytes.fromByteArray(ciphertext);

        DecryptRequest request = DecryptRequest.builder()
            .ciphertextBlob(ciphertextBytes)
            .build();

        DecryptResponse response = kmsClient.decrypt(request);
        return response.plaintext().asUtf8String();
    }
}

Testing with Test Credentials

Manual verification steps

Search for hard-coded credentials

Scan codebase for passwords

# Search for hard-coded passwords
grep -ri "password.*=.*\"[^$]" src/ config/
grep -ri "secret.*=.*\"" src/
grep -ri "api[_-]key.*=.*\"" src/

# Search for JDBC URLs with credentials
grep -ri "jdbc:.*://.*:.*@" src/

# Search for AWS keys (pattern: AKIA...)
grep -ri "AKIA[0-9A-Z]\{16\}" src/

Verify environment variable usage

Ensure credentials come from env vars

# Check application reads from environment
grep -r "System.getenv\|@Value.*\$" src/

# Verify .env files are in .gitignore
grep "\.env" .gitignore

Test with Testcontainers (for integration tests)

// Use Testcontainers for isolated test database
import org.testcontainers.containers.PostgreSQLContainer;

PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
    .withDatabaseName("testdb")
    .withUsername("testuser")
    .withPassword("testpass");  // Test-only, not production

postgres.start();
String jdbcUrl = postgres.getJdbcUrl();
// Use for testing - no production credentials needed

Check Git history for leaked credentials

# Search Git history for passwords (use tools like gitleaks, truffleHog)
gitleaks detect --source . --verbose

# Or manually search history
git log -p | grep -i "password\|secret\|api_key"

Verify configuration files

Check no credentials in config

# Review application.properties, application.yml
grep -i "password\|secret" application.properties application.yml
# Should only see placeholders like ${DB_PASSWORD}

Automated verification

# Use secret scanning tools
gitleaks detect --source .
truffleHog git file://. --json

# SonarQube rule: S2068 (hard-coded credentials)
mvn sonar:sonar

.gitignore Best Practices

# Add these patterns to .gitignore

# Environment files

.env
.env.local
.env.*.local

# Configuration files with secrets

application-local.properties
application-local.yml
secrets.properties
config/secrets.yml

# IDE files (may contain credentials)

.idea/
*.iml
.vscode/

# AWS credentials

.aws/credentials

# Private keys

*.pem
*.key
*.p12
*.jks

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