Skip to content

Cryptographic failures

CWE-327OWASP A02:2021Updated August 31, 20266 min read

Cryptographic failures is the umbrella term for sensitive data that is inadequately protected because cryptography is missing, outdated or applied incorrectly. Typical examples are passwords hashed with MD5, encryption in ECB mode and home-grown algorithms. The fix is built from standard parts, Argon2id for passwords and an authenticated mode such as AES-GCM for stored data.

Almost every application protects something with cryptography: passwords in the database, a session token in a cookie, personal data in a backup. The algorithms are rarely the weak point, the way they are applied almost always is. Here are the mistakes that come up most often and their replacements.

What are cryptographic failures?

Cryptographic failures is the umbrella term for vulnerabilities in which sensitive data is inadequately protected, because cryptography is missing, outdated or applied incorrectly. OWASP has listed the category as A02 in its Top 10 since 2021, under the earlier name Sensitive Data Exposure.

An everyday comparison: a safe is only as strong as its weakest link, and the heaviest door protects nothing while the key sits under the doormat. AES, in full the Advanced Encryption Standard, is not broken; AES in the wrong mode, with a key from the source code, protects nothing either.

The category comes down to a handful of recurring mistakes:

  • Passwords hashed with MD5, SHA-1 or plain SHA-256. Fast hash functions built for throughput; that speed is what makes them unfit for passwords.
  • Encryption in ECB mode. ECB stands for Electronic Codebook: every sixteen-byte block goes through the algorithm alone, so identical blocks yield identical ciphertext.
  • A hardcoded initialisation vector (IV). An IV from the source code makes encryption deterministic: the same input always produces the same output.
  • Home-grown crypto. An XOR against a fixed word is not encryption, it is encoding.
  • Encryption without authentication. Ciphertext with no integrity check can be altered unnoticed.

How does an attack on weak cryptography work?

An attacker who reaches the user table through SQL injection, a leaked backup or a stolen admin account does not have passwords, only hashes. What happens next depends on the hash function you chose.

Vulnerable:

// Storing at registration
$hash = md5($password);
$pdo->prepare("INSERT INTO users (email, pw_hash) VALUES (?, ?)")
    ->execute([$email, $hash]);

// Checking at login
if (md5($input) === $row["pw_hash"]) {
    login($row);
}

The attacker’s work is offline: they load the hashes into hashcat and try billions of candidates per second from a wordlist. With MD5 that computation is trivial, and common passwords sit ready-made in public lookup tables. Without a salt they crack every account sharing a password at once, while your application never sees a login attempt.

Secure:

// Argon2id: 64 MiB of working memory, 3 iterations
$options = ["memory_cost" => 65536, "time_cost" => 3, "threads" => 2];
$hash = password_hash($password, PASSWORD_ARGON2ID, $options);

// Login, including migration of legacy hashes
if (password_verify($input, $row["pw_hash"])) {
    if (password_needs_rehash($row["pw_hash"], PASSWORD_ARGON2ID, $options)) {
        store_hash($row["id"], password_hash($input, PASSWORD_ARGON2ID, $options));
    }
    login($row);
}

Argon2id is deliberately slow and memory-hungry, and it is the memory that makes parallelising on a graphics card unattractive. The salt is generated automatically and stored inside the result, so identical passwords get different hashes. A dump is still unpleasant, but bulk cracking is off the table.

The second pattern sits in the stored data itself.

Vulnerable:

const crypto = require("node:crypto");

// Key and IV are hardcoded and never change
const KEY = Buffer.from("0123456789abcdef0123456789abcdef");
const IV = Buffer.alloc(16, 0);

// ECB encrypts each block on its own, CBC reuses one fixed IV.
// Neither mode authenticates the ciphertext.
function encrypt(plaintext, mode) {
  const c = crypto.createCipheriv(mode, KEY, mode.endsWith("ecb") ? null : IV);
  return Buffer.concat([c.update(plaintext, "utf8"), c.final()]);
}

Both variants are deterministic, and that is the leak. Under ECB identical blocks stay identical once encrypted, so the structure of your data stays visible; the classic demonstration is an image still recognisable after encryption. Under CBC with a fixed IV, equality between whole messages leaks: anyone reading the table sees which records hold the same value. Without an authentication tag an attacker can also modify ciphertext deliberately, and an application that answers differently to bad padding becomes a padding oracle that recovers the plaintext.

Secure:

const crypto = require("node:crypto");
const key = loadKeyFromKms(); // 32 bytes, not from the repository

function encrypt(plaintext) {
  const iv = crypto.randomBytes(12); // unique per message
  const c = crypto.createCipheriv("aes-256-gcm", key, iv);
  const ct = Buffer.concat([c.update(plaintext, "utf8"), c.final()]);
  return Buffer.concat([iv, c.getAuthTag(), ct]);
}

