Cacat Logika

Cacat logika bisnis: nilai negatif, lompatan alur kerja, dan penumpukan potongan nilai.

Daftar isi

Cacat logika bisnis (business logic flaw) adalah kesalahan pada aturan alur kerja atau validasi sebuah fitur, sehingga operasi yang seharusnya ditolak tetap dijalankan server. Permintaannya tidak memuat sintaks berbahaya karena setiap masukannya terlihat wajar, tetapi nilai dan urutannya melanggar asumsi yang tidak pernah diperiksa. Topik ini mereproduksi tiga cacat yang sering muncul pada lab peminjaman buku perpustakaan dan pendaftaran kegiatan, yaitu jumlah negatif yang memundurkan tanggal kembali, lompatan alur pendaftaran, dan penumpukan kode potongan hingga total menjadi negatif. Setiap kasus dieksekusi dengan Python, keluaran aslinya diperiksa, lalu tambalannya ditunjukkan pada skrip yang sama.

Nilai negatif pada jumlah perpanjangan#

Perpustakaan memperbolehkan anggota memperpanjang peminjaman buku untuk sejumlah hari tertentu. Jumlah tersebut dikirim sebagai bagian permintaan, sehingga nilainya berada sepenuhnya dalam kendali pengirim dan bukan pada server. Apabila handler langsung menambahkannya ke tanggal kembali, angka negatif memundurkan batas peminjaman, sehingga anggota yang memanipulasi permintaan justru memperoleh tenggat yang lebih longgar. PortSwigger mengelompokkan perilaku ini dalam kegagalan menangani masukan tak lazim (failing to handle unconventional input), dan pemindai otomatis jarang menemukannya karena tidak ada payload yang menyerupai pola serangan. Skrip berikut berisi versi cacat dan versi perbaikan dengan tanggal kembali yang tetap, dan seluruhnya dijalankan dengan perintah python3 perpanjangan.py.

python perpanjangan.py
"""Perpanjangan peminjaman buku dengan jumlah negatif.

Versi cacat menerima jumlah apa pun sehingga tanggal kembali bisa
mundur. Versi perbaikan menolak jumlah yang tidak lebih dari 0
sebelum operasi penanggalan dijalankan.
"""

from datetime import date, timedelta


def perpanjang_cacat(tanggal_kembali, jumlah_hari):
    return tanggal_kembali + timedelta(days=jumlah_hari)


def perpanjang_aman(tanggal_kembali, jumlah_hari):
    if jumlah_hari <= 0:
        raise ValueError("jumlah perpanjangan harus lebih dari 0")
    return tanggal_kembali + timedelta(days=jumlah_hari)


kembali = date(2026, 3, 20)
print(f"tanggal kembali mula-mula: {kembali}")
print(f"versi cacat, perpanjang -7: {perpanjang_cacat(kembali, -7)}")
try:
    perpanjang_aman(kembali, -7)
except ValueError as pesan:
    print(f"versi perbaikan, perpanjang -7: ditolak ({pesan})")
print(f"versi perbaikan, perpanjang 10: {perpanjang_aman(kembali, 10)}")
text Keluaran
tanggal kembali mula-mula: 2026-03-20
versi cacat, perpanjang -7: 2026-03-13
versi perbaikan, perpanjang -7: ditolak (jumlah perpanjangan harus lebih dari 0)
versi perbaikan, perpanjang 10: 2026-03-30

Keluaran versi cacat memperlihatkan tanggal kembali bergeser dari 2026-03-20 menjadi 2026-03-13 karena perpanjangan minus tujuh hari diterima begitu saja. Pada versi perbaikan, handler menolak jumlah yang tidak lebih dari 0 sebelum operasi penanggalan dijalankan, sehingga nilai negatif tidak pernah menyentuh data peminjaman. Tambalannya dapat dirumuskan demikian, yaitu setiap batasan yang berlaku pada dunia nyata dinyatakan sebagai pemeriksaan eksplisit pada kode server.

