In the previous lesson we secured who can reach each piece of data. Now we protect the data itself. A02 – Cryptographic Failures groups everything that goes wrong when sensitive information isn't encrypted, is encrypted with weak algorithms, or when the protection of keys and secrets is mishandled. In the 2021 Top Ten this category absorbed the old "Sensitive Data Exposure" from 2017: the focus shifted from the consequence (the exposed data) to the cause (the cryptographic failure that allowed it).

The impact falls directly on confidentiality: passwords, personal data, card numbers (PAN) or tokens that end up in the wrong hands because they travel in the clear, are stored unencrypted, or are protected with broken algorithms. In BazarNube we'll find two classic gems of legacy code: passwords stored with an obsolete hash, and card numbers kept "for convenience." Let's fix them.

Legal and ethical notice: the examples use fake data. Do not process or store real card data outside a certified environment, and test only on your own systems or with explicit authorization.

Contents

  1. Identifying sensitive data and its lifecycle
  2. Protection in transit: well-configured TLS
  3. Protection at rest: encryption and key management
  4. Password hashing: bcrypt/argon2 vs. MD5/SHA1
  5. Data you simply shouldn't store (PAN, CVV)
  6. Secret and key management
  7. Common mistakes and tips
  8. Exercises and solutions

  1. Identifying sensitive data and its lifecycle

Before encrypting, you need to know what to protect. In BazarNube we classify:

Data Sensitivity In transit At rest
Password Critical TLS Slow hash with salt (never reversible encryption)
Personal data (name, address) High TLS Encrypted in the DB
PAN (card number) Critical (PCI-DSS) TLS Preferably don't store; if mandatory, tokenize/encrypt
CVV Critical TLS Forbidden to store, even encrypted
Session token High TLS + secure cookie Do not persist in the clear

The starting rule: minimize. The safest data is the data you don't keep.

  1. Protection in transit: TLS

All of BazarNube's traffic (React front end, Express API, Java module) must travel over HTTPS/TLS. Frequent mistakes:

  • Internal endpoints "HTTP only" because "they're on the private network" (an attacker who gets onto the network listens in on them).
  • Poorly configured TLS: obsolete versions (SSLv3, TLS 1.0/1.1), weak ciphers.
  • Missing HSTS, which forces the browser to always use HTTPS.
// app.js — force HTTPS and enable HSTS in Express
const helmet = require('helmet');
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true, preload: true }));
// Redirect HTTP -> HTTPS (behind a proxy/TLS terminator)
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] === 'http') {
    return res.redirect(301, 'https://' + req.headers.host + req.url);
  }
  next();
});

HSTS with a one-year maxAge and includeSubDomains prevents downgrade attacks (SSL stripping). We'll look at security headers in more detail in A05 (03-06); here we care about the cryptographic part: always encrypt the channel.

  1. Protection at rest: encryption and key management

Customers' personal data is encrypted in PostgreSQL. Distinguish between:

  • Symmetric encryption (AES-256-GCM) for data you need to recover in the clear (e.g. a shipping address).
  • Hashing (irreversible) for what you only need to compare (passwords). Never encrypt a password reversibly.
// crypto/fieldCrypto.js — authenticated field encryption with AES-256-GCM
const crypto = require('crypto');
const KEY = Buffer.from(process.env.FIELD_KEY, 'hex'); // 32 bytes, from the secrets manager

function encrypt(plaintext) {
  const iv = crypto.randomBytes(12);               // unique IV per operation
  const cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv);
  const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();                 // integrity (GCM)
  return Buffer.concat([iv, tag, enc]).toString('base64');
}

