Choose your stack

Stacks without a card

How it happens

  • The integration starts with the partner's quick-start: the key is pasted into a constant to get the first request working, and it ships. CWE-798 calls these outbound hard-coded credentials, because the app uses them to authenticate to another system.
  • Once committed, the key is in every clone and fork, in the history after the line is deleted, in CI logs and caches, and in every build artifact: the compiled class, the .pyc file, the DLL and the container image. Reading constants out of a program is CAPEC-191.
  • The usual first fix moves the key without removing it: into application.properties, appsettings.json or a .env file that is committed after all (CWE-260), or into a base64 string (CWE-261) that decodes in one call. The code now looks configurable or obfuscated, so reviewers stop flagging it, and the base64 version also slipped past every scanner checked for this card. The key is still in the repository, and the app still starts without any configuration.
  • Partner quick-starts often put the key in the query string, and URLs are recorded everywhere: proxies, the partner's access logs and error messages. In this card's tests, the requests library put the whole URL, key included, into its HTTPError and connection-error messages, which the vulnerable Flask app returned and logged, and urllib3 logged it again at DEBUG. Spring's RestClient and .NET's HttpRequestException leave the query string out of their messages, and .NET 9 and later redact it in HttpClientFactory logs, so in those stacks the key leaked only through the developer's own log line.
  • Environment variables are a delivery mechanism, not a vault. OWASP's Secrets Management Cheat Sheet warns that they are generally accessible to all processes and may end up in logs or system dumps, child processes inherit them by default, and debug pages and diagnostics endpoints can print them.

Fix principles

  • Keep secrets out of everything you commit or build: source, configuration files, Dockerfile ENV and ARG lines, build scripts and fixtures that ship. Commit the names of the settings (an .env.example with empty values), and ignore .env in git.
  • Read each secret at startup through the platform's configuration layer, filled at deploy time from a secret store: environment variables set by the orchestrator, mounted secret files (Kubernetes or Docker secrets, Spring Boot's configtree: import, ASP.NET Core's key-per-file provider) or a secret-store client such as Vault, AWS Secrets Manager or Azure Key Vault. OWASP recommends mounted files or a client over environment variables where the platform allows it.
  • Fail fast: check at startup that every required secret is present and not blank, and stop with a message that names the setting, never the value. A default in code, such as process.env.KEY || 'dev-key', is a hard-coded secret again.
  • Send the secret in a header, preferably Authorization, never in the URL. Checked: Node's fetch, Python's requests and .NET's HttpClient drop the Authorization header when a redirect leads to another origin, but forward a custom X-Api-Key header.
  • Keep the secret out of responses and logs: answer partner failures with a generic message, log the status code or error type rather than exception messages that may embed URLs, and keep secrets out of toString() and repr() of settings objects.
  • Treat a committed key as compromised: revoke and replace it at the provider first. Rewriting history does not remove it from clones, forks or cached views, as GitHub's own guidance points out.
  • Run a secret scanner before commits and in CI, but do not rely on it alone. Generic rules key on variable names, character sets and entropy: on a probe copy of this card's code with a random 32-character key, each plain constant and committed file was caught by Semgrep's generic rules or detect-secrets (no single tool caught them all), and none of them caught the base64 version.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • The tests check each claim directly. They scan every file the selected implementation ships (its source, its configuration files and the build manifests) for the key in plain text or inside any base64 token; start the app with the key missing and with it empty and expect a refusal that names the setting; and serve a forecast with a rotated key supplied by configuration alone. Against a loopback stand-in for the partner API, they check that the key travels only in the Authorization header and never appears in a 502 response or in anything the app logs with debug logging switched on, during a partner outage or when the partner is unreachable.
  • The vulnerable implementations fail the file scan, the URL check, the log check, both startup checks and the rotation check in every stack. Node also fails the partner-error check, because its own error message includes the URL, and Python fails both error-response checks, because requests puts the URL in its exception messages. The wrong implementations fail the file scan and the missing-key startup check everywhere; Python's base64 version also fails the empty-key and rotation checks because it ignores configuration entirely.
  • Scanners see little of this. The card's key is deliberately fake (FAKE-forecast-key-for-tests-only), and Semgrep's generic secret rules skip it: one needs 32 to 45 letters and digits after a name like api_key, the other ignores values containing words such as fake and test. detect-secrets flagged the keyword assignments anyway. Bandit's hard-coded password checks only consider names like password, token and secret, and CodeQL's hard-coded credential queries follow constants only into modelled credential APIs (in JavaScript also the Authorization header), so a key in a query string passes them. Each card's scannerRules records what was run and what it found.

