Dasar-dasar keamanan

Praktik keamanan dasar dengan secrets, validasi masukan, dan pencegahan injection.

Daftar isi

Program yang menerima masukan dari luar juga menerima risiko dari luar, misalnya token yang mudah ditebak, data yang tidak diperiksa, dan perintah basis data yang dirangkai dari masukan pengguna. Topik ini membahas tiga praktik dasar yang menutup risiko tersebut, yaitu pembuatan token dengan secrets, validasi masukan di tepi program, dan pencegahan injection SQL dengan query berparameter. Seluruh contoh berjalan lokal, dan contoh basis datanya memakai sqlite3 bawaan Python.

Token acak dengan secrets#

Modul random dirancang untuk simulasi dan permainan, bukan untuk keamanan, karena urutan nilainya dapat diprediksi apabila benih (seed) yang ditanam diketahui. Modul secrets mengambil keacakan dari sumber acak sistem operasi yang dirancang untuk keperluan kriptografi, dan fungsi token_hex serta token_urlsafe menghasilkan token teks siap pakai. Skrip berikut membuat dua token lalu membandingkannya dengan compare_digest, yaitu fungsi perbandingan yang durasinya tidak membocorkan letak karakter pertama yang berbeda.

python demo_secrets.py
"""Demo modul secrets untuk token acak yang layak dipakai keperluan keamanan."""
import secrets

token_a = secrets.token_hex(16)
token_b = secrets.token_hex(16)
print("panjang token_hex(16)    :", len(token_a), "karakter")
print("panjang token_urlsafe(32):", len(secrets.token_urlsafe(32)), "karakter")
print("dua token selalu berbeda :", token_a != token_b)
print("compare_digest cocok     :", secrets.compare_digest(token_a, token_a))
print("compare_digest salah     :", secrets.compare_digest(token_b, token_a))
text Keluaran
panjang token_hex(16)    : 32 karakter
panjang token_urlsafe(32): 43 karakter
dua token selalu berbeda : True
compare_digest cocok     : True
compare_digest salah     : False

Nilai token itu sendiri tidak dicetak karena sifatnya rahasia, sedangkan panjangnya dapat diperiksa, yaitu 32 karakter heksadesimal untuk 16 byte dan 43 karakter untuk token_urlsafe 32 byte. Panjang token menentukan ruang tebakannya, yaitu 2 pangkat 128 kemungkinan untuk 16 byte, sehingga nilainya tidak dapat ditebak dengan mencoba beruntun. Dua token yang dibuat berurutan selalu berbeda, dan compare_digest mengembalikan True hanya ketika kedua nilainya persis sama. Fungsi perbandingan inilah yang dipakai saat mencocokkan token yang dikirim pengguna dengan token yang tersimpan, karena perbandingan biasa berhenti pada karakter pertama yang berbeda dan durasinya dapat diukur oleh penyerang.

Validasi masukan di tepi program#

Validasi masukan adalah pemeriksaan bentuk dan batas data sebelum data diproses lebih jauh, dan tempat terbaiknya ada di tepi program, yaitu tepat setelah data diterima. Prinsipnya berupa daftar putih (whitelist), yaitu menolak apa pun yang tidak secara eksplisit diizinkan, alih-alih mendaftar satu per satu pola yang dilarang. Skrip berikut memeriksa nama pengguna terhadap pola yang ketat dengan ekspresi reguler.

python demo_validasi.py
"""Demo validasi masukan dengan pola ekspresi reguler yang ketat."""
import re

POLA_NAMA = re.compile(r"^[a-z0-9_]{3,20}$")


def nama_valid(nama):
    return POLA_NAMA.fullmatch(nama) is not None


for calon in ["budi_01", "Budi 01", "x", "admin; drop"]:
    print(f"{calon!r:16} -> {nama_valid(calon)}")
text Keluaran
'budi_01'        -> True
'Budi 01'        -> False
'x'              -> False
'admin; drop'    -> False

Pola ^[a-z0-9_]{3,20}$ menuntut huruf kecil, angka, dan garis bawah dengan panjang 3 sampai 20 karakter, sedangkan fullmatch menolak string yang hanya cocok sebagian. Masukan “Budi 01” ditolak karena memuat huruf kapital dan spasi, masukan “x” ditolak karena terlalu pendek, dan masukan yang memuat tanda baca ditolak sebelum sempat menyentuh basis data. Aturan yang sama dapat dicoba pada demo berikut dengan mengetikkan beberapa calon nama.

Perilaku demo ini persis seperti fungsi nama_valid pada Python, yaitu pola yang sama diterapkan pada setiap masukan. Validasi di sisi klien seperti ini hanya kenyamanan antarmuka, karena permintaan tetap dapat dibuat langsung tanpa melalui halaman, sehingga pemeriksaan yang sama wajib diulang di sisi server.

Injection SQL dan query berparameter#