function decrypt(blob) {
  const raw = Buffer.from(blob, 'base64');
  const iv = raw.subarray(0, 12), tag = raw.subarray(12, 28), data = raw.subarray(28);
  const decipher = crypto.createDecipheriv('aes-256-gcm', KEY, iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
}

Key points: AES-GCM provides encryption and authenticity (it detects tampering); the IV is random and unique per operation; the key lives in a secrets manager, never in the code.

  1. Password hashing: the BazarNube case

Here's the legacy-code classic. User registration stored passwords like this:

// auth.js — VULNERABLE: fast hash, no salt, broken
const crypto = require('crypto');
function storePassword(user, password) {
  const hash = crypto.createHash('md5').update(password).digest('hex');
  return db.query('UPDATE users SET pwd = $1 WHERE id = $2', [hash, user.id]);
}

Why this is a disaster

  • MD5/SHA1 are broken for this use: they're fast and have known collisions.
  • No salt: two users with the same password get the same hash; a rainbow table cracks them instantly.
  • Fast = bad for passwords: an attacker with a GPU tries billions of MD5 hashes per second. If they steal the users table, they recover most passwords in hours.

Illustrative exploitation

With the leaked DB, the attacker takes the hash 5f4dcc3b5aa765d61d8327deb882cf99, looks it up in any public MD5 hash database, and instantly gets the password password. With no salt and no cost, it's almost instantaneous.

Fixed code with bcrypt

// auth.js — SECURE: bcrypt with automatic salt and adjustable cost
const bcrypt = require('bcrypt');
const COST = 12; // work factor; raise it over time

async function storePassword(user, password) {
  const hash = await bcrypt.hash(password, COST); // generates and includes the salt
  return db.query('UPDATE users SET pwd = $1 WHERE id = $2', [hash, user.id]);
}

async function verifyPassword(password, storedHash) {
  return bcrypt.compare(password, storedHash); // constant-time comparison
}

bcrypt generates a random salt per password (included in the hash itself) and is deliberately slow (the cost factor 12 forces many iterations), which makes mass cracking impractical. The comparison runs in constant time, avoiding timing attacks.

Algorithm comparison

Algorithm Suitable for passwords? Reason
MD5 / SHA1 No Broken, fast, no salt
SHA-256 "plain" No Too fast for passwords
bcrypt Yes Slow, salted, battle-tested
argon2id Yes (preferred today) Resistant to GPU/ASIC, memory cost
PBKDF2 Acceptable Valid with many iterations (legacy/FIPS use)

For a new service, argon2id is the current recommendation of the OWASP Password Storage Cheat Sheet; bcrypt remains a solid and widely available option.

Migration without knowing the passwords

BazarNube can't "decrypt" the old MD5 hashes to rehash them. The strategy is to rehash on the next valid login: if the user succeeds against the old hash, it's recomputed with bcrypt and marked as migrated. Those who don't return are forced to reset after a deadline.

async function login(email, password) {
  const u = await getUser(email);
  if (u.pwd_alg === 'md5') {
    if (md5(password) !== u.pwd) return fail();
    await storePassword(u, password);          // rehash with bcrypt
    await setAlg(u, 'bcrypt');
    return ok(u);
  }
  return (await verifyPassword(password, u.pwd)) ? ok(u) : fail();
}

  1. Data you shouldn't store

The Java billing module stored the full card number (PAN) "to show the history." This falls within the scope of PCI-DSS and is an enormous risk.

  • The CVV can never be stored, not even encrypted, under any circumstances.
  • The PAN should only be stored if it's essential, encrypted or tokenized.
  • The usual approach is to delegate payment to a provider (PSP) that returns a token; BazarNube keeps only the last 4 digits for display, and nothing else.
// PaymentRecord.java (legacy module) — VULNERABLE
record PaymentRecord(String pan, String cvv, BigDecimal amount) {}  // stores PAN and CVV
// PaymentRecord.java — SECURE: only a PSP token and the last 4 digits
record PaymentRecord(String pspToken, String last4, BigDecimal amount) {}
// The full PAN and the CVV never touch our database.

Fewer sensitive data stored = smaller exposure surface and smaller regulatory scope.

  1. Secret and key management

AES-256 is useless if the key sits in the Git repository.

flowchart LR
  A[BazarNube app] -->|requests secret at startup| B[Secrets manager]
  B --> A
  C[Git repository] -. never contains secrets .-> A

Good practices:

  • Secrets outside the code: injected environment variables or a secrets manager (Vault, AWS Secrets Manager, etc.). Never in Git, nor in a versioned .env.
  • Periodic key rotation and rotation upon suspicion of compromise.
  • Key separation per environment (dev/staging/prod) and per purpose.
  • If a secret leaks in a commit, rotate it: purging it from history isn't enough; you have to assume it's compromised.

Common Mistakes and Tips

  • Encrypting passwords reversibly. Passwords are hashed (bcrypt/argon2), not encrypted.
  • Using plain SHA-256 for passwords. It's fast; you need a slow algorithm with salt.
  • Storing the CVV or full PAN without need. Delegate to a PSP and keep only a token.
  • HTTP on the internal network. Encryption in transit applies inside the perimeter too.
  • Keys in the repository. Use a secrets manager; rotate anything that leaked.
  • Reusing the IV in AES. Each operation needs a unique IV; with GCM, reusing it breaks security.
  • Tip: classify your data first. You can't protect what you don't know you have.

Exercises

Exercise 1. A colleague proposes storing passwords with SHA-256 + salt. Does it solve the MD5 problem? What would you recommend?

Exercise 2. Spot the two cryptographic failures in this snippet and fix them:

const key = 'super-secret-key-123';
const iv = Buffer.alloc(16, 0); // fixed all-zeros IV
const cipher = crypto.createCipheriv('aes-256-cbc', key.padEnd(32,'0'), iv);

Exercise 3. BazarNube needs to show "card ending in 4242" in the history. What exactly should it store, and what should it not?

Solutions

Solution 1. Adding salt to SHA-256 eliminates rainbow tables, but SHA-256 is still too fast: a brute-force attack with a GPU remains viable. The recommendation is a slow algorithm designed for passwords: argon2id (preferred) or bcrypt, which incorporate salt and adjustable cost.

Solution 2. Two failures: (1) the IV is fixed (all zeros): it breaks confidentiality when encrypting repeatedly; it must be random per operation (crypto.randomBytes). (2) The key is a string in the code, padded with zeros: it must be a random 32-byte key coming from the secrets manager. Also, prefer AES-256-GCM (authenticated) over CBC. See the implementation in section 3.

Solution 3. Store only: a token from the payment provider (for charges/reference) and the last 4 digits (last4), plus perhaps the brand (Visa/Mastercard). Never the full PAN or the CVV.

Conclusion

A02 reminds us that protecting data is a problem of cause (cryptography and key management), not just of consequence. In BazarNube's backlog we noted: TLS + HSTS on all traffic, AES-256-GCM for personal data at rest, migrating passwords from MD5 to bcrypt/argon2 via rehash on login, removing PAN/CVV in favor of PSP tokens, and moving all secrets to a manager with rotation.

Backlog entry — A02: replaced MD5 hashing with bcrypt (cost 12) with a migration plan; removed PAN/CVV storage in the Java module; encrypted personal fields with AES-GCM; moved secrets out of Git.

We've protected the data in transit and at rest. But there's a family of failures where the user's data slips into an interpreter and takes control: injection. That's the next lesson, A03:2021 – Injection, with SQLi in BazarNube's Node/PostgreSQL API and an injection in the legacy Java module.

OWASP Course: Guidelines and Standards for Web Application Security

Module 1: Introduction to OWASP

Module 2: Main OWASP Projects

Module 3: OWASP Top Ten 2021 in Depth

Module 4: OWASP ASVS (Application Security Verification Standard)

Module 5: OWASP SAMM (Software Assurance Maturity Model)

Module 6: OWASP ZAP (Zed Attack Proxy)

Module 7: Best Practices and Recommendations

Module 8: Practical Exercises and Case Studies

Module 9: Assessment and Certification

© Copyright 2026. All rights reserved