CWE-522: Insufficiently Protected Credentials
Overview
Credentials such as passwords, API keys, or tokens are stored or transmitted without adequate protection, so they can be stolen or misused.
Relationship to Other CWEs
Where a finding names the defect precisely - a password column in plain text, a credential encoded rather than hashed - report it against the child that names it and use this page for the surrounding lifecycle. MITRE marks CWE-522 ALLOWED-WITH-REVIEW for that reason: it "is a Class and might have Base-level children that would be more appropriate", so "examine children of this entry to see if there is a better fit". In MITRE's simplified mapping view (view-1003), which a CWE taken from a CVE record usually follows, CWE-522 appears under CWE-287 (Improper Authentication) instead, so a finding routed through published vulnerability data may arrive carrying either.
The pages around it differ by which part of the credential lifecycle failed, and by what the stored value actually is:
- CWE-522 (this page) - a credential is stored, transmitted, or displayed without protection adequate to what it unlocks, and no narrower child fits the finding
- CWE-256 (Plaintext Storage of a Password) - a child, and the storage case at its most direct: the password reaches a database column, config file, or log with no transformation applied at all
- CWE-261 (Weak Encoding for Password) - a child, where the protection is a reversible encoding such as Base64 or hex. The stored value is not the password verbatim, which is the only difference to the risk, but it does change the migration: an encoded column has to be decoded before it can be hashed
- CWE-526 (Cleartext Storage of Sensitive Information in an Environment Variable) - not a child, but the page that owns the environment-variable distinction this page's storage guidance leans on: a platform injecting a fetched secret into the process environment is legitimate, keeping a long-lived secret there is the finding
Four more of MITRE's children have no page here, and each is the Base-level entry a review would prefer over this one: CWE-257 for passwords stored encrypted but recoverable, CWE-260 for a password in a configuration file, CWE-523 for credentials transmitted without protection in transit, and CWE-549 for a password field that is never masked.
OWASP Classification
A06:2025 - Insecure Design
Risk
High: An attacker who intercepts or reads a credential gets access under a legitimate identity, reaching whatever data and privilege that credential carries.
Remediation Steps
Core Principle: Keep credentials inside a protected trust boundary - hashed or vaulted at rest, encrypted in transit - and out of logs, URLs, and anything the client can see.
Locate the insufficiently protected credentials
- Start from the flaw details to find where the credential is stored or transmitted
- Identify the credential type: password, API key, token, private key, database credential, or service-account credential
- Identify the defect: plaintext storage, weak hashing (MD5/SHA-1), hardcoded in source, transmitted over HTTP, or committed to version control
- Trace the flow: where the credential is created, stored, transmitted, and used
Store credentials securely (Primary Defense)
- Hash passwords with bcrypt (work factor 12-14), Argon2id (19-47 MiB memory, 2-4 iterations), or scrypt (N=2^17)
- Never use MD5, SHA-1, SHA-256 (too fast), unsalted hashes, or a hashing scheme of your own
- Store secrets in a secrets management system (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, GCP Secret Manager)
- Never embed passwords, API keys, or tokens in source code
- Treat environment variables as injection, not storage: Where no vault is available, having the deployment platform inject the value into the process environment at start-up is legitimate; keeping the secret in a
.envfile, a deployment manifest, or a shell profile is not. A secret left in the environment is still inherited by child processes, captured by crash dumps, and visible to anyone who can rundocker inspect- see CWE-526
Transmit credentials over secure channels
- Never send a credential over HTTP or any other unencrypted channel
- Never put a password or token in a URL query parameter, where it lands in server logs and browser history
- Set the
Secureflag (HTTPS only) and theHttpOnlyflag (no JavaScript access) on every session cookie, and chooseSameSiteper flow - see CWE-614.Securehas no legitimate exception and is safe to force globally.SameSite=Strictalso withholds the cookie from inbound links, SSO redirects and OAuth callbacks, which lands the user signed out with no error anywhere, so it is not simply the stronger setting - Send tokens in an
Authorization: Bearer <token>header rather than the URL or the body
Limit credential exposure and scope
- Keep access tokens short-lived, and set refresh-token lifetimes according to application risk, device trust, and revocation capability
- Grant each credential the minimum permissions it needs
- Rotate API keys, service-account passwords, and signing keys on a defined interval, and immediately on suspected compromise or when someone with access leaves
- Do not force periodic user password changes. NIST SP 800-63B and the OWASP Authentication Cheat Sheet both advise against scheduled expiry - it pushes users toward predictable variations. Force a change on evidence of compromise (breached-password match, account takeover), at the user's request, or when the authenticator itself changes
- Use separate credentials for dev, staging, and production
- Keep credential files out of version control with .gitignore, and scan for committed secrets (git-secrets, TruffleHog)
Monitor and audit credential usage
- Log authentication attempts: successful and failed logins, API key usage, token generation
- Alert on failed authentication patterns, unusual access locations, and credential exposure attempts
- Track credential age and alert on old passwords, expired certificates, and long-lived API keys
- Record who read which secret in the secrets management system, and when
- Lock an account temporarily after repeated failed attempts
Test the credential protection fix
- Inspect the database and confirm passwords are stored as bcrypt or Argon2 hashes
- Grep the source for "password=" and "api_key=" and confirm no hardcoded credentials remain
- Confirm credentials are transmitted over HTTPS only
- Confirm tokens stop working after the configured expiry
- Check credentials are not in version control history
- Re-scan with the security scanner to confirm the issue is resolved
Common Vulnerable Patterns
Plaintext Password Storage
Why this is vulnerable: Anyone who reaches the database - through SQL injection, an exposed backup, an insider, or a compromised account - reads every password directly, with no cracking step. That is immediate login as any user, administrators included. Because people reuse passwords, the same list works against other services (credential stuffing). Plaintext password storage can violate applicable PCI DSS requirements or security obligations under GDPR or HIPAA. Which requirements apply depends on the organisation, system and data involved.
Weak Password Hashing
// VULNERABLE - fast, general-purpose hash used for passwords
password_hash = fast_hash(password) // e.g. MD5, SHA-1, unsalted SHA-256
Why this is vulnerable: MD5, SHA-1 and SHA-256 are built for data integrity, where speed is the point. A modern GPU computes billions of them per second, so simple passwords fall in seconds and complex ones in hours or days. Unsalted hashes fall faster still, because the work can be done before the breach rather than after it: one precomputed table is built once and then reused against every stolen database, and the same password gives the same digest for every user holding it. That precomputation is what a unique per-password salt defeats - a separate weakness, CWE-759, with CWE-760 for a salt that is predictable rather than absent. Even salted, SHA-256 is too fast - a salt defeats the precomputation, not the speed, so the attacker is back to billions of guesses a second against each hash separately. Password storage needs a deliberately slow algorithm (bcrypt, Argon2id, scrypt) that makes each guess expensive.
Hardcoded Credentials in Source Code
// VULNERABLE - committed to version control, persists in history even after deletion
DB_PASSWORD = "hardcoded_secret"
Why this is vulnerable: A credential written into source code is committed with it, and stays in git history after someone deletes the line. Everyone with repository access reads it - current developers, former employees, and anyone who has taken over one of their accounts. If the repository is ever made public or leaked, the credential goes with it. Rotating it takes a code change and a deploy, which makes incident response slow, and the same value is readable to anyone who reaches the deployed application files or decompiles the build.
Weak JWT Secrets and Long-Lived Tokens
// VULNERABLE - brute-forceable secret, and a year-long window if it's ever stolen
jwt_token = create_token(user, secret = "weak", expiry = 365_days)
Why this is vulnerable: An attacker who captures one token can brute-force a weak signing secret and then mint valid tokens for any user without authenticating. Short or predictable secrets such as "secret" or "123456" fall in minutes to off-the-shelf JWT cracking tools. A long expiry - days, months, or years - compounds it: a stolen token keeps working until it expires, with no re-authentication needed, so the account stays exposed for the token's whole lifetime unless someone rotates the signing secret.
Secure Patterns
Strong Password Hashing with bcrypt
// SECURE - deliberately slow, salted, tunable work factor
user.password_hash = bcrypt_hash(password, salt_rounds = 12)
Why this works: bcrypt is built for password hashing. It gives each password a unique random salt, so no precomputed table applies, and its work factor (salt_rounds) sets how slow the hash is. At 12 rounds a single hash takes roughly 250ms, which nobody notices on a login but which puts years of GPU time behind each password an attacker wants out of a stolen database. As hardware improves, raising the factor to 13, 14 or 15 is a configuration change rather than a code change. Weak passwords still fall; strong ones stay out of reach.
Secrets from Secure Vault Storage
// SECURE - fetched at runtime, never stored in code or plain config
db_creds = secrets_manager.get_secret('prod/database')
connection = create_connection(db_creds)
Why this works: A secrets management system (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) holds the credential encrypted at rest behind authentication and authorization, and records who read it and when. Rotation happens centrally without a code change, though the application still needs a reload, cache refresh, or restart strategy to pick up the new value. Because the code holds only the secret's name, there is nothing to commit to version control or leave sitting in a configuration file.
Short-Lived JWT Tokens with Strong Secrets
// SECURE - strong secret from a vault, bounded token lifetimes
jwt_secret = vault.get_secret('jwt/secret-key')
access_token = create_token(user_id, jwt_secret, expiry = 1_hour)
refresh_token = create_token(user_id, refresh_secret, expiry = 30_days)
Why this works: A cryptographically strong secret - 256 or more bits of random data - cannot be brute-forced, so an attacker cannot forge a token without stealing the secret itself. A one-hour access token bounds what a stolen token is worth. The refresh token spares users a password prompt every hour, and its 30-day expiry is still an expiry. Keeping both secrets in a vault rather than in code means either can be rotated once compromised.
HTTPS Enforcement for Credential Transmission
// SECURE - move a browser's plain-HTTP navigation to HTTPS, then tell it not to try HTTP again
// CANONICAL_HOST is configuration, not req.host: the Host header is attacker-controlled,
// so building the target from it turns this into an open redirect
on_request(req):
if req.protocol != 'https':
return redirect('https://' + CANONICAL_HOST + req.path)
set_header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
// SECURE - cookie attributes that protect a session token
set_cookie('session', token, secure = true, http_only = true, same_site = 'lax')
Why this works: TLS encrypts everything in transit, so an attacker on the same WiFi or at the ISP sees nothing usable. The secure cookie flag keeps the session cookie off plain HTTP, and httpOnly keeps JavaScript from reading it, so an XSS bug cannot lift the token. sameSite decides which cross-site requests carry the cookie; lax is the safe default because it still allows the top-level navigation that an email link or an SSO callback performs. The redirect and HSTS cover the browser case, in that order: the redirect moves a plain-HTTP navigation to HTTPS, and Strict-Transport-Security then makes the browser rewrite http:// to https:// itself, so later visits never emit a plaintext request at all. Neither one rescues a credential that has already been sent - a form POST or an API call addressed to http:// puts the secret on the wire before your redirect is written, and a redirect is a response. So the client is what has to require HTTPS for anything that is not a browser navigation, and HSTS preloading is what closes the very first visit, before any header of yours has been seen. Behind a load balancer, read the forwarded scheme rather than the connection's own, or the check never sees HTTPS and the redirect loops.
Language-Specific Guidance
- C#/.NET - appsettings.json secrets, ASP.NET Core Identity password hashing
- Java - BCrypt, Spring Security, environment variables
- JavaScript/Node.js - bcrypt, dotenv, secure password storage
- PHP - password_hash()/password_verify(), avoiding config.php credentials
- Python - bcrypt, Argon2, environment variable management