Injection SQL terjadi ketika masukan pengguna dirangkai menjadi teks perintah SQL, sehingga masukan tertentu dapat mengubah makna perintahnya. Skrip berikut membuat tabel siswa pada basis data sqlite3 di dalam memori, lalu membandingkan dua fungsi pencarian. Fungsi pertama merangkai string, sedangkan fungsi kedua memakai query berparameter dengan tanda tanya sebagai penampung nilai.

python demo_sql_injection.py
"""Demo injection SQL: query dirangkai string dibandingkan query berparameter."""
import sqlite3

koneksi = sqlite3.connect(":memory:")
koneksi.execute("CREATE TABLE siswa (nama TEXT, kelas TEXT)")
koneksi.executemany(
    "INSERT INTO siswa VALUES (?, ?)",
    [("Ali", "7A"), ("Budi", "7B"), ("Citra", "7A")],
)
koneksi.commit()


def cari_tidak_aman(nama):
    query = "SELECT nama FROM siswa WHERE nama = '" + nama + "'"
    return [baris[0] for baris in koneksi.execute(query)]


def cari_aman(nama):
    query = "SELECT nama FROM siswa WHERE nama = ?"
    return [baris[0] for baris in koneksi.execute(query, (nama,))]


masukan = "x' OR '1'='1"
print("masukan    :", masukan)
print("tidak aman :", cari_tidak_aman(masukan))
print("aman       :", cari_aman(masukan))
print("cari Ali   :", cari_aman("Ali"))
text Keluaran
masukan    : x' OR '1'='1
tidak aman : ['Ali', 'Budi', 'Citra']
aman       : []
cari Ali   : ['Ali']

Masukan x' OR '1'='1 menutup tanda kutip pembuka lalu menambahkan kondisi yang selalu benar, dan fungsi pertama menyerahkan seluruh isi tabel kepada penyerang. Fungsi kedua mengirim masukan sebagai nilai terpisah, sehingga sqlite3 memperlakukannya murni sebagai data dan hasil pencariannya kosong. Pencarian nama Ali tetap berjalan normal pada fungsi berparameter, yang menunjukkan bahwa pencegahan ini tidak mengorbankan fungsi.

Pola yang sama berlaku di luar SQL. Perintah shell yang dirangkai dari masukan membuka injection perintah, sedangkan jalur berkas yang dirangkai dari masukan dapat keluar dari folder yang dimaksud melalui ../. Pencegahannya senada, yaitu memisahkan struktur dari data, baik melalui daftar argumen subprocess, validasi jalur, maupun query berparameter pada basis data.

Menyimpan kata sandi dengan hash#

Kata sandi tidak boleh disimpan dalam bentuk aslinya, karena kebocoran berkas berarti kebocoran seluruh sandi. Bentuk penyimpanannya berupa hash yang diperlambat, dan pustaka standar menyediakan hashlib.pbkdf2_hmac untuk itu, yaitu fungsi turunan kunci (key derivation) yang mengulang hash berkali-kali. Garam (salt) berupa byte acak dari secrets ditambahkan agar dua pengguna dengan sandi sama menghasilkan hash yang berbeda.

python Contoh: hash PBKDF2 untuk kata sandi
# menurunkan hash kata sandi dengan PBKDF2 dan garam acak
import hashlib
import secrets

garam = secrets.token_bytes(16)
kunci = hashlib.pbkdf2_hmac("sha256", b"sandi-piket-2026", garam, 200_000)
print("panjang garam:", len(garam), "byte")
print("panjang kunci:", len(kunci), "byte")
text Keluaran
panjang garam: 16 byte
panjang kunci: 32 byte

Keluaran menunjukkan panjang kunci 32 byte sesuai SHA-256, sedangkan nilai garam sengaja tidak dicetak karena pada praktiknya ikut tersimpan di samping hash. Angka 200.000 pada contoh adalah jumlah perulangan hash yang memperlambat perhitungan secara sengaja, sehingga pencocokan kata sandi yang dicoba beruntun oleh penyerang menjadi jauh lebih mahal, sedangkan verifikasi yang sah hanya dilakukan sekali per upaya masuk. Jumlah perulangan semestinya bernilai besar karena itulah tujuan desainnya, dan angka pada contoh hanya dipilih agar demonstrasi tetap cepat. Proses verifikasi mengulang perhitungan yang sama dengan garam yang tersimpan, kemudian membandingkan hasilnya dengan compare_digest dari bagian pertama. Penyimpanan yang dijaga berupa pasangan garam dan hash untuk setiap pengguna, bukan sandi aslinya.

Ringkasan#

  • secrets menghasilkan token dari sumber acak yang layak keamanan, dan compare_digest membandingkan token tanpa membocorkan informasi lewat durasi.
  • Validasi masukan memakai daftar putih yang ketat di tepi program, dan pemeriksaan yang sama diulang di sisi server.
  • Query berparameter memisahkan perintah SQL dari data, sehingga masukan tidak dapat mengubah makna perintah.
  • Kata sandi disimpan sebagai hash yang diperlambat bersama garam acak, bukan dalam bentuk aslinya.

Referensi#