function decrypt(blob) {
  const d = crypto.createDecipheriv("aes-256-gcm", key, blob.subarray(0, 12));
  d.setAuthTag(blob.subarray(12, 28));
  // final() throws as soon as a single bit has changed
  return Buffer.concat([d.update(blob.subarray(28)), d.final()]).toString();
}

AES-GCM, in full Galois/Counter Mode, encrypts and authenticates in one operation. The nonce is unique per message, sits next to the ciphertext and need not be secret. Tampering produces an error instead of quietly wrong data. If you would rather not pick parameters at all, use libsodium.

AES-GCM comes with one hard rule: never use a nonce twice with the same key. Two messages sharing that combination hand an attacker the difference between the plaintexts and the material to forge valid authentication tags. Generate the nonce randomly per message, or choose XChaCha20-Poly1305, whose nonce is large enough to rule out collisions.

What is the impact of cryptographic failures?

Technically it comes down to data you believed was encrypted. Cracked passwords give access to your application and, because people reuse passwords, to their accounts elsewhere. Badly encrypted fields expose personal or payment data in readable form, and whoever can alter an encrypted cookie sometimes changes a role or an amount.

Commercially it starts with the breach. The question is then whether the data was genuinely unreadable to outsiders; outdated cryptography does not count towards that, so an incident still has to be reported and communicated. Recovery is expensive as well, because resetting every password and re-encrypting every column touches the whole chain.

That is why the rating runs from medium to high. An outdated algorithm on data that is public anyway is a tidy finding without drama. The same mistake on the password table is not.

How do you detect cryptographic failures?

Start in the code. Search for md5, sha1, DES, RC4 and for ECB in a cipher name, and for keys or IVs that appear as literal values. Look at the generator behind tokens and reset links too: Math.random and rand are not cryptographically secure.

In the database the hash format is immediately visible: thirty-two hexadecimal characters point to MD5, forty to SHA-1, while bcrypt starts with $2y$ and Argon2id with $argon2id$. From the outside, encrypt the same value twice and compare the output; identical ciphertext means ECB or a fixed IV. With an encrypted cookie, flip a byte and see whether the application rejects it or carries on.

Scanners find the transport side and known weak ciphers, but not that your storage layer uses ECB or that a key has been the same for years. Static analysis gets further, a code review further still. AssistSec covers the cryptographic choices in your application during a penetration test, reviewing password storage and key management together.

How do you prevent cryptographic failures?

  • Hash passwords with Argon2id. Pick deliberate parameters for memory and iterations, and fall back to bcrypt or scrypt only where Argon2id is unavailable. Plain MD5, SHA-1 or SHA-256 is never an option.
  • Migrate existing hashes at login. On every successful login, check whether the format still meets your standard and rehash if not; for dormant accounts, force a reset.
  • Encrypt with authentication only. Use AES-GCM or XChaCha20-Poly1305, never ECB, with a fresh nonce per message.
  • Do not write your own crypto. Use libsodium or your platform’s standard library; a home-grown construction fails silently, not visibly.
  • Keep keys out of the code. Store them in a vault, use separate keys per purpose, and plan rotation before you need it.
  • Use a secure generator for anything unpredictable. Session tokens and API keys come from crypto.randomBytes, secrets or SecureRandom.
  • Enforce TLS and keep it current. Disable old protocol versions and ciphers, and switch HSTS on.

Sources

Frequently asked questions

Can I still use MD5 or SHA-256 for passwords?

No. MD5, SHA-1 and SHA-256 are designed to process as much data per second as possible, which on a modern graphics card translates into billions of guesses per second. A password hash needs the opposite property: it should be slow and memory-hungry. Use Argon2id, or bcrypt or scrypt if your platform does not offer Argon2id.

What is wrong with ECB mode?

ECB encrypts every sixteen-byte block independently of the rest. Identical blocks therefore produce identical ciphertext, so the structure of your data stays visible; the classic demonstration is an image that is still recognisable after encryption. ECB also offers no protection at all against tampering with the ciphertext.

Argon2id or bcrypt, which should I pick?

Argon2id is preferred, because it demands working memory as well as processing time and so removes much of the advantage of specialised cracking hardware. Bcrypt remains defensible where Argon2id is unavailable, provided you raise the cost factor over time. What you should not do in either case is assemble your own salt-and-hash scheme.

Is HTTPS enough to protect sensitive data?

No. TLS protects data in transit between browser and server and says nothing about how it is stored afterwards. A database holding MD5 password hashes or personal data in ECB is just as exposed on a server that only speaks HTTPS. Treat transport and storage as two separate problems.

Is AES-CBC insecure?

CBC is not broken, but it is a mode without built-in integrity checking and with sharp edges: a unique, unpredictable IV per message is mandatory and the ciphertext has to be authenticated separately. In practice that often goes wrong. AES-GCM or XChaCha20-Poly1305 handles encryption and authentication in one operation and is the better default.

Related articles

Press / to search · Esc