● CWE-362 · CWE-367

Race condition dan TOCTOU (CWE-362, CWE-367): bagaimana request paralel menembus pemeriksaan dan cara memperbaikinya

Kode yang membaca sebuah nilai, memeriksanya, lalu menulis kembali membuat dua request yang tiba pada saat bersamaan sama-sama lolos pemeriksaan, sehingga kartu hadiah ditukar dua kali atau saldo menjadi negatif. Bagaimana jendela antara memeriksa dan bertindak terjadi, mengapa pemeriksaan di aplikasi tidak bisa menutupnya, dan bagaimana satu UPDATE ... RETURNING bersyarat, kunci baris, dan constraint database membuat operasi menjadi atomik, dengan contoh Python dan PostgreSQL.

Penjelasan Sederhana (ELI5)

Sebuah bioskop punya dua kasir dan hanya satu kursi kosong di baris F. Keduanya melihat denah kursi pada detik yang sama, sama-sama melihat F7 kosong, dan sama-sama menjualnya. Setiap kasir sudah memeriksa sebelum menjual; masalahnya, memeriksa dan menjual adalah dua momen terpisah, dan kasir lain bertindak di antaranya. Perbaikannya adalah satu bundel tiket yang harus disobek keduanya: siapa pun yang menyobek tiket F7 terakhir mendapatkannya, dan yang lain mendapati bundelnya kosong. Memeriksa dan mengambil menjadi satu tindakan.

Konsep Kunci & Istilah

TOCTOU (dari saat pemeriksaan ke saat penggunaan)
Keadaan yang sudah diperiksa bisa berubah sebelum dipakai. Dalam kode web, pemeriksaannya biasanya berupa SELECT dan penggunaannya UPDATE sesudahnya, dengan request lain berjalan di antaranya.
Jendela race dan request paralel
Jendelanya sering hanya beberapa milidetik, tetapi penyerang bisa menyusun puluhan request agar tiba bersamaan. Riset PortSwigger tahun 2023 menunjukkan bahwa mengirim byte terakhir dari banyak request HTTP/2 dalam satu paket membuat semuanya tiba dalam selisih sekitar satu milidetik.
Update bersyarat yang atomik
UPDATE ... WHERE code = %s AND NOT redeemed RETURNING ... memeriksa dan mengubah baris dalam satu pernyataan. Hanya satu pemanggil yang mendapat baris kembali; yang lain tidak mendapat apa pun.
Kunci baris pada Read Committed
Di PostgreSQL, UPDATE kedua pada baris yang sama menunggu transaksi pertama, lalu mengevaluasi ulang klausa WHERE-nya terhadap baris yang sudah di-commit. Pemeriksaan ulang inilah yang membuat update bersyarat aman tanpa menaikkan tingkat isolasi.
Constraint dan kunci idempotensi
Indeks UNIQUE dan CHECK (balance_cents >= 0) membuat race yang kalah gagal dengan jelas alih-alih memakai saldo dua kali. Idempotency-Key yang disimpan dengan indeks unik membuat klien bisa mengulang pembayaran tanpa menjalankannya dua kali.

Alur Serangan Langkah demi Langkah

Langkah 1

Penyerang memegang satu kartu hadiah yang sah

Ia punya satu kartu bernilai tetap dan sebuah akun untuk menerima saldonya.

Langkah 2

Ia mengirim banyak penukaran sekaligus

Dua puluh request dengan kode yang sama tiba dalam satu milidetik. Dua request paralel dengan kode uji TEST-0001 menggambarkan kasus ini.

Langkah 3

Setiap request lolos pemeriksaan

Setiap SELECT berjalan sebelum ada request yang menandai kartu sebagai sudah ditukar, sehingga semuanya melihat redeemed = false.

Langkah 4

Saldo dikreditkan berkali-kali

Setiap request menambahkan nilai kartu sebelum penanda ditulis. Pada 2015 Egor Homakov menunjukkan bahwa transfer paralel antar kartu hadiah Starbucks bisa menciptakan saldo dari ketiadaan.

Kode Sumber: Rentan vs Aman

IMPLEMENTASI RENTAN
# gift_cards.py: pemeriksaan dan penggunaan adalah pernyataan terpisah
from .db import pool


# setiap request paralel bisa melihat redeemed = false sebelum ada yang menulis
def redeem_gift_card(user_id: int, code: str) -> int:
    with pool.connection() as conn:
        card = conn.execute("SELECT id, balance_cents, redeemed FROM gift_cards WHERE code = %s", (code,)).fetchone()
        if card is None or card[2]:
            raise ValueError("invalid or used card")
        conn.execute("UPDATE accounts SET balance_cents = balance_cents + %s WHERE user_id = %s", (card[1], user_id))
        conn.execute("UPDATE gift_cards SET redeemed = true WHERE id = %s", (card[0],))
        return card[1]
PERBAIKAN AMAN & KUAT
# gift_cards.py: pemeriksaan dan penggunaan dalam satu pernyataan
from .db import pool


# hanya satu pemanggil yang mendapat baris; sisanya menunggu kunci lalu gagal di NOT redeemed
def redeem_gift_card(user_id: int, code: str) -> int:
    with pool.connection() as conn, conn.transaction():
        card = conn.execute(
            "UPDATE gift_cards SET redeemed = true, redeemed_by = %s, redeemed_at = now() "
            "WHERE code = %s AND NOT redeemed RETURNING balance_cents",
            (user_id, code),
        ).fetchone()
        if card is None:
            raise ValueError("invalid or used card")
        # kredit dalam transaksi yang sama, jadi kedua perubahan di-commit bersama atau tidak sama sekali
        conn.execute("UPDATE accounts SET balance_cents = balance_cents + %s WHERE user_id = %s", (card[0], user_id))
        return card[0]

Daftar Periksa Penguatan Sistem Rekayasa

Sumber

← Lihat seluruh direktori keamanan Semua panduan kerentanan →