Skip to content

CWE-798: Use of Hard-coded Credentials

Overview

Hard-coded credentials occur when authentication secrets (passwords, API keys, encryption keys, tokens) are embedded directly in source code, configuration files, or binaries. This violates the principle of separation of code and configuration: credentials become visible to anyone with code or artifact access, changing them requires redeployment, and committed secrets persist in version control history unless the history is deliberately rewritten and all copies are handled.

The weakness has two halves, and only one of them is fixed by a secrets manager. MITRE splits it by the direction the credential travels:

  • Outbound - the product holds a credential it sends to something else: a database password, an API key, a service account, a private key. This is the common case, the one scanners report, and the one the remediation below is mostly about. The fix is to fetch the value at runtime from a managed store.
  • Inbound - the product holds a credential it accepts: a login handler comparing the submitted password against a literal, a support or diagnostic account created at install, a licence or activation check, a device password shipped identical across every unit. Here there is no external store to move the value to, because the value is not a secret the product needs - it is an authenticator the product should never have had. Moving it into Key Vault changes nothing: an attacker who reads the shipped binary or the vendor documentation still authenticates.

The inbound half is the one usually missed, and it is the one that produces the more severe finding, because a hard-coded comparison is an authentication bypass available to anyone who can reach the endpoint - no repository access required. Obfuscating it, hashing it, or comparing against a stored digest of a fixed value does not help: the credential is still the same on every installation, so compromising one instance compromises all of them. See Remove credentials the product itself accepts.

Relationship to Other CWEs

A hard-coded password is better reported against CWE-259 and a hard-coded key against CWE-321. This page is the right level when the credential is neither - an API key, a bearer token, a connection string - or when one finding covers several kinds at once. MITRE marks CWE-798 ALLOWED-WITH-REVIEW for that reason: "it has lower-level children that cover specific kinds of credentials", with the comment "Consider children such as CWE-259 and CWE-321". A finding whose number came from a CVE record may arrive citing CWE-287 (Improper Authentication) as the parent, which it is only in MITRE's simplified mapping view.

The pages around it differ by what kind of credential is hard-coded, and by where a half-finished fix leaves it:

  • CWE-798 (this page) - any credential embedded as a literal in source, configuration, or a build artifact, whether the product sends it outbound or accepts it inbound
  • CWE-259 (Use of Hard-coded Password) - the child to report when the credential is a password. The secrets-manager, rotation and history-cleanup mechanics stay here; that page carries what is specific to passwords
  • CWE-321 (Use of Hard-coded Cryptographic Key) - the child to report when it is key material. Worth separating because replacing a key also means re-encrypting or re-wrapping everything it protected, which swapping an API credential does not
  • CWE-615 (Inclusion of Sensitive Information in Source Code Comments) - MITRE lists no relationship between the two, but a credential commented out rather than deleted is still a hard-coded credential. Closing that page's finding means deleting the comment; closing this one means rotating the value, and version control keeps it recoverable until both happen
  • CWE-526 (Cleartext Storage of Sensitive Information in an Environment Variable) - MITRE lists no relationship between the two, but it is where a CWE-798 finding lands when the literal is moved into the environment and stopped there. The environment variable below is the minimum fallback, not the fix, and that page covers what is still exposed once the value sits there

OWASP Classification

A07:2025 - Authentication Failures

Risk

Critical: The credential is exactly as exposed as the code that contains it, and cannot be changed without a release.

  • Anyone who can read the source, the build artifact, or the repository history has the credential.
  • An attacker who has it authenticates the way the application does - to the database, the API, the cloud account, or the admin interface - with whatever privileges the credential carries. A hard-coded admin credential grants full system access; a database credential exposes everything stored behind it.
  • A credential in something published, such as a public repository or a distributed binary, is available to everyone who downloads it, which is how one leaked key becomes a widespread compromise.
  • Compliance exposure depends on the regime rather than applying uniformly. PCI DSS prohibits shared and hard-coded application credentials outright; under SOC 2 and HIPAA the finding is failing a control you asserted you had, or one your own risk assessment called for.

Remediation Steps

Core Principle: Never hard-code credentials. Fetch them at runtime from a managed store, and rotate them.