References

  1. OWASP Secrets Management Cheat Sheet
  2. OWASP Top 10:2025 A07 Authentication Failures
  3. CWE-798: Use of Hard-coded Credentials
  4. CWE-260: Password in Configuration File
  5. CWE-261: Weak Encoding for Password
  6. CAPEC-191: Read Sensitive Constants Within an Executable
  7. OWASP ASVS 5.0.0, V13 Configuration (13.3 Secret Management)
  8. OWASP ASVS 5.0.0, V14 Data Protection (14.2.1, no sensitive data in URLs)
  9. OWASP ASVS 5.0.0, V16 Security Logging and Error Handling (16.2.5, 16.5.1)
  10. The Twelve-Factor App: III. Config
  11. GitHub Docs: Removing sensitive data from a repository (revoke or rotate first)

Attack patterns:

Guides on this weakness

Guide

Secrets management: storage, rotation and leaked-secret response

Follow one application credential from issuance through caching, rotation, revocation and restored backups, prove that its old value stops working, and respond correctly when a secret leaks.

Guide

Place each security control in the pipeline and decide what it blocks

Put secrets scanning, SAST, SCA, IaC and image scanning, DAST, fuzzing, attestation and admission control where each sees the right input, block only on findings a developer can act on, and deploy only images the pipeline has attested.

Guide

Apply a security baseline across AWS, Azure and Google Cloud

Give workloads platform identities instead of keys, guard the instance metadata endpoint, block public storage, keep audit logs past the default retention, close inbound admin ports and use the native secret store, with tested Terraform for AWS, Azure and Google Cloud.

Recipe

Contain a leaked secret, then investigate and clean up

Revoke an exposed API key, token or private key at its issuer before anything else, end the sessions it minted, check the provider's logs, then remove every copy and replace it with a credential that expires on its own.

Guide

Apply CISA Secure by Design: principles, pledge goals and evidence

What CISA's Secure by Design guidance and pledge ask of software manufacturers as of October 2026, and how to turn each of the three principles, the seven pledge goals and the Product Security Bad Practices into engineering actions, metrics and evidence.

Guide

Secrets at runtime: Vault, Kubernetes Secrets, CSI driver, External Secrets and SOPS

How a running workload should get its credentials: encrypt Kubernetes Secrets with a KMS v2 provider and lock down RBAC, mount from an external store with the Secrets Store CSI Driver, sync with External Secrets Operator, issue dynamic credentials with Vault and its Agent Injector, keep Git copies encrypted with SOPS, and make rotation reach the process.

Recipe

Call a hosted LLM API safely

A code-first checklist for wiring up an OpenAI or Anthropic API call from a backend: keep keys server-side, redact PII before prompts and logs, choose provider retention settings deliberately, validate structured output and still treat it as untrusted, and set timeouts, retries and per-user cost caps.

Guide

Keep secrets and controls out of hidden context (OWASP LLM08:2026)

OWASP LLM08:2026 Hidden Context Exposure, which replaced System Prompt Leakage: why system prompts, tool schemas and retrieved policy text get extracted, what must never be in them, and how to enforce rules where a leak changes nothing.

Case study

Case study: the CircleCI security incident (January 2023)

How a session stolen from one engineer's laptop reached CircleCI's production systems and customers' stored secrets in 2023, and what platform operators and CI users should change.

Case study

Case study: Microsoft AI researchers' over-permissive SAS token (2023)

How an over-permissive Azure Storage SAS token in a public repository for AI models exposed internal Microsoft data in 2023, and how to scope, scan for and revoke signed URLs.