Safeguard
Application Security

Detecting weak cryptographic algorithms in code

SHAttered proved a SHA-1 collision for ~$110,000 in 2017. NIST retires SHA-1 entirely by Dec 31, 2030 — here's how to find and fix MD5/SHA-1 in your code now.

Safeguard Research Team
Research
7 min read

On February 23, 2017, researchers from Google and CWI Amsterdam published SHAttered, the first practical SHA-1 collision: two distinct PDFs that hashed to the same value, produced by roughly 9.2 quintillion (2^63.1) SHA-1 computations at an estimated cost of $75,000–$120,000 in cloud compute. That single demonstration — call it $110,000 — collapsed a two-decade assumption that SHA-1 was safe enough for signatures and certificates, and it followed thirteen years after Wang, Feng, Lai, and Yu first showed practical MD5 collisions in 2004. NIST had already flagged the writing on the wall in Special Publication 800-131A back in 2011, disallowing SHA-1 for federal digital signature generation after December 31, 2013, and in December 2022 NIST set a hard retirement date: SHA-1 will be fully disallowed by December 31, 2030. Yet both algorithms remain scattered across production codebases today — in hashlib.md5() calls, MessageDigest.getInstance("SHA1") invocations, and legacy password hashing that predates bcrypt. This post covers how weak crypto actually gets found in a real codebase, why grep isn't enough, and what to swap it for.

Why are MD5 and SHA-1 still considered broken?

MD5 and SHA-1 are broken because their core property — that no two inputs should produce the same hash — has been demonstrated to fail in practice, not just in theory. Wang, Feng, Lai, and Yu published a practical MD5 collision method in 2004, and chosen-prefix collision techniques refined afterward made it possible to craft two files with attacker-chosen, meaningfully different content (colliding X.509 certificates, colliding executables) that still hash identically. SHAttered did the equivalent for SHA-1 in February 2017: the CWI Amsterdam and Google team spent an estimated 6,500 CPU-years plus 100 GPU-years, at a cloud cost documented on shattered.io as roughly $110,000, to produce two distinct PDFs sharing one SHA-1 digest. Once a collision is affordable, an attacker can substitute a malicious file, certificate, or code signature for a legitimate one without detection, because the systems trusting that hash have no way to tell them apart. Following SHAttered, Chrome and Firefox stopped trusting SHA-1 certificates, certificate authorities stopped issuing them, and Git added the sha1collisiondetection library in 2017 to catch collision attempts on commit objects.

Where does weak crypto actually hide in a codebase?

Weak crypto hides in three predictable places: integrity checks, password storage, and legacy interoperability code. The most common pattern is a developer reaching for a hash function to "checksum" or "fingerprint" something — a file upload, a cache key, a deduplication ID — and picking MD5 or SHA1 purely because it's the shortest, most familiar API call, in Python's hashlib, Java's MessageDigest, or Node's crypto.createHash. The riskier pattern is password storage: code that runs a user's password through a single pass of SHA-256 or MD5 and stores the digest, with no salt and no deliberate slowness, leaving it trivially crackable with commodity GPU rainbow-table attacks. The third hiding spot is TLS and cipher configuration — legacy SSLContext or Java security-provider settings that still permit DES, 3DES, or RC4 cipher suites for backward compatibility with old clients, often left in place from a decade-old config file nobody has revisited.

How do you find weak crypto usage with static analysis instead of grep?

Static analysis finds weak crypto reliably because it parses the abstract syntax tree and resolves what a call actually does, where a plain grep for "md5" or "sha1" produces both false positives (a variable literally named sha1_migration_flag) and false negatives (an aliased import like from hashlib import md5 as h). An AST-aware SAST engine matches the resolved call target — hashlib.md5(...), MessageDigest.getInstance("MD5"), crypto.createHash('sha1') — regardless of import aliasing or wrapper functions, and can trace whether the output of that call flows into a password-storage sink versus a cache-key sink. Safeguard's first-party SAST engine, for example, traces data from source to sink across functions and files and attaches a CWE mapping and severity to each finding, which is exactly the mechanism needed to tell "MD5 used to fingerprint an uploaded file for deduplication" apart from "MD5 used to hash a password before storing it" — the same function call, two very different severities. SCA and dependency scanning add a second layer, flagging crypto libraries pinned to versions with insecure defaults even when your own code never calls the weak primitive directly.

Why isn't every MD5 or SHA-1 call a finding worth fixing?

Not every MD5 or SHA-1 call is a security bug, because both algorithms remain fine for non-adversarial integrity checks where nobody is trying to forge a match — accidental-corruption checksums, content-addressable cache keys, and Git's own object hashing (which Git has kept on SHA-1, hardened with collision detection, rather than ripping out entirely). The distinction that matters is adversarial context: does an attacker control one side of the comparison, and does a successful collision let them substitute malicious content for legitimate content undetected? A build system that MD5-hashes a downloaded dependency purely to skip re-downloading an unchanged file is low risk; a system that verifies a software update's authenticity using an MD5 or SHA-1 digest supplied over an untrusted channel is not. Flagging every occurrence by algorithm name alone produces the kind of noisy backlog that trains developers to ignore scanner output altogether — the fix is triaging by data flow and use case, which is precisely what dataflow-aware SAST is built to do instead of a bare pattern match.

What should you replace deprecated crypto with?

Replace deprecated crypto based on what it's actually protecting, not with a single universal substitute. For general-purpose hashing and integrity — file fingerprints, digital signatures, digest fields in protocols — move to SHA-256 or SHA-3, or to BLAKE2/BLAKE3 where raw speed matters and the ecosystem support exists. For password storage, never use a raw hash function at all, fast or slow; use a purpose-built, memory-hard KDF: Argon2 is the current OWASP recommendation, with bcrypt and scrypt as established fallbacks where Argon2 support is unavailable. For message authentication, use HMAC-SHA256 rather than a bare hash concatenated with a secret. For symmetric encryption, retire DES, 3DES, and RC4 in favor of AES-256-GCM or ChaCha20-Poly1305, both of which provide authenticated encryption so tampering is detected, not just concealed. None of these swaps are drop-in one-liners — password migration in particular requires re-hashing on next login rather than a bulk conversion — so budget the remediation as a project with a rollout plan, not a single find-and-replace commit.

How Safeguard helps

Safeguard's SAST engine traces data flow from source to sink across JavaScript/TypeScript, Python, and Java code, so a weak-crypto call site is evaluated by what it protects rather than flagged uniformly by function name — each finding carries the dataflow trace, a CWE mapping, and a severity so a password-storage MD5 call and a cache-key MD5 call don't compete for the same triage priority. Because SAST findings share Safeguard's unified findings model with SCA and secrets scanning, a legacy crypto library pinned in a dependency manifest and a raw MessageDigest.getInstance("SHA1") call in your own code surface in the same queryable view, correlated rather than scattered across separate tool outputs. That's the difference between a backlog of a few hundred "MD5 found" alerts nobody triages and a short list of the handful that are actually protecting something an attacker can reach.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.