Secret hygiene

Catching leaked secrets and API keys before they reach production

A credential that ships inside a browser extension or a published package is a credential the whole internet can read. The fix is to find it in the build pipeline, while it is still cheap to rotate, not after it lands in a store or a registry.

In shortSecrets leak through hardcoded keys, committed .env files, and build artifacts that bundle developer config. Scan every build for credential-shaped strings, anchor each pattern to a known provider format to keep false positives near zero, and fail the CI gate when a real secret is found so it never ships.

Why does a leaked secret in a shipped artifact matter so much?

When source code stays on a private server, a stray API key is a contained mistake. When that same code is compiled into a published npm package, a PyPI wheel, or a browser extension, the key is now in a file that anyone can download and read. Minified code does not help: a token embedded in a bundle is still a literal string, and extracting it is a one-line search.

The blast radius depends on the credential. A read-only analytics key is an annoyance. A cloud provider secret, a payment processor key, or a CI deploy token can give an attacker the ability to spend money, read customer data, or push their own malicious update through your release channel. That last case feeds directly into the broader pattern of supply chain attacks, where a single leaked deploy credential becomes the entry point for compromising every downstream user.

The economics are simple. Finding the secret in CI costs one rotation and one commit. Finding it after publication costs an emergency rotation, a forced package or extension update, and an incident review, often while attackers are already using the key.

How do secrets actually leak into builds?

Most leaks are not exotic. They fall into a small number of repeated mistakes:

  • Hardcoded keys: a developer pastes a token directly into a source file to make something work, then forgets to remove it. This is the most common path and survives all the way into the published artifact.
  • Committed .env files: an environment file meant for local development gets added to version control, then copied into the build output by a bundler that ships everything in the project directory.
  • Build artifacts and source maps: webpack and similar tools can inline environment variables at build time. A value like process.env.STRIPE_SECRET read during bundling becomes a literal string in the output, and source maps can expose the original code containing it.
  • Config files in the package root: cloud credential files, kubeconfig, .npmrc with an auth token, or service-account JSON that ends up inside the package because the publish step did not exclude it.

Browser extensions add their own twist: the entire bundle is delivered to the user's machine, so any secret in a content script, background worker, or even a JSON locale file is fully exposed. We cover the extension-specific surface in browser extension security.

What does a secret look like to a scanner?

Detection works because most credentials are not random noise. Providers give their tokens a recognizable shape: a fixed prefix, a known length, and a defined character set. An AWS access key begins with a specific letter sequence. A GitHub token, a Stripe key, a SendGrid key, a Slack token, each has a documented format. A private key block is wrapped in distinctive header and footer lines.

This is the foundation of secret leak detection. Instead of guessing whether a long string might be sensitive, a scanner matches it against a catalog of provider formats. When a string matches the AWS access key pattern, it is almost certainly an AWS access key, not a coincidence.

Generic high-entropy detection still has a role for credentials with no fixed prefix, such as raw connection-string passwords, but it is applied carefully and with context, because entropy alone produces noise on hashes, UUIDs, and minified identifiers.

Why does pattern anchoring keep false positives near zero?

The hardest part of secret scanning is not finding secrets. It is not flagging things that are not secrets. A scanner that screams on every long base64 blob, every git commit hash, and every minified variable name trains developers to ignore it. Once a tool cries wolf, its findings get suppressed, and the real leak slips through with the noise.

Anchoring each rule to a distinctive provider prefix or structure is what prevents this. A rule that requires the literal AWS prefix plus the correct length and charset will not fire on a random hash. A rule for a connection string requires an actual password segment, not just the presence of a database URL. A public hostname, an endpoint, an OAuth client id, or a measurement id is not a credential, so those are deliberately excluded.

The discipline behind this is documented in our detection methodology and the detection rules catalog: every secret rule is paired with a positive fixture that must match and a negative fixture that must not. A rule that fires on a UUID, a hostname, or a dictionary word is a bug, not a finding. This is why a credential-shaped value embedded in an image data URI or a font glyph table is filtered out rather than reported.

How do you scan extensions and packages, not just source?

