CWE-261: Weak Encoding for Password
Overview
Weak encoding (Base64, XOR, ROT13, URL encoding) is not cryptography and provides no security. Encoded passwords are trivially reversible and offer zero protection. Passwords must be hashed with strong algorithms (bcrypt, Argon2), not encoded. Encoding is for data representation, not security.
OWASP Classification
A04:2025 - Cryptographic Failures
Risk
Critical: Base64/encoded passwords are instantly reversible (echo <base64> | base64 -d), provide no protection against database compromise, violate compliance requirements, create false sense of security, and are equivalent to plaintext storage for security purposes.
Remediation Steps
Core principle: Never rely on weak encodings for passwords; use proper cryptographic password hashing and secret storage.
Locate Weak Password Encoding
When reviewing security scan results:
- Search for Base64 encoding: Look for
btoa(),atob(),base64_encode(),base64.b64encode() - Check for XOR operations: Find XOR on password strings
- Review password storage: Check database schema for VARCHAR password columns
- Look for reversible operations: Find URL encoding, hex encoding on passwords
- Check password comparison: Look for decoded password comparisons
Vulnerable patterns:
# Base64 encoding - REVERSIBLE!
import base64
password_encoded = base64.b64encode(password.encode())
# XOR with static key - TRIVIAL TO BREAK!
password_xor = ''.join(chr(ord(c) ^ 0x42) for c in password)
# ROT13 - NOT ENCRYPTION!
password_rot13 = password.translate(str.maketrans(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'
))
Use Cryptographic Hashing, Not Encoding (Primary Defense)
import base64
import bcrypt
from argon2 import PasswordHasher
# VULNERABLE - Base64 encoding
def store_password_base64_bad(username, password):
# Base64 is ENCODING, not encryption!
password_encoded = base64.b64encode(password.encode()).decode()
# Trivially reversible:
# >>> base64.b64decode('cGFzc3dvcmQxMjM=').decode()
# 'password123'
db.execute(
"INSERT INTO users (username, password) VALUES (?, ?)",
(username, password_encoded)
)
def verify_password_base64_bad(username, password):
user = db.get_user(username)
password_decoded = base64.b64decode(user['password']).decode()
return password == password_decoded # Comparing plaintext!
# VULNERABLE - XOR "encryption"
def store_password_xor_bad(username, password):
# XOR with static key - trivial to reverse!
KEY = 0x5A
password_xor = ''.join(chr(ord(c) ^ KEY) for c in password)
db.execute(
"INSERT INTO users (username, password) VALUES (?, ?)",
(username, password_xor)
)
# SECURE - bcrypt hashing (one-way, irreversible)
def store_password_bcrypt_safe(username, password):
# Cryptographic hashing - CANNOT be reversed!
password_hash = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
)
db.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)",
(username, password_hash)
)
def verify_password_bcrypt_safe(username, password):
user = db.get_user(username)
if not user:
# Timing attack mitigation
bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
return False
# Compare using constant-time comparison
return bcrypt.checkpw(
password.encode('utf-8'),
user['password_hash']
)
# BEST - Argon2id (most secure, modern)
def store_password_argon2_best(username, password):
ph = PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # 64 MB memory
parallelism=4, # Number of threads
hash_len=32, # Hash output length
salt_len=16 # Salt length
)
password_hash = ph.hash(password)
db.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)",
(username, password_hash)
)
Why encoding fails:
# Base64 decoding is trivial:
echo "cGFzc3dvcmQxMjM=" | base64 -d
# Output: password123
# URL decoding:
echo "password%21%40%23" | python3 -c "import sys, urllib.parse; print(urllib.parse.unquote(sys.stdin.read()))"
# Output: password!@#
# Hex decoding:
echo "70617373776f7264" | xxd -r -p
# Output: password
Understand the Difference: Encoding vs Encryption vs Hashing
const crypto = require('crypto');
const bcrypt = require('bcrypt');
// ENCODING - Reversible data representation (NOT SECURITY!)
function encoding_example(password) {
// Base64 encoding
const encoded = Buffer.from(password).toString('base64');
console.log('Encoded:', encoded);
// Trivially reversed
const decoded = Buffer.from(encoded, 'base64').toString();
console.log('Decoded:', decoded); // Original password!
// Use case: Data transport, NOT security
}
// ENCRYPTION - Reversible with key (WRONG FOR PASSWORDS!)
function encryption_example(password) {
const algorithm = 'aes-256-gcm';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
// Encrypt
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(password, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Can decrypt with key!
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
console.log('Decrypted:', decrypted); // Original password!
// Use case: Sensitive data you NEED to decrypt later
// NOT for passwords!
}
// HASHING - One-way, irreversible (CORRECT FOR PASSWORDS!)
async function hashing_example(password) {
const saltRounds = 12;
// Hash password
const hash = await bcrypt.hash(password, saltRounds);
console.log('Hash:', hash);
// CANNOT reverse the hash to get original password!
// Can only verify by hashing input and comparing
const match = await bcrypt.compare(password, hash);
console.log('Match:', match); // true
// Use case: Passwords, where you never need original value
}
Comparison table: | Method | Reversible | Use Case | Example | |--------|-----------|----------|----------| | Encoding | Yes (trivial) | Data format | Base64, URL encoding, hex | | Encryption | Yes (with key) | Protecting data | AES-256-GCM, RSA | | Hashing | No (one-way) | Passwords | bcrypt, Argon2, PBKDF2 |
Migrate from Encoding to Hashing
import base64
import bcrypt
import sqlite3
# Migration script
def migrate_encoded_passwords():
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
# Add password_hash column
cursor.execute("""
ALTER TABLE users
ADD COLUMN password_hash BLOB
""")
# CRITICAL: Cannot convert encoded passwords to hashes!
# Must force users to reset passwords
print("WARNING: Cannot migrate encoded passwords to hashes!")
print("Encoded passwords can be decoded to plaintext.")
print("You must force all users to reset their passwords.")
# Option 1: Force password reset for all users
cursor.execute("""
UPDATE users
SET password_reset_required = 1,
password = NULL -- Clear encoded password
""")
conn.commit()
conn.close()
# Handle password reset
def handle_password_reset(username, new_password):
# Hash the new password properly
password_hash = bcrypt.hashpw(
new_password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
)
db.execute("""
UPDATE users
SET password_hash = ?,
password_reset_required = 0
WHERE username = ?
""", (password_hash, username))
# Gradual migration on login
def login_and_migrate(username, password):
user = db.get_user(username)
# Old encoding-based system
if user.get('password_encoded'):
# Decode and verify
stored_password = base64.b64decode(user['password_encoded']).decode()
if password == stored_password:
# Successful login - migrate to hash!
password_hash = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt(rounds=12)
)
db.execute("""
UPDATE users
SET password_hash = ?,
password_encoded = NULL
WHERE username = ?
""", (password_hash, username))
return True
return False
# New hash-based system
if user.get('password_hash'):
return bcrypt.checkpw(
password.encode('utf-8'),
user['password_hash']
)
return False
Use Hashing for All Password Operations
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Base64;
public class PasswordService {
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12);
// VULNERABLE - Base64 encoding
public void createUserBad(String username, String password) {
// Base64 is reversible!
String encoded = Base64.getEncoder().encodeToString(
password.getBytes()
);
// Can trivially decode:
// byte[] decoded = Base64.getDecoder().decode(encoded);
// String original = new String(decoded); // Got password back!
userRepository.save(username, encoded);
}
// SECURE - bcrypt hashing
public void createUserSafe(String username, String password) {
// One-way hash - cannot be reversed!
String passwordHash = passwordEncoder.encode(password);
userRepository.save(username, passwordHash);
}
public boolean verifyPassword(String username, String password) {
User user = userRepository.findByUsername(username);
if (user == null) {
// Timing attack mitigation
passwordEncoder.encode(password);
return false;
}
// Constant-time comparison
return passwordEncoder.matches(password, user.getPasswordHash());
}
}
Test Password Security
import unittest
import base64
import bcrypt
class PasswordSecurityTest(unittest.TestCase):
def test_no_base64_encoding(self):
"""Verify passwords are not Base64 encoded"""
password = "testPassword123"
# Create user
create_user("testuser", password)
# Get stored value
user = db.get_user("testuser")
stored = user['password_hash']
# Should NOT be Base64 encoded
try:
decoded = base64.b64decode(stored)
# If it decodes, it's Base64 - BAD!
self.fail("Password appears to be Base64 encoded!")
except:
pass # Good - not Base64
# Should start with bcrypt prefix
self.assertTrue(stored.startswith(b'$2b$'))
def test_password_is_hashed_not_encoded(self):
"""Verify password storage uses hashing, not encoding"""
password = "myPassword"
create_user("user1", password)
create_user("user2", password) # Same password
user1 = db.get_user("user1")
user2 = db.get_user("user2")
# Same password should produce different hashes (unique salts)
self.assertNotEqual(user1['password_hash'], user2['password_hash'])
# Both should verify correctly
self.assertTrue(verify_password("user1", password))
self.assertTrue(verify_password("user2", password))
def test_password_irreversible(self):
"""Verify password cannot be recovered from hash"""
password = "secretPassword"
create_user("testuser", password)
user = db.get_user("testuser")
# Should not be able to get original password
# Hash is one-way - no decode/decrypt possible
# Can only verify by hashing input and comparing
self.assertTrue(verify_password("testuser", password))
self.assertFalse(verify_password("testuser", "wrongPassword"))
if __name__ == '__main__':
unittest.main()
Common Vulnerable Patterns
- Base64 encoding passwords
- XOR with static key
- ROT13 "encryption"
- Simple character substitution
- Reversible "obfuscation"
Security Checklist
- No Base64, hex, URL encoding used for passwords
- No XOR or ROT13 "encryption"
- Using bcrypt, Argon2, or PBKDF2 for password hashing
- Cost factor configured (bcrypt ≥12, PBKDF2 ≥600,000)
- Passwords never logged or exposed in any form
- Migration plan for existing encoded passwords
- Tests verify hashing (not encoding) used
- Same password produces different hashes (salts working)