Some vulnerabilities take a chain of five clever steps. These take one. A live key sits in a config file that ships. A token check calls decode where it should call verify. Either way, an attacker does not need to break in. They log in as you, or as anyone they like.

Both bugs are old, well documented, and getting more common. Coding agents copy whatever the sample did, and samples are written to work, not to be safe.

A secret in git history is published. A token you do not verify is a form the user fills in.

Hardcoded secrets are growing faster than developers

GitGuardian's State of Secrets Sprawl 2026 counted about 28.65 million new hardcoded secrets pushed to public GitHub in 2025, up 34% in a year, the largest jump it has recorded. Secrets for AI services rose 81%. Commits co-authored by an AI coding agent leaked secrets at roughly twice the baseline rate. A year earlier, the 2025 report found public repositories using Copilot leaked secrets about 40% more often than the average.

Private repositories are not a safe place for them either. GitGuardian finds internal repos several times more likely than public ones to hold a hardcoded secret. Private today is one misconfigured permission, one contractor's fork, or one leaked laptop away from public.

The worst number is about time. Of the credentials GitGuardian confirmed valid in 2022, most were still valid years later. Leaks are found. Rotation is what does not happen.

What a leaked key costs

Uber is the classic case. According to the FTC, intruders in 2016 used an access key an engineer had posted on a code-sharing site, and downloaded files holding data on more than 25 million people. Toyota's T-Connect source, uploaded to public GitHub by a contractor, carried a data-server key for nearly five years and exposed details of about 296,000 customers.

Scope makes it worse. Microsoft AI researchers shared a model-download link that included an Azure SAS token. Wiz found it granted full control of the entire storage account, not read access to one folder: 38 TB, including workstation backups and internal Teams messages. At Sourcegraph, an engineer committed an active site-admin token in July 2023, an attacker used it six weeks later, and the company's automated code analysis had not flagged it.

Detection without rotation is the recurring failure. KrebsOnSecurity reported that an xAI developer's key to private models stayed valid for weeks after an automated alert. In 2026, a CISA contractor's public repository held admin keys for AWS GovCloud accounts and a CSV of plaintext passwords. The commit history shows GitHub's default secret blocking had been switched off, and the keys stayed live for more than two days after notification. CISA's own postmortem noted its incident playbook did not cover GitHub.

Broken token checks: alg:none and decode without verify

A JSON Web Token is three base64 strings: a header, a set of claims, and a signature. The claims say who you are. The signature is the only thing that makes them true. Code that reads the claims without checking the signature is trusting text the client wrote.

There are three ways this goes wrong. The first is the shortcut. In Node's jsonwebtoken library, as PortSwigger's JWT guide explains, decode() parses a token and verify() checks it. The decode snippet is shorter, so it gets pasted. The second is letting the token pick its own algorithm. The original Auth0 write-up showed libraries accepting alg: none as a valid signature, and servers expecting RSA accepting an HMAC token signed with the server's public key. The third is a weak HMAC secret that can be cracked offline.

WHAT GETS PASTED
  const user = jwt.decode(token)         <- parses only
  if (user.role === "admin") { ... }     <- client wrote this

WHAT IT NEEDS
  const user = jwt.verify(token, KEY, {
    algorithms: ["RS256"],               <- pinned, never
    issuer: "https://auth.example.com",     taken from the
    audience: "api",                        token header
  })
  // expired, unsigned, wrong-alg, wrong-issuer -> throws

AN ATTACKER'S TOKEN
  header  {"alg":"none","typ":"JWT"}
  claims  {"sub":"1","role":"admin"}
  sig     (empty)

These are not theoretical. CVE-2015-9235 let attackers choose the algorithm in jsonwebtoken. CVE-2022-23540 made the same library fall back to none when verify() was called with no algorithm list and an empty secret. CVE-2020-15084 in express-jwt allowed authorization bypass because the algorithms option was optional. Java's "Psychic Signatures" bug (CVE-2022-21449) accepted an all-zero ECDSA signature for any message, which forged signed JWTs among other things.

The rules are settled. RFC 8725, the JWT best current practice, requires libraries to let callers fix the accepted algorithms and reject everything else, and to validate issuer and audience. The OWASP JWT cheat sheet adds that HMAC secrets need real entropy, not a password, and that kid, jku and jwk headers are attacker input.

The login page is the other half

Stolen credentials do not need a bug at all, only a login endpoint that answers as fast as it is asked. Verizon's credential stuffing research found stuffing made up a median of 19% of daily authentication attempts in SSO logs, and that each stolen password is usually tried once per account, so it rarely trips a lockout. Credential abuse was the top initial access vector in the 2025 DBIR.

OWASP API2:2023 lists the failures to test for: no brute-force protection on login, the same on password reset, tokens in URLs, unsigned or expired JWTs accepted. Its example shows GraphQL batching getting around a three-attempts-per-minute limit, because the limit counted requests, not guesses.

What to check before release

SECRETS
[1] No live keys in the repo, including history
    and the client bundle
[2] Push protection on; a CI step fails the build
    on a live key pattern
[3] Every key scoped to the least it needs
    (read-only, one bucket, short expiry)
[4] A leaked key is rotated, not just deleted
    from the latest commit

TOKENS
[5] verify(), never decode(), on every path that
    trusts claims
[6] Algorithm pinned server-side; alg:none and
    algorithm switches rejected
[7] Issuer, audience and expiry checked
[8] HMAC secret is random and long, not a word

LOGIN
[9] Login, reset and OTP endpoints slow down per
    account and per source, including batched calls

Neil Madden's API Security in Action makes a point worth keeping: a stateless token cannot easily be taken back. Keep expiry short, and keep a way to revoke the ones that matter.

Find them in the build you ship

Secret scanners catch known key formats in new commits. They miss generic passwords, keys in history before the scanner was switched on, and keys that only appear in the built frontend bundle. Nothing in a scanner tells you that a token with no signature is accepted by your API. That takes sending one.

GhostProbe does both. The Pentest job is $1,490 for up to four staging or preproduction addresses you own. Connect up to 2 GitHub repositories and the review reads the source for keys and for verification code that only decodes. It also sends forged and unsigned tokens at the running app, and repeats login and reset requests to see whether they slow down. Recon is $490 for a single address. A security engineer confirms each finding, removes false positives, writes the fix, down to the key to rotate, and signs the dossier.

Run it before the next release, and rotate whatever it finds the same day.

Buy Pentest Buy Recon