Lompatan alur pendaftaran#

Alur pendaftaran kegiatan bergerak melalui status yang berurutan. Peserta membuat pendaftarannya, panitia memverifikasi datanya, dan baru setelah itu kehadirannya dapat dikonfirmasi. Versi cacat pada skrip berikut mengubah status menjadi dikonfirmasi tanpa memeriksa keadaan saat ini, sehingga permintaan konfirmasi yang dikirim sebelum verifikasi tetap dilayani. Perbaikannya berupa mesin status (state machine) sederhana, yaitu tabel yang hanya mengizinkan transisi dari status tertentu ke status tertentu. Diagram berikut memperlihatkan transisi yang sah beserta lompatan yang dieksploitasi, lalu skripnya diuji dengan perintah python3 alur-pendaftaran.py.

dibuatterverifikasidikonfirmasiverifikasikonfirmasilompatan alur yang dieksploitasi
Mesin status alur pendaftaran kegiatan
python alur-pendaftaran.py
"""Lompatan alur pada konfirmasi kehadiran pendaftaran kegiatan.

Versi cacat mengubah status tanpa memeriksa keadaan saat ini,
sehingga pendaftaran yang baru dibuat dapat langsung dikonfirmasi.
Versi perbaikan memakai mesin status sederhana berupa tabel transisi
yang hanya mengizinkan urutan yang sah.
"""

DIBUAT = "dibuat"
TERVERIFIKASI = "terverifikasi"
DIKONFIRMASI = "dikonfirmasi"


def konfirmasi_cacat(pendaftaran):
    pendaftaran["status"] = DIKONFIRMASI
    return pendaftaran["status"]


TRANSISI = {
    DIBUAT: {TERVERIFIKASI},
    TERVERIFIKASI: {DIKONFIRMASI},
    DIKONFIRMASI: set(),
}


def transisi_aman(pendaftaran, status_baru):
    if status_baru not in TRANSISI[pendaftaran["status"]]:
        raise ValueError(
            f"transisi {pendaftaran['status']} -> {status_baru} tidak sah"
        )
    pendaftaran["status"] = status_baru
    return pendaftaran["status"]


daftar = {"peserta": "Budi", "status": DIBUAT}
print(f"status mula-mula: {daftar['status']}")
print(f"versi cacat, konfirmasi langsung: {konfirmasi_cacat(daftar)}")

cadangan = {"peserta": "Budi", "status": DIBUAT}
print("versi perbaikan, konfirmasi langsung:")
try:
    transisi_aman(cadangan, DIKONFIRMASI)
except ValueError as pesan:
    print(f"  ditolak ({pesan})")
transisi_aman(cadangan, TERVERIFIKASI)
print(f"versi perbaikan, setelah verifikasi: {transisi_aman(cadangan, DIKONFIRMASI)}")
text Keluaran
status mula-mula: dibuat
versi cacat, konfirmasi langsung: dikonfirmasi
versi perbaikan, konfirmasi langsung:
  ditolak (transisi dibuat -> dikonfirmasi tidak sah)
versi perbaikan, setelah verifikasi: dikonfirmasi

Keluaran versi cacat menunjukkan pendaftaran berstatus dibuat berubah menjadi dikonfirmasi dalam satu langkah, sehingga tahap verifikasi dilewati tanpa jejak. Pada versi perbaikan, transisi yang sama ditolak karena tabel TRANSISI tidak mencantumkannya, sedangkan urutan yang sah tetap dapat ditempuh sampai akhir. Tabel semacam ini sekaligus mendokumentasikan alur kerja, karena semua transisi yang diizinkan terbaca dalam satu tempat.

Penumpukan kode potongan#