Scanning the repository is not enough, because the leak often appears only in the build output. A bundler can inline a secret that never existed as a literal in source. So the artifact itself, the thing you are about to publish, must be scanned.

For an extension, that means unpacking the CRX or XPI and reading every shipped file, including JSON resources. For a package, it means opening the npm tarball or the PyPI archive and scanning the files that will actually be installed. Our static analysis engine does exactly this across all supported ecosystems, including npm and PyPI, so a key that only appears in the compiled output is still caught.

There is a runtime dimension too. A credential may not sit in any source file but appear only when the code runs and fetches it, then forwards it somewhere. The dynamic sandbox observes captured traffic and surfaces secrets that show up at runtime, next to the static findings.

How does Extuno gate CI on real findings?

A scanner is only useful in CI if it can make a pass or fail decision and produce output your platform understands. Extuno exposes a gate that returns a clear verdict and a SARIF report.

The SARIF output is the standard format that code-hosting platforms read to render inline annotations on a pull request, so a leaked key shows up as a comment on the exact line. The gate endpoint takes a policy, for example fail on any finding in the secret category, and returns whether the build passes along with the list of blocking findings. A small CLI runs the whole flow: upload the artifact, scan, fetch SARIF, evaluate the gate, and exit non-zero if it fails.

Two design choices keep this practical. First, an inline allow annotation lets a developer acknowledge a finding in version control when it is a deliberate, reviewed exception, so a known non-issue does not block forever. Second, a baseline file lets you adopt scanning on an existing codebase without drowning in pre-existing findings, focusing the gate on newly introduced leaks. The git-history mode also scans deleted and old commits, because a secret removed in a later commit is still readable in history and still needs rotation. See CI/CD secret scanning for the full setup and a sample report for what the output looks like.

What should a team do when a secret is found?

The first reflex is often to delete the line and move on. That is not enough. Once a credential has been committed or built, treat it as compromised, even if it was never published. The correct response is to rotate the secret at the provider, so the leaked value stops working, and only then remove it from the code.

To prevent recurrence, move the value into a secrets manager or an environment variable that is injected at deploy time and never written into the artifact, add the file type to your ignore rules, and keep the CI gate in place so the next accidental commit is caught automatically. Because credentials linger in git history, pair the gate with a one-time history scan so old leaks are found and rotated, not just new ones.

For published software, the discipline is the same idea applied continuously. Pairing secret scanning with version diffing means that when a new release suddenly introduces a credential or a new exfiltration endpoint that was absent in the prior version, the change is flagged as a high-signal event rather than slipping through as a routine update.

Run the CI gate and fail the build on any secret finding
extuno-scan ./dist --fail-on secret --sarif results.sarif
# exit 0 = clean, exit 1 = blocking secret found, exit 2 = error

Frequently asked questions

Is rotating the key really necessary if I never published the code?

Yes. Once a secret has been committed to version control or written into a build artifact, you cannot be certain who has seen it. Build logs, caches, forks, and CI systems all retain copies. Rotating the credential at the provider is the only way to guarantee the leaked value is dead.

Why does anchoring patterns to provider formats matter more than catching everything?

A scanner that flags every high-entropy string produces so much noise that developers stop reading it, and the real leak gets suppressed along with the false positives. Anchoring each rule to a known provider prefix and structure keeps the signal high, so a finding almost always means a genuine credential.

Can a secret leak even if it is not written in my source files?

Yes. Bundlers can inline environment variables at build time, turning a value read from process.env into a literal string in the output. That is why scanning the built artifact, not just the repository, is essential, along with checking for committed .env files and credential config in the package root.

How do I stop a leaked secret from blocking my pipeline forever when it is a deliberate exception?

Use an inline allow annotation in the code or a baseline file that records accepted findings. Both let you acknowledge a reviewed exception in version control while the gate continues to fail on newly introduced secrets.

Does scanning published packages and extensions differ from scanning source?

The technique is the same, but the target changes. You unpack the shipped artifact, the CRX, XPI, npm tarball, or PyPI archive, and scan the files that will actually be installed, because the leak often appears only in the compiled output rather than the original source.

Sources

Related