Identify all hard-coded credentials in the codebase

  • Look for database passwords, API keys, encryption keys, tokens, and certificates.
  • Cover source code, configuration files, build scripts, and values assigned to environment variables in code.
  • Search the version history as well. A credential deleted from the current tree is still in the commits that carried it.
  • Run a scanner: git-secrets, TruffleHog, GitGuardian, Gitleaks.
  • The patterns that find most of them are password=, api_key=, secret=, token=, connection strings, and Base64-encoded values.
  • Check test code. Mock credentials in tests are often real production secrets.
  • Search for comparisons as well as assignments. Everything above finds the outbound half; a credential the product accepts appears as an equality test against a literal and matches none of it.

Migrate to secrets management systems (Primary Defense)

Fetch the credential at runtime from a managed store. The value then never exists in code, and the store becomes the one place where access to it is audited and rotation happens:

  • AWS: Secrets Manager, Systems Manager Parameter Store
  • Azure: Key Vault
  • Google Cloud: Secret Manager
  • HashiCorp: Vault
  • Kubernetes: Secrets
  • Enterprise: CyberArk, Thycotic, enterprise PAM solutions

The pattern is the same whichever you use:

  1. The application authenticates to the secrets manager through an IAM role, a service account, or a Managed Identity, so it holds no credential in order to reach the store.
  2. It retrieves the credential at runtime.
  3. It keeps the value in memory, never logging or persisting it.
  4. The credential can be rotated without a code change or a redeployment.

Beyond that: enable automatic rotation for database passwords and API keys, cache retrieved values with a TTL to keep call volume down, and use explicit versions or controlled rollout labels where rotation needs staged deployment - understand how "latest" behaves before relying on it.

Use environment variables (Minimum alternative if secrets manager unavailable)

  • Set the credential in the environment outside the code and read it at runtime with os.getenv(), process.env, or Environment.GetEnvironmentVariable().
  • Add .env, .env.*, secrets.*, credentials.*, *.pem and *.key to .gitignore. Use .env.* rather than *.env: measured with git, *.env ignores .env and prod.env but not .env.local, which is the spelling a secret usually reaches the repository under.
  • Inject values at deployment time through the platform's secret mechanism rather than baking them into images or manifests.
  • Use different credentials for dev, staging, and production.

Environment variables are a fallback, not a complete secret-management system. They leak through process inspection, debug dumps, platform metadata, and accidental logging - see CWE-526.

Remove credentials the product itself accepts (inbound)

This is the other half of the CWE and it needs a different fix. Nothing here is solved by a secrets manager, because the value is not a secret the product needs to hold - it is an authenticator the product should never have accepted.

  • Find the comparisons, not just the assignments. The outbound half looks like password = "..."; this half looks like if (user == "admin" && pass == "..."), if (hash(input) == "68af404b..."), or a licence check against a fixed key. A grep for assignment patterns finds none of them. Search instead for equality tests against literals in authentication, authorization, activation and diagnostic code paths, and for any account the installer creates.
  • Delete the credential rather than relocating it. There is no correct place to keep a password that is identical on every installation. Reading it from Key Vault, an environment variable or an obfuscated blob leaves the same authenticator working on every deployment; the attacker's copy of the product is as good as the customer's.
  • Replace a default account with first-run enrolment. Where an account is genuinely needed at install time, generate a unique random credential per installation and require it to be changed before the system serves traffic. Refusing to start, or refusing all non-setup requests, until enrolment completes is what makes this stick - a default that merely warns stays in place.
  • Do not accept a hard-coded hash either. A fixed digest is a fixed credential. Store a per-user hash of a per-user password (see CWE-916), so that compromising one installation does not yield the credential for every other one.
  • Remove maintenance and diagnostic backdoors outright. Support access belongs behind the same authentication and authorization as everything else, with per-technician identities and an audit trail. Where a physical or console path is genuinely required, bind it to local access rather than to a shared secret reachable over the network.
  • Treat every deployed instance as compromised. As with the outbound half, the credential is already published - in this case in the binary or the manual. Removing it in the next release does not protect installations running the current one, so a fix needs a rollout plan, not just a commit.

Remove credentials from version control

Once a credential has been committed:

  • Rotate every exposed credential and revoke exposed keys.
  • Remove the value from the current code.
  • Rewrite repository history only after planning the side effects.
  • Coordinate with collaborators and clean downstream clones, forks, caches, and release artifacts where possible.

