Skip to content

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)

Additional Resources