Cryptographic failures are any weakness in how an application protects data in transit or at rest, from missing encryption to weak algorithms to leaked keys. They sit at A02 in the OWASP Top 10 because they are the root cause of most headline-grabbing data breaches. In this post I want to walk through what cryptographic failures actually look like in production code, what causes them, and the small set of habits I rely on to keep them out of the systems I build.
What are cryptographic failures?
Cryptographic failures are mistakes in the design or implementation of cryptography that expose sensitive data. The category used to be called Sensitive Data Exposure in the 2017 OWASP Top 10, but the name was changed in 2021 to point at the real cause, which is broken or missing crypto, not the leak itself [SOURCE: OWASP Top 10 2021]. The category covers everything from sending passwords over plain HTTP, to storing them with MD5, to hardcoding an AES key in a Git repo, to using a deprecated TLS version on an internal API.
The word failure is precise here. Cryptography is one of the few areas of software where doing nothing and doing the wrong thing produce the same outcome: an attacker reads your data.
Why A02 cryptographic failures matter
The 2021 OWASP refresh moved cryptographic failures from third place to second, just behind broken access control. The reason is that almost every regulated data type, payment cards, health records, personal identifiers, authentication credentials, depends on cryptography to stay confidential. When the crypto fails, the regulatory and reputational fallout is enormous. GDPR, HIPAA, and PCI DSS all treat unencrypted sensitive data as a reportable incident, even if no attacker is proven to have read it.
In my own consulting work I see cryptographic errors in about half the codebases I audit, and the failures are almost never exotic. They are forgotten flags, copy-pasted snippets from a 2012 Stack Overflow answer, and secrets checked into version control.
What are the causes of cryptographic failures?
Most real-world cases I have seen come from a small set of root causes. If you internalise this list you will catch the majority of issues in code review:
1. **No encryption at all** for data that should be protected, especially internal service-to-service traffic and database backups. 2. **Weak or deprecated algorithms** like MD5, SHA-1, DES, RC4, or RSA with a 1024 bit key. 3. **Custom or roll-your-own crypto** instead of vetted libraries. 4. **Hardcoded secrets and keys** in source code, config files, or container images. 5. **Improper key management**, including keys that never rotate, are shared across environments, or live next to the data they protect. 6. **Misconfigured TLS**: outdated versions, weak cipher suites, or self-signed certificates trusted in production clients. 7. **Insecure randomness**, using `Math.random` or `rand()` for tokens, salts, or session ids instead of a cryptographically secure RNG. 8. **Plaintext password storage** or the use of fast hashes like SHA-256 without a slow key derivation function.
What is the impact of cryptographic failures?
The impact ranges from embarrassing to existential. A weak hash leaks the entire user table the moment your database is dumped. A missing TLS check on a mobile client lets a coffee shop attacker steal session tokens. A leaked private key invalidates every signed token in circulation. The 2017 Equifax breach, which exposed 147 million records, started as an unpatched application but propagated because internal traffic and data at rest were not properly encrypted [SOURCE: US GAO Equifax Report].
Beyond the data loss, there is the cleanup. Rotating a leaked symmetric key means re-encrypting every record it protected. Rotating a leaked signing key means invalidating every JWT and forcing every user to log in again. I have lived through both. Neither is a Friday afternoon job.
Common cryptographic failures, with code
Let me show you a few of the patterns I see most often. The fixes are short, but you would be surprised how often the broken version ships.
// Bad: fast hash, no salt, instant rainbow table victim
import { createHash } from "node:crypto";
const hash = createHash("sha256").update(password).digest("hex");
// Good: argon2id, slow by design, salt and parameters baked in
import argon2 from "argon2";
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
});
// Bad: predictable token, do not use Math.random for anything secret
const token = Math.random().toString(36).slice(2);
// Good: cryptographically secure random bytes
import { randomBytes } from "node:crypto";
const token = randomBytes(32).toString("base64url");
// Bad: hardcoded key in source, ECB mode, no auth tag
const key = "supersecretkey123";
const cipher = createCipheriv("aes-128-ecb", key, null);
// Good: key from a managed secret store, GCM mode, random IV
const key = await secrets.get("payments.encryption-key");
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const authTag = cipher.getAuthTag();
Cryptographic failures attacks: how the bad version plays out
A cryptographic failures attack rarely looks like a Hollywood scene. It looks like a script that downloads a leaked database dump, runs it through Hashcat with a wordlist, and recovers 70 percent of the passwords by Tuesday morning. It looks like an attacker on a hotel Wi-Fi running mitmproxy against a mobile app that does not pin certificates. It looks like an intern grepping a public GitHub mirror for `AKIA` and finding live AWS keys.
The pattern is always the same: cheap, automated, and patient. That is why defending against cryptographic failures is mostly about closing the easy doors, not about resisting nation-state cryptanalysis.
How I prevent cryptographic failures
There is no single silver bullet, but there is a small playbook that catches the majority of issues. These are the rules I now apply on every project:
- **Classify your data first.** You cannot decide what to encrypt until you know what is sensitive. Tag every field as public, internal, or confidential. - **Encrypt in transit, always.** TLS 1.2 minimum, TLS 1.3 preferred, including for internal services. Disable weak ciphers and old protocols. - **Encrypt at rest where it matters.** Use platform-native encryption for databases, object storage, and backups. Add field-level encryption for high-sensitivity columns. - **Use modern, vetted primitives.** AES-256-GCM or ChaCha20-Poly1305 for symmetric encryption, Ed25519 or RSA-3072 for signatures, Argon2id or scrypt for password hashing. - **Never roll your own.** Use libsodium, the platform's standard library, or a well-known wrapper. Custom crypto is how researchers find CVEs. - **Manage keys properly.** Store them in a managed KMS or secret manager, never in code or config. Rotate them on a schedule and on suspicion of leak. - **Use a CSPRNG for every secret.** Tokens, salts, session ids, password reset codes. If your language has a `crypto` module, use it. - **Pin certificates in mobile and desktop clients** that talk to APIs you own.
Password hashing deserves its own section
The single most common cryptographic error I see is treating passwords like any other data. Passwords are not encrypted, they are hashed, and the hash function must be slow on purpose. SHA-256 is fast, which is great for file integrity and terrible for passwords. A modern GPU can compute billions of SHA-256 hashes per second, which means a leaked database with SHA-256 password hashes is effectively plaintext for any password under twelve characters.
Use Argon2id, scrypt, or bcrypt with sensible parameters. Tune the cost so a single hash takes around 250 to 500 milliseconds on your production hardware. That is fast enough for a login form and slow enough to make brute-force economically painful.
TLS misconfiguration is its own quiet disaster
A correctly issued certificate is not the same as a correctly configured server. I have seen production load balancers that still accept TLS 1.0, internal services that trust any certificate signed by a permissive corporate CA, and mobile apps that disable certificate validation in development and never re-enable it in release builds.
Two tools end most of the guesswork. Run [SOURCE: Qualys SSL Labs] against every public endpoint and aim for an A or A+ grade. For internal services, write an integration test that connects with an intentionally invalid certificate and asserts the connection is refused. If that test passes, your TLS is real.
What are considered cryptographic failures in code review?
When I review pull requests, I treat the following as automatic blockers, no discussion needed:
- Any string that looks like a key, token, or password committed to the repo. - Calls to `MD5`, `SHA1`, `DES`, `RC4`, or `ECB` mode anywhere. - Use of `Math.random`, `rand`, or any non-CSPRNG function for security tokens. - Custom encryption or signature schemes built from primitives. - TLS verification disabled in HTTP clients, even with a comment saying it is temporary. - Password storage using anything other than a slow KDF. - Encryption keys that travel in environment variables across multiple environments without a clear rotation story.
This list is not exhaustive, but if a PR clears all of these, it has cleared most of A02.
Logging, error messages, and side channels
Cryptography also fails through what surrounds it. Logging a full request body that contains a password, returning a verbose error that distinguishes invalid user from invalid password, or comparing tokens with `===` instead of a constant-time function are all cryptographic errors in spirit. Use timing-safe comparison for any secret. Scrub sensitive fields from logs at the source, not in a downstream pipeline. Treat your error messages as part of your attack surface.
Key takeaways
Cryptographic failures are A02 in the OWASP Top 10 because they sit underneath almost every category of sensitive data your application handles. The fixes are not exotic: classify your data, encrypt it in transit and at rest with modern primitives, hash passwords with a slow KDF, store keys in a managed vault, and use a CSPRNG for every secret. Avoid custom crypto, deprecated algorithms, and hardcoded keys. Most failures of cryptography I see in the wild are not clever attacks, they are tired defaults. Replacing those defaults is the highest-leverage security work you can do this quarter.
If you want a broader walkthrough of the rest of the list, my [INTERNAL LINK: OWASP Top 10 guide] covers all ten categories, and the [INTERNAL LINK: broken access control deep dive] pairs naturally with this one.
Have you shipped, or caught, a nasty cryptographic failure? I would love to hear the story. Find me on [X / Twitter](https://x.com/ru5iru) or [LinkedIn](https://www.linkedin.com/in/ru5iru) and tell me what tipped you off.