To keep it from happening again:

  • Add .env, .env.*, secrets.*, credentials.*, *.pem and *.key to .gitignore. Use .env.* rather than *.env: measured with git, *.env ignores .env and prod.env but not .env.local, which is the spelling a secret usually reaches the repository under.
  • Use pre-commit hooks - git-secrets, detect-secrets - to block credential commits.
  • Enable secret scanning: GitHub Secret Scanning, GitLab Secret Detection, or Amazon Q Developer on AWS. Amazon CodeGuru Security, which carried the secrets detector this page used to name, reached end of support on 20 November 2025.
  • Require peer review for any config change.

Implement least privilege and credential rotation

Least privilege limits what a leaked credential reaches:

  • Give each credential the minimum permissions it needs - a read-only database user where nothing writes.
  • Separate credentials by environment (dev, staging, prod) and by service.
  • Apply IP allow listing where possible.
  • Prefer temporary credentials such as AWS STS or OAuth tokens with an expiry.

Rotation limits how long a leaked credential keeps working:

  • Rotate production credentials every 30-90 days, automated through the secrets manager.
  • Support multiple active credentials during a rotation so it needs no downtime.
  • Monitor for use of old credentials after rotation.
  • Exercise the rotation process regularly rather than first running it during an incident.

Test and verify credential removal

Verification has to cover the artifact as well as the source. A value removed from a file can still sit in a compiled binary, a container image layer, or a history that a scanner reaches and grep does not, so check the build output and the secrets manager's own access logs alongside the four checks below.

1. Code Scanning

# Search for common credential patterns

grep -r "password\s*=\s*['\"]" .
grep -r "api_key\s*=\s*['\"]" .
grep -r "secret\s*=\s*['\"]" .
grep -r "jdbc:.*://.*:.*@" .

# Use automated tools

trufflehog filesystem ./
gitleaks detect --source . --verbose
git-secrets --scan

Expected result: No hard-coded credentials found in code.

2. Configuration Verification

  • Verify credentials load from the secrets manager or environment, and that config files (application.properties, web.config, settings.py) hold none
  • Test application startup with missing credentials (should fail gracefully with clear error)
  • Verify different credentials used in dev vs. production
  • Check logs don't expose credentials

3. Credential Rotation Test

  • Rotate a credential in secrets manager
  • Verify application picks up new credential (with or without restart, depending on design)
  • Confirm old credential no longer works
  • Ensure no downtime during rotation

4. Version Control Audit

# Scan entire git history for secrets

trufflehog git file://.
gitleaks detect --source . --log-opts="--all"

Expected result: No credentials in current code or the repository history you control. Any exposed secret should still be considered compromised and rotated because clones, forks, logs, and caches may retain it.

Common Pitfalls

  • Moving the credential from code into a committed config file. Replacing password = "secret" in source with the same value in appsettings.json, config.yaml, or a tracked Dockerfile ENV line means the credential is no longer "in code," but it is still readable by anyone with repository access, so the exposure is unchanged.
  • Setting the value in plaintext in a committed CI/CD pipeline file - .gitlab-ci.yml, a GitHub Actions workflow, a Kubernetes manifest - instead of the platform's encrypted secrets store. This looks like the environment-variable fix, but the value is still readable by anyone who can view the file's history.
  • Deleting the secret from the current file without treating the old commits, forks, and CI caches as still compromised. The credential remains recoverable from history until it is rotated at the source system, not just removed from the latest commit.
  • Adding a secrets manager for one service while a sibling service or script still reads the same credential from a local .env or config file. Partial migration leaves the secret duplicated, so an attacker only needs to find the weaker of the two locations.

Language-Specific Guidance

Implementation detail for specific languages:

  • C# - Azure Key Vault, DefaultAzureCredential, configuration providers
  • Go - AWS Secrets Manager, HashiCorp Vault, environment variables
  • Java - AWS SDK, Azure SDK, Spring Boot externalized configuration
  • JavaScript/Node.js - dotenv, AWS SDK, Azure Key Vault, Google Secret Manager, HashiCorp Vault, Kubernetes secrets
  • PHP - Environment variables, Vault SDK, database credential management
  • Python - boto3 for AWS Secrets Manager, environment variables, python-dotenv

Additional Resources