The Encryption That Wasn't
base64_encode() dressed up as cryptography
Published Oct 29, 2026
Executive Summary
A vibe-coded app 'encrypts' its gateway API key with base64_encode() before storing it — and names the helper functions encrypt() and decrypt(). base64 is an encoding, not encryption: it is reversible by anyone, with no key. The secret is plaintext in a costume, and the naming makes reviewers skip it.
The Prompt
“Encrypt the payment gateway API key before storing it in the database.”
The Flaw
base64_encode() produces ciphertext-looking output, so the functions read like real cryptography. But base64 is a reversible encoding with no key: 'decrypting' is just decoding. Anyone who reads the database row — or a backup, or a log line — recovers the plaintext key in one call. The encryption is theater, and the function names are what get reviewed, not what they do.
Risk & Impact
[Security] The gateway key sits in the database in plaintext-equivalent form. The names encrypt()/decrypt() create false confidence, so the flaw survives review until the first leak.
Cost: One leaked database dump or backup exposes the live payment key.
Code Comparison
❌ Vulnerable (AI generated)
❌ Vulnerable (AI generated)
✅ Hardened (production ready)
The Fix
Use real authenticated encryption (openssl_encrypt with aes-256-gcm, or libsodium) with a key held outside the database — and treat base64 as a transport encoding only.
Prevention
- [linter] Semgrep/PHPStan rules that flag base64_encode()/str_rot13()/md5() calls inside functions named encrypt/decrypt or around secret handling.
- [ci] Fail the build if any 'encryption' helper resolves to base64, XOR, rot13, or unsalted md5.
- [manual] Name the primitive in the prompt (rule 10: libsodium/openssl), and explicitly forbid hand-rolled crypto (rule 6).
Why This Happened
Root cause: the prompt omitted Rule 6 and Rule 10 — the guardrails that would have caught this. See the full guide →
