← OWASP Top 10:2025

A04:2025

Cryptographic Failures

Data that should be protected is not: weak or missing encryption, secrets hardcoded in source, passwords hashed badly, tokens generated with Math.random().

What it is

This category covers failures to protect data with cryptography, or protecting it with cryptography that is broken. Transmitting in plain HTTP. Storing passwords with MD5 or SHA-1 or no salt. Generating tokens with a non-cryptographic random source. Comparing secrets with == so timing leaks them. Hardcoding the key or secret in the source, which makes every other control moot.

It slipped from second to fourth in 2025, not because it got rarer but because misconfiguration and supply chain grew faster. It remains the category behind most credential and data leaks.

How it shows up in AI-generated code

In our scans this is the category AI tools get wrong most often. Across 2,148 AI-built repositories, a hardcoded credential was the top high-severity finding in every group, 276 repos in total. Weak randomness, Math.random() for a token or reset code, was in the top five. Disabled TLS verification, added to make an HTTPS call work, showed up in 49 repos.

Each one has the same cause. The tool needs the feature to work now. Pasting the key inline works. Math.random() works. rejectUnauthorized: false works. bcrypt with a low cost or plain SHA-256 works. Nothing in the loop checks whether it is safe, so the working version ships.

Example: A password reset token from Math.random()

The pattern
const token = Math.random().toString(36).slice(2);   // predictable
await db.reset.create({ data: { userId, token } });
const secret = process.env.JWT_SECRET || "supersecret";   // fallback ships
The fix
import { randomBytes, timingSafeEqual } from "node:crypto";
const token = randomBytes(32).toString("hex");            // 256 bits, CSPRNG
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error("JWT_SECRET is required");  // refuse to start
// compare secrets with timingSafeEqual, never ==

The fallback secret is the sneakiest: it looks like configuration, and in production where the variable is unset, every token is signed with a string that is in the public repo.

How to find it

  • Search the codebase for API key formats, 'password', 'secret' and 'token' assignments, and for the || fallback pattern on env lookups.
  • Search for Math.random and random.random near anything called token, code, id or nonce.
  • Search for md5, sha1, and password hashing without bcrypt, scrypt or argon2.
  • Search for rejectUnauthorized, verify=False, and NODE_TLS_REJECT_UNAUTHORIZED.
  • Prbl rules PRBL-C001, C002, R001, R002, R003 and C003 cover all of these, and the live scan finds keys that reached the browser.

How to fix it

  • Move every secret to a server-side environment variable and make the app refuse to start without it. No fallbacks.
  • Rotate any secret that was ever in the code. Assume it was read.
  • Use the platform's cryptographic random source for anything security-sensitive.
  • Hash passwords with bcrypt, scrypt or argon2 at a current cost factor.
  • Compare secrets and signatures with a constant-time function.
  • Enforce HTTPS and never disable certificate verification.

What Prbl checks for this category

PRBL-C001 Hardcoded credentialPRBL-C002 Hardcoded session or signing secretPRBL-R001 Weak randomnessPRBL-R002 Timing-unsafe comparisonPRBL-R003 AES-GCM without tag checkPRBL-C003 TLS verification disabledURL-SECRET-* live checks

Run a free scan on a public repo or a live URL. Findings link to the fix guides below.

Fix guides for this category

Go deeper

Common questions

Is a hardcoded secret really a cryptographic failure?

OWASP files it here because the secret is the key to the cryptography: a signing secret in the repo means every signature can be forged, an API key in the bundle means the provider's protection is bypassed. It is also the most common serious finding we see in AI-generated code, so it gets its own page: the secret scanner.

Is SHA-256 fine for passwords?

No. It is fast, which is the problem; a GPU tries billions per second. Password hashing needs a deliberately slow, salted algorithm: bcrypt, scrypt or argon2. AI tools often reach for SHA-256 because it is in the standard library.

Why does Math.random() matter if the token is long?

Length does not help if the generator is predictable. Math.random() is seeded from state an attacker can sometimes recover from a few outputs. Use crypto.randomBytes or the Web Crypto API; it is one line.

Is this category already in something you shipped? Scan a live URL or a public repo free, no account.

Scan my app →
A04:2025 Cryptographic Failures Explained, With AI-Generated Code Examples | Prbl