ASVS 5.0 V11.3.3 (Level 2) requires that encrypted data is protected against unauthorized modification, preferably with an approved authenticated encryption (AEAD) method, or else an approved cipher combined with an approved MAC.
Why it matters
Encryption hides data. It does not stop someone from changing it. With unauthenticated modes such as CBC or CTR, an attacker who cannot read the ciphertext can still flip bits to change the plaintext in predictable ways, swap in records from another user, or replay old values. The application then decrypts garbage or attacker chosen data and trusts it.
How to implement it
Use AEAD: AES GCM or ChaCha20 Poly1305. They encrypt and authenticate in one step, and decryption fails if anything was altered.
Bind each ciphertext to its context using the associated data parameter, so a valid blob cannot be moved to a different row or user.
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
def protect(key, user_id: int, field: str, value: bytes) -> bytes:
nonce = os.urandom(12)
aad = f"{user_id}:{field}".encode()
return nonce + AESGCM(key).encrypt(nonce, value, aad)
def unprotect(key, user_id: int, field: str, blob: bytes) -> bytes:
aad = f"{user_id}:{field}".encode()
return AESGCM(key).decrypt(blob[:12], blob[12:], aad) # raises on any change
If an AEAD mode is not available, use encrypt then MAC with separate keys (see V11.3.5), verify the MAC first, and compare in constant time.
Treat a decryption failure as a security event: reject the request, return a generic error and record it in private logs. Never "repair" or default the field.
Also protect data that lives outside the cipher text: version headers, key ids and lengths. Include them in the associated data so an attacker cannot downgrade the scheme by editing the header.
How to test it
Take a stored ciphertext and flip one bit at the start, middle and end. Decryption must fail each time. Then copy a valid ciphertext from user A's record into user B's record and confirm decryption fails because the context differs. Finally, swap in an older valid ciphertext for the same record and decide whether replay matters for that field; if it does, add a version counter inside the authenticated data.
Search code for CBC, CTR, CFB and OFB without a MAC call nearby, and for GCM calls that pass no associated data when a natural context exists.
Common mistakes
- Encrypting database fields with CBC and no integrity check
- Ignoring the tag, or catching the exception and continuing
- Reusing a nonce with the same key (see V11.3.4)
- No context binding, so ciphertexts can be moved between rows
- Using an unkeyed hash as an integrity check; anyone can recompute it
Quick check
Does encryption alone stop tampering?
No. Without authentication an attacker can alter ciphertext and change what decrypts.
What does associated data add?
It ties a ciphertext to its context, such as user and field, so it cannot be reused elsewhere.