● CWE-916 · OWASP A02:2021

Hashing kata sandi yang tidak aman (CWE-916): mengapa SHA-256 tidak cukup dan cara menyimpan kata sandi

Menyimpan kata sandi sebagai digest SHA-256, SHA-1, atau MD5 tanpa salt berarti tabel yang bocor bisa diuji dengan miliaran tebakan per detik di GPU, dan semua pengguna dengan kata sandi yang sama memakai satu hash yang sama. Bagaimana salt, fungsi yang boros memori seperti Argon2id, rehash saat login, dan verifikasi tiruan melindungi database yang dicuri, dengan contoh Python memakai argon2-cffi.

Penjelasan Sederhana (ELI5)

Tempat penitipan mantel menyimpan salinan setiap tiket untuk memeriksa mantel mana milik siapa. Jika salinannya hanya fotokopi biasa, pencuri yang mengambil mapnya bisa membawa semua mantel. Hash yang cepat seperti fotokopi yang sedikit buram: pencuri dengan pemindai cepat bisa menjernihkan jutaan per detik. Hash yang lambat dan boros memori seperti salinan yang dikunci dalam brankas kecil yang butuh satu detik penuh dan meja kerja besar untuk dibuka, satu brankas per tiket dengan kunci yang berbeda-beda. Pemilik membuka satu brankas di pintu tanpa merasakan jeda; pencuri harus membuka jutaan.

Konsep Kunci & Istilah

Hash cepat
SHA-256, SHA-1, dan MD5 dirancang untuk cepat. Satu GPU modern menghitung miliaran hash SHA-256 per detik, sehingga kata sandi yang pendek atau umum jebol dalam hitungan jam oleh brute force dan serangan kamus.
Salt
Nilai acak yang disimpan bersama setiap hash agar kata sandi yang sama menghasilkan hash berbeda. Tanpa salt, satu tabel yang sudah dihitung sebelumnya bisa membobol semua akun sekaligus, dan hash yang sama mengungkap pengguna yang memakai kata sandi sama.
Fungsi boros memori (Argon2id)
Argon2id membuat setiap tebakan memakan waktu dan memori, sehingga sebagian besar keunggulan GPU hilang. Batas minimum OWASP adalah memori 19 MiB, dua iterasi, dan satu jalur (memory_cost=19456, time_cost=2, parallelism=1); scrypt dan bcrypt adalah alternatif yang dapat diterima.
String PHC dan rehash
PasswordHasher.hash() mengembalikan string seperti $argon2id$v=19$m=19456,t=2,p=1$... yang berisi algoritma, parameter, salt, dan hash. check_needs_rehash() memberi tahu ketika parameter yang tersimpan lebih lemah dari yang sekarang, sehingga hash bisa ditingkatkan pada login berhasil berikutnya.
Waktu respons dan enumerasi akun
Jika email yang tidak dikenal langsung dijawab sementara email asli butuh waktu satu verifikasi hash, waktu respons membocorkan akun mana yang ada. Memverifikasi terhadap hash tiruan membuat kedua jalur memakan waktu yang sama.

Alur Serangan Langkah demi Langkah

Langkah 1

Tabel pengguna bocor

Injeksi SQL, cadangan yang terbuka, atau laptop yang dicuri memberi penyerang kolom password_hash.

Langkah 2

Penyerang menjalankan cracker GPU

SHA-256 tanpa salt memungkinkannya meng-hash daftar kata sekali lalu membandingkannya dengan semua baris. Akun uji dengan kata sandi Summer2024! menunjukkan betapa cepat pola umum jebol.

Langkah 3

Kata sandi yang sama jebol bersamaan

Setiap pengguna yang memilih kata sandi yang sama punya digest yang sama, jadi satu hash yang jebol mengekspos mereka semua.

Langkah 4

Kata sandi dipakai ulang di tempat lain

Kata sandi yang jebol dicoba di akun email, bank, dan cloud. Pada 2012 LinkedIn kehilangan sekitar 6,5 juta hash kata sandi SHA-1 tanpa salt, dan pada 2016 sekumpulan sekitar 117 juta kredensial dari insiden yang sama dijual.

Kode Sumber: Rentan vs Aman

IMPLEMENTASI RENTAN
# users.py: satu digest cepat tanpa salt per kata sandi
import hashlib

from .db import db


# kata sandi sama, hash sama, miliaran tebakan per detik di GPU
def create_user(email: str, password: str) -> None:
    digest = hashlib.sha256(password.encode()).hexdigest()
    db.execute("INSERT INTO users (email, password_hash) VALUES (?, ?)", (email, digest))


# email tak dikenal langsung dijawab, jadi waktu membocorkan akun yang ada
def check_login(email: str, password: str) -> bool:
    row = db.execute("SELECT password_hash FROM users WHERE email = ?", (email,)).fetchone()
    return row is not None and row[0] == hashlib.sha256(password.encode()).hexdigest()
PERBAIKAN AMAN & KUAT
# users.py: Argon2id dengan salt per kata sandi dan peningkatan saat login
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError

from .db import db

# minimum OWASP untuk Argon2id: 19 MiB, 2 iterasi, 1 jalur
hasher = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
DUMMY_HASH = hasher.hash("timing-equaliser")


# string yang disimpan memuat algoritma, parameter, salt, dan hash
def create_user(email: str, password: str) -> None:
    if not 12 <= len(password) <= 128:
        raise ValueError("password must be 12 to 128 characters")
    db.execute("INSERT INTO users (email, password_hash) VALUES (?, ?)", (email, hasher.hash(password)))


# pengguna tak dikenal tetap memakan satu verifikasi; hash lemah ditingkatkan
def check_login(email: str, password: str) -> bool:
    row = db.execute("SELECT id, password_hash FROM users WHERE email = ?", (email,)).fetchone()
    stored = row[1] if row else DUMMY_HASH
    try:
        hasher.verify(stored, password)
    except (VerificationError, InvalidHashError):
        return False
    if row is None:
        return False
    if hasher.check_needs_rehash(stored):
        db.execute("UPDATE users SET password_hash = ? WHERE id = ?", (hasher.hash(password), row[0]))
    return True

Daftar Periksa Penguatan Sistem Rekayasa

Sumber

← Lihat seluruh direktori keamanan Semua panduan kerentanan →