Biaya pendaftaran kegiatan dapat dikurangi dengan kode potongan. Cacat muncul ketika handler menerapkan setiap kode yang dikirim secara berturut-turut, sehingga dua kode yang masing-masing sah dapat ditumpuk pada satu transaksi. Penyerang tinggal mengirim keduanya sekaligus, dan totalnya dapat mencapai nol atau negatif. Pola ini tidak terbatas pada satu jenis aplikasi karena bentuknya selalu sama, yaitu operasi pengurangan yang boleh diulang tanpa pembatas. Jalankan skrip berikut dengan perintah python3 potongan.py, lalu perhatikan total pada tiap versinya.

python potongan.py
"""Penumpukan kode potongan pada biaya pendaftaran kegiatan.

Versi cacat mengurangi biaya dengan setiap kode yang dikirim secara
berturut-turut, sehingga total bisa menjadi nol atau negatif. Versi
perbaikan menerima paling banyak satu kode, menolak kode yang tidak
dikenal, dan selalu menghitung ulang total di server.
"""

BIAYA = 100
POTONGAN = {"AKTIVITAS60": 60, "KOMUNITAS50": 50}


def bayar_cacat(daftar_kode):
    total = BIAYA
    for kode in daftar_kode:
        total -= POTONGAN[kode]
    return total


def bayar_aman(daftar_kode):
    if len(daftar_kode) > 1:
        raise ValueError("paling banyak satu kode potongan per transaksi")
    total = BIAYA
    if daftar_kode:
        kode = daftar_kode[0]
        if kode not in POTONGAN:
            raise ValueError(f"kode {kode} tidak dikenal")
        total -= POTONGAN[kode]
    return total


kombinasi = ["AKTIVITAS60", "KOMUNITAS50"]
print(f"biaya pendaftaran: {BIAYA}")
print(f"versi cacat, dua kode ditumpuk: {bayar_cacat(kombinasi)}")
try:
    bayar_aman(kombinasi)
except ValueError as pesan:
    print(f"versi perbaikan, dua kode ditumpuk: ditolak ({pesan})")
print(f"versi perbaikan, satu kode AKTIVITAS60: {bayar_aman(['AKTIVITAS60'])}")
text Keluaran
biaya pendaftaran: 100
versi cacat, dua kode ditumpuk: -10
versi perbaikan, dua kode ditumpuk: ditolak (paling banyak satu kode potongan per transaksi)
versi perbaikan, satu kode AKTIVITAS60: 40

Keluaran versi cacat memperlihatkan biaya 100 turun 60 lalu turun 50 lagi, sehingga transaksi berakhir dengan nilai minus 10 yang tidak masuk akal. Versi perbaikan menerima paling banyak satu kode, menolak kode yang tidak dikenal, dan selalu menghitung ulang total di server atas dasar biaya yang tersimpan, bukan atas total yang dikirim klien. Potongan bertingkat yang memang dirancang tetap dapat didukung, tetapi aturannya dinyatakan secara eksplisit pada kode dan tidak disimpulkan dari masukan.

Kesalahan umum#

Kesalahan berikut paling sering muncul ketika logika bisnis dikerjakan dengan terburu-buru.

  • Mengandalkan validasi klien sebagai satu-satunya pengaman, padahal permintaan dapat disusun tanpa melalui formulir.
  • Memeriksa status alur pada satu endpoint, tetapi melupakannya pada endpoint lain yang juga mengubah data yang sama.
  • Menghitung total atau tanggal di klien, lalu mengirimkan hasilnya untuk disimpan server tanpa perhitungan ulang.
  • Menguji hanya dengan nilai dan urutan normal, sehingga nilai negatif, nol, angka besar, dan permintaan yang dilompati tidak pernah dicoba.

Ringkasan#

  • Nilai negatif, lompatan alur, dan penumpukan potongan lahir dari asumsi yang tidak dinyatakan sebagai pemeriksaan di sisi server.
  • Tambalannya berupa validasi batasan pada server, mesin status untuk transisi alur kerja, serta pembatasan jumlah kode potongan dengan perhitungan ulang total.
  • Pemindai otomatis jarang menemukan cacat ini, sehingga pengujian manual atas nilai tak lazim dan urutan permintaan menjadi bagian wajib dari pengujian keamanan.

Referensi#