DEV Community

Olawale Afuye
Olawale Afuye

Posted on

Cryptographic Failures: The Silent Killer in Your Codebase (OWASP #2)

You ship a feature. Tests pass. Deployment goes smooth. Everyone's happy.

Meanwhile, somewhere in your codebase, you're storing passwords with MD5.

And someone, right now, is cracking them in under a second.

That's the thing about Cryptographic Failures — they don't throw errors. They don't break your CI pipeline. They sit quietly in production until the day they don't.


What Are Cryptographic Failures?

OWASP ranks them #2 on the Top 10 list of most critical web application vulnerabilities. Not #7. Not #5. Number two.

And the definition is deceptively simple: sensitive data is not protected by cryptography — or it's protected badly.

That second part is where most developers get caught. It's not that they skipped encryption entirely. It's that they used the wrong algorithm, mismanaged their keys, or trusted a default that hasn't been safe since 2004.

The result is the same either way: unauthorized access to data that was supposed to be locked.


Why Does This Keep Happening?

Three root causes show up again and again.

1. Weak or Deprecated Algorithms

Not all encryption is created equal. Some algorithms that were considered secure a decade ago are now trivially breakable with modern hardware.

The most common offender? MD5 for password hashing.

MD5 was never designed for passwords — it was designed for fast checksums. "Fast" is the exact opposite of what you want when hashing credentials. Fast means attackers can run billions of attempts per second against a leaked hash database.

Here's a concrete example of what not to do:

import hashlib

# ❌ Never do this
password = "mypassword123"
hashed = hashlib.md5(password.encode()).hexdigest()
Enter fullscreen mode Exit fullscreen mode

This is what led to the Freecycle data breach — a real-world incident where attackers accessed user credentials because the platform was using MD5. Once they had the hash database, cracking the passwords wasn't a challenge. It was a formality.

What you should use instead:

import bcrypt

# ✅ Use a proper password hashing algorithm
password = b"mypassword123"
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
Enter fullscreen mode Exit fullscreen mode

bcrypt, argon2, and scrypt are slow by design. That's the point.

The same principle applies to transport protocols. SSL 2.0, SSL 3.0, and TLS 1.0/1.1 are all deprecated. If your server still supports them, you're offering attackers a downgrade path. TLS 1.3 is the minimum you should be running.


2. Poor Key Management

You encrypted the data.

You stored the encryption key in the same repository.

Congratulations — you haven't protected anything.

This is embarrassingly common. Developers hardcode secrets directly into source code, commit them to GitHub (sometimes public repos), and ship them to production. The encryption is technically present. The protection is not.

// ❌ This is not secret management
const SECRET_KEY = "my_super_secret_key_123";
Enter fullscreen mode Exit fullscreen mode

The fix isn't complicated, but it requires intentionality:

// ✅ Pull from environment or a secrets manager
const SECRET_KEY = process.env.SECRET_KEY;
Enter fullscreen mode Exit fullscreen mode

Better yet, don't use environment variables for truly sensitive credentials. Use a dedicated secrets manager — AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager. These give you rotation, access control, audit logs, and a single source of truth for your credentials.


3. No Encryption at Rest or in Transit

Some applications simply don't encrypt sensitive data at all.

No HTTPS enforcement. Database fields stored in plaintext. PII sitting in logs. Backups with no encryption.

If your threat model includes "what happens when our database is dumped?" — and it should — plaintext storage is catastrophic. Encryption at rest means that even if an attacker exfiltrates your data, they get ciphertext, not customer information.


How to Prevent Cryptographic Failures

Prevention isn't one thing. It's a stack of practices.

Use Modern, Appropriate Algorithms

Use Case Recommended Avoid
Password hashing bcrypt, argon2, scrypt MD5, SHA-1, plain SHA-256
Data encryption AES-256-GCM DES, 3DES, RC4
Transport security TLS 1.3 SSL, TLS 1.0/1.1
Key exchange ECDH, RSA-2048+ Anything < 2048-bit RSA

Manage Secrets Properly

  • Never hardcode credentials in source code
  • Never commit .env files to version control (add to .gitignore immediately)
  • Use a secrets manager for production
  • Rotate keys regularly and have a rotation plan
  • Enforce least-privilege access to secrets

Bake Security Into Your Dev Workflow

The best time to catch cryptographic failures is before they reach production. These four tool categories should be in your pipeline:

SAST — Static Application Security Testing

Scans your source code without running it. Catches hardcoded secrets, insecure function calls, and deprecated API usage.

→ Try: Bandit (Python), Semgrep

DAST — Dynamic Application Security Testing

Tests your running application. Finds vulnerabilities that only appear at runtime — weak cipher suites, missing security headers, misconfigured TLS.

→ Try: OWASP ZAP

Secrets Detection

Scans your git history and codebase for leaked credentials, API keys, and tokens. Because git push is forever.

→ Try: GitLeaks, TruffleHog

SCA — Software Composition Analysis

Audits your dependencies for known vulnerabilities. That npm package you haven't updated in two years? It might be pulling in an insecure crypto library.

→ Try: Trivy, npm audit, Snyk


A Practical Checklist

Before you ship, run through these:

  • [ ] Are passwords hashed with bcrypt, argon2, or scrypt?
  • [ ] Is TLS 1.3 enforced? SSL and early TLS versions disabled?
  • [ ] Are encryption keys stored outside the codebase?
  • [ ] Is sensitive data encrypted at rest in the database?
  • [ ] Are .env files in .gitignore?
  • [ ] Is SAST running in your CI pipeline?
  • [ ] Have you scanned your git history for leaked secrets?
  • [ ] Are dependencies up to date and audited?

The Uncomfortable Truth

Most cryptographic failures aren't the result of developers not caring.

They're the result of developers moving fast, inheriting legacy code, or trusting defaults that were never safe to trust. MD5 was everywhere in tutorials for years. Hardcoded secrets are convenient. "We'll fix it later" is a sentence said in every company on earth.

The problem is that "later" sometimes arrives looking like a breach notification email.

Cryptography isn't optional infrastructure. It's the floor. Everything else you build sits on top of it — and if the floor is weak, it doesn't matter how good the rest of the building is.


Further Reading


If this helped clarify something that's been fuzzy, share it with a teammate who's still using MD5 somewhere. You'd be doing them a favour.

Top comments (4)

Collapse
 
harjjotsinghh profile image
Harjot Singh

"Silent killer" is the right phrase because crypto failures are the bugs that pass every test, ship clean, and only surface when someone's already exploited them. The classics are exactly the ones that look fine: rolling your own crypto, using a fast hash (MD5/SHA1, or plain SHA-256) for passwords instead of bcrypt/argon2, hardcoded keys, ECB mode, missing TLS on an internal hop, predictable IVs/tokens. None throw an error, all are catastrophic, and the codebase looks healthy the whole time. That invisibility is exactly why this can't be a "remember to do it right" problem - it has to be defaults and automated checks, because the failure mode is silence.

This is why I treat security as a structural-defaults + verification problem, not developer-discipline - the silent ones never get caught by "be careful." It's core to how I build Moonshift, the thing I work on: a multi-agent pipeline that takes a prompt to a deployed SaaS, where secure crypto/secret-handling patterns are wired as verified defaults rather than left for someone to remember (and an AI will absolutely generate a confident MD5-password line if you let it). Multi-model routing keeps a build ~$3 flat, first run free no card. Genuinely important post - the OWASP series is great public-good content. Which crypto failure do you see most in real codebases - weak password hashing, or hardcoded/poorly-managed keys? Those two are neck-and-neck in everything I've reviewed.

Collapse
 
walosha profile image
Olawale Afuye • Edited

I'd say both, but leaked keys edge it for me.

Weak hashing is bad, but at least security reviews and penetration tests sometimes catch it. Leaked secrets are often much quieter. I've seen cases where a key was removed from the latest commit and everyone considered the issue closed, even though the secret was still sitting in Git history.

Once a key hits a repository, I assume it's compromised. Removing it isn't enough—you need to revoke it and issue a new one. That's why I'm a big fan of automated secret scanning and enforced key rotation. The dangerous security failures are usually the ones that don't break anything and therefore don't get attention.

Collapse
 
circuit profile image
Rahul S

Agreed on leaked keys being the quieter failure mode. There's a second persistence vector beyond git history that catches teams off guard though: CI/CD build logs. A key gets printed in a debug step in GitHub Actions, the developer removes it from code and rotates it, but that Actions run log from three months ago is still accessible via the API and contains the old key in plaintext. Most secret scanning tools focus on repos and commits — they don't touch historical build artifacts, and BFG/filter-branch can't help you with logs that live outside git entirely. If your pipeline ever ran with LOG_LEVEL=debug, it's worth assuming everything in the environment was logged somewhere that isn't getting cleaned up.

Thread Thread
 
walosha profile image
Olawale Afuye

thats for bringing it up. I never knew about this. thanks!