~/teardowns/TD-034.md
←Back to teardowns
# TD-034HighSecurity

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)

secret.phpphp
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// secret.php
function encrypt($data) {
return base64_encode($data); // reversible by anyone, no key
}
function decrypt($data) {
return base64_decode($data);
}
$key = encrypt('sk_live_1234567890abcdef');
// store $key in the database
?>

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 →

Sources

Is your codebase vulnerable to this?

Run Diagnostic on My Project →