● CWE-362 · CWE-367

रेस कंडीशन और TOCTOU (CWE-362, CWE-367): समानांतर रिक्वेस्ट जाँच को कैसे तोड़ती हैं और इसे कैसे ठीक करें

जो कोड कोई मान पढ़ता है, उसे जाँचता है और फिर लिखता है, वह एक ही पल में आने वाली दो रिक्वेस्ट को दोनों को जाँच पार करने देता है, जिससे gift card दो बार भुन जाता है या balance ऋणात्मक हो जाता है। जाँचने और कार्रवाई करने के बीच की खिड़की कैसे बनती है, ऐप्लिकेशन की जाँच इसे क्यों बंद नहीं कर सकती, और एक ही conditional UPDATE ... RETURNING, row locks और database constraints इस ऑपरेशन को atomic कैसे बनाते हैं, Python और PostgreSQL के उदाहरण के साथ।

आसान भाषा में (ELI5)

एक सिनेमा में दो कैशियर हैं और F पंक्ति में केवल एक सीट ख़ाली है। दोनों एक ही सेकंड में सीटों का नक़्शा देखते हैं, दोनों को F7 ख़ाली दिखती है और दोनों उसे बेच देते हैं। हर कैशियर ने बेचने से पहले जाँचा था; समस्या यह है कि जाँचना और बेचना दो अलग-अलग पल थे, और बीच में दूसरे कैशियर ने कार्रवाई कर दी। उपाय टिकटों की एक ही गड्डी है जिससे दोनों को फाड़ना पड़े: जो आख़िरी F7 टिकट फाड़े, टिकट उसका, और दूसरे को गड्डी ख़ाली मिलती है। जाँचना और लेना एक ही क्रिया बन जाते हैं।

इस पेज के मुख्य शब्द

TOCTOU (जाँच के समय से इस्तेमाल के समय तक)
जिस स्थिति की जाँच हुई, वह इस्तेमाल से पहले बदल सकती है। वेब कोड में जाँच आमतौर पर एक SELECT होती है और इस्तेमाल बाद का UPDATE, जिनके बीच दूसरी रिक्वेस्ट चलती रहती हैं।
Race window और समानांतर रिक्वेस्ट
यह खिड़की अक्सर कुछ ही milliseconds की होती है, पर हमलावर दर्जनों रिक्वेस्ट को एक साथ पहुँचने के लिए क़तार में लगा सकते हैं। PortSwigger के 2023 के शोध ने दिखाया कि कई HTTP/2 रिक्वेस्ट के आख़िरी bytes एक ही packet में भेजने से वे लगभग एक millisecond के भीतर पहुँचती हैं।
Atomic conditional update
UPDATE ... WHERE code = %s AND NOT redeemed RETURNING ... एक ही statement में row को जाँचता और बदलता है। केवल एक caller को row वापस मिलती है; बाक़ी को कुछ नहीं।
Read Committed में row locks
PostgreSQL में उसी row पर दूसरा UPDATE पहले transaction का इंतज़ार करता है, फिर commit हुई row पर अपना WHERE clause दोबारा जाँचता है। यही दोबारा जाँच isolation level बढ़ाए बिना conditional update को सुरक्षित बनाती है।
Constraints और idempotency keys
UNIQUE indexes और CHECK (balance_cents >= 0) हारी हुई race को दोहरा ख़र्च करने के बजाय साफ़ तौर पर फ़ेल कर देते हैं। unique index के साथ सहेजी गई Idempotency-Key clients को भुगतान दोबारा चलाए बिना फिर से कोशिश करने देती है।

हमले का चरण-दर-चरण प्रवाह

चरण 1

हमलावर के पास एक वैध gift card है

उसके पास तय राशि का एक ही कार्ड और उसे जमा करने के लिए एक खाता है।

चरण 2

वह एक साथ कई redemptions भेजता है

एक ही code वाली बीस रिक्वेस्ट एक millisecond के भीतर पहुँचती हैं। टेस्ट code TEST-0001 वाली दो समानांतर रिक्वेस्ट यह मामला दिखाती हैं।

चरण 3

हर रिक्वेस्ट जाँच पार कर लेती है

हर SELECT तब चलता है जब किसी भी रिक्वेस्ट ने कार्ड को भुनाया हुआ चिह्नित नहीं किया होता, इसलिए सबको redeemed = false दिखता है।

चरण 4

balance कई बार जमा हो जाता है

हर रिक्वेस्ट flag लिखे जाने से पहले कार्ड की राशि जोड़ देती है। 2015 में Egor Homakov ने दिखाया कि Starbucks gift cards के बीच समानांतर transfers से बिना किसी आधार के balance बनाया जा सकता था।

सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन

कमज़ोर कार्यान्वयन
# gift_cards.py: जाँच और इस्तेमाल अलग-अलग statements हैं
from .db import pool


# किसी के लिखने से पहले हर समानांतर रिक्वेस्ट को redeemed = false दिख सकता है
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]
सुरक्षित और सुदृढ़ फ़िक्स
# gift_cards.py: जाँच और इस्तेमाल एक ही statement में
from .db import pool


# केवल एक caller को row मिलती है; बाक़ी lock का इंतज़ार करते हैं और 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")
        # उसी transaction में जमा, इसलिए दोनों बदलाव साथ commit होते हैं या कोई नहीं
        conn.execute("UPDATE accounts SET balance_cents = balance_cents + %s WHERE user_id = %s", (card[0], user_id))
        return card[0]

इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट

स्रोत

← पूरी सुरक्षा निर्देशिका देखें सभी कमज़ोरी गाइड →