This vulnerability occurs when an application receives or retrieves data (such as cookies, session data, user-provided parameters, or files) but fails to verify that the data has not been tampered with. Even if data is encrypted, encryption alone (especially in modes like CBC without a MAC) does not prevent an attacker from modifying or corrupting the ciphertext, which may lead to predictable changes in the decrypted plaintext (e.g., a “bit-flipping” attack).This flaw also applies to plaintext data where integrity is crucial, like signed JWTs (where the signature must be checked) or cookies where a MAC (Message Authentication Code) should be used to prevent tampering.
This is a design and implementation flaw. Modern frameworks often provide integrity checks by default on sensitive data like cookies and tokens, but custom implementations can easily miss this.Key Remediation Principles:
Use Authenticated Encryption (AEAD): Prefer modern encryption ciphers like AES-GCM or ChaCha20-Poly1305 that bundle encryption (confidentiality) and a MAC (integrity/authenticity) together.
Encrypt-then-MAC: If using older ciphers (like AES-CBC), you must apply a strong MAC (like HMAC-SHA256) to the ciphertext and verify it before decrypting.
Verify Signatures: For signed data (like JWTs), always verify the signature using a secure key before trusting the payload (see CWE-347).
Use Framework Defaults: Rely on built-in framework mechanisms for session and cookie management (e.g., Django/Laravel encrypted cookies, ASP.NET Core Data Protection) as they typically include integrity checks.
# views.py (Django)def set_prefs_unsafe(request): # DANGEROUS: Storing user ID in a cookie without signing. # Attacker can change 'user_id=123' to 'user_id=1' (admin). response.set_cookie('user_prefs', 'user_id=123') return response
Cookies: Use Django’s signed cookies (request.get_signed_cookie(), response.set_signed_cookie()) or encrypted cookies (default session backend) which provide integrity.
Identify data passed between client/server or stored/retrieved that requires integrity (encrypted data, session cookies, tokens).
Encrypted Data: If using CBC, attempt to flip bits in the ciphertext and observe the decrypted result. If using AEAD (GCM), modify any part of the nonce, tag, or ciphertext; verify decryption fails.
Cookies: Modify the value of a signed cookie (like Django’s sessionid or a manually signed cookie). Verify the application rejects it (BadSignature).
Use AES-GCM (AES/GCM/NoPadding). This AEAD cipher mode handles encryption and integrity verification in one step. If CBC must be used, implement an Encrypt-then-MAC scheme (compute HMAC-SHA256 on the ciphertext + IV, append HMAC, and verify HMAC before decrypting).
Identify where encryption is used. If the algorithm is AES/CBC/..., check if an HMAC is also being calculated and verified. If not, it’s vulnerable. If AES/GCM/... is used, it’s generally secure by default. Test by intercepting the encrypted payload (e.g., in a cookie or API request) and modifying one or more bytes. The secure implementation (AES-GCM or CBC+HMAC) should fail decryption with an integrity/tag/MAC error, not just return garbled data.
Identify encryption usage. If CipherMode.CBC is used, check if HMAC is also used and verified before decryption. If AesGcm is used, it’s generally secure. Intercept and modify encrypted payloads (cookies, API data). Verify that tampered payloads cause a CryptographicException (e.g., “Authentication tag mismatch”) rather than returning garbled data.
Use openssl_encrypt with AEAD: Use aes-256-gcm (PHP 7.1+) which provides integrity.
Use Laravel’s Crypt:Crypt::encryptString() and Crypt::decryptString() use an Encrypt-then-MAC scheme by default and are secure against tampering.
Manual Encrypt-then-MAC: If CBC must be used, calculate an HMAC (hash_hmac('sha256', $iv . $ciphertext, $hmac_key, true)) and append it. Verify the HMAC before decrypting.
Identify encryption usage. If openssl_encrypt uses CBC mode, check if HMAC is manually applied and verified. If GCM mode is used, it’s generally secure. Test by intercepting and modifying the Base64 encoded payload (IV, tag, or ciphertext parts). Verify that openssl_decrypt (GCM) returns false or Crypt::decryptString throws a DecryptException.
Setting a cookie with res.cookie containing sensitive state (e.g., userId: 1) without signing it. (Express cookie-session middleware signs by default).
Identify encryption usage. If aes-*-cbc is used, check for crypto.createHmac usage and verification beforecreateDecipheriv. If aes-*-gcm is used, check that setAuthTag() is called on the decipher before final() or data processing. Intercept and modify encrypted payloads (IV, tag, or ciphertext parts) and verify that decryption fails with an integrity error, not just garbled output.
Using OpenSSL::Cipher with aes-256-cbc mode without a MAC. Rails’ ActiveSupport::MessageEncryptor (used for encrypted cookies/credentials) is secure as it uses an Encrypt-then-MAC (AES-CBC + HMAC) scheme by default.
# app/controllers/legacy_controller.rbdef set_pref # DANGEROUS: Plain cookie, value can be tampered by client. cookies[:user_role] = 'user'enddef check_pref # DANGEROUS: Trusts the value read from the cookie. # Attacker modifies cookie to user_role=admin if cookies[:user_role] == 'admin' # ... grant admin access ... endend
Use ActiveSupport::MessageEncryptor: For encrypting data, rely on Rails’ built-in MessageEncryptor which provides authenticated encryption (AEAD, Encrypt-then-MAC).
Use aes-256-gcm: If using OpenSSL::Cipher manually, switch to GCM mode which includes integrity.
Use Signed/Encrypted Cookies: Use cookies.signed[:key] (provides integrity) or cookies.encrypted[:key] (provides integrity + confidentiality) for storing sensitive data in cookies.
# lib/encryption.rb (Secure - AES-GCM)require 'openssl'require 'base64' # For easier transportdef encrypt_gcm(key, data) cipher = OpenSSL::Cipher.new('aes-256-gcm') cipher.encrypt cipher.key = key iv = cipher.random_iv # 12 bytes cipher.auth_data = "" # Optional AAD encrypted = cipher.update(data) + cipher.final tag = cipher.auth_tag # 16 bytes # SECURE: Store IV + Tag + Ciphertext Base64.strict_encode64(iv + tag + encrypted)enddef decrypt_gcm(key, encoded_data) raw = Base64.strict_decode64(encoded_data) cipher = OpenSSL::Cipher.new('aes-256-gcm') iv_len = cipher.iv_len # 12 bytes tag_len = 16 # Assuming 16-byte tag iv = raw[0...iv_len] tag = raw[iv_len...(iv_len + tag_len)] encrypted_data = raw[(iv_len + tag_len)..-1] cipher.decrypt cipher.key = key cipher.iv = iv cipher.auth_tag = tag # Set the expected tag cipher.auth_data = "" # Must match AAD from encryption # SECURE: final() will raise OpenSSL::Cipher::CipherError if tag doesn't match begin decrypted = cipher.update(encrypted_data) + cipher.final return decrypted rescue OpenSSL::Cipher::CipherError => e puts "Decryption failed! Data tampered or invalid key/tag." return nil endend# app/controllers/cookie_controller.rb (Secure Cookie)def set_pref_secure # SECURE: Uses encrypted cookie store (integrity + confidentiality) # Requires a secret_key_base or per-cookie secret. cookies.encrypted[:user_role] = 'user' # Or signed cookie (integrity only): # cookies.signed[:user_role] = 'user' render plain: "Secure preference set"enddef check_pref_secure # SECURE: Returns nil if cookie is tampered with or invalid. role = cookies.encrypted[:user_role] if role == 'admin' # This check is safe, as role could not be tampered with # ... grant admin access (after server-side authorization!) ... end # ...end
Identify encryption usage. If aes-*-cbc mode is used manually, check for HMAC verification. If aes-*-gcm is used, check auth_tag is set/verified. Intercept encrypted data (cookies, payloads) and modify bytes; verify decryption fails with an integrity error. Modify signed/encrypted cookies (_my_app_session, custom ones) and verify they are rejected as invalid.