● CWE-352 · OWASP A01:2021

क्रॉस-साइट रिक्वेस्ट फ़ॉर्जरी (CSRF, CWE-352): यह कैसे काम करता है और इससे कैसे बचें

किसी दूसरी साइट का पेज लॉग-इन यूज़र के browser से आपके ऐप को डेटा बदलने वाली रिक्वेस्ट भिजवा सकता है, और browser session cookie अपने-आप जोड़ देता है, इसलिए केवल cookie जाँचने वाला route वह कार्रवाई कर देता है। synchronizer tokens, SameSite cookies और Sec-Fetch-Site जाँच इसे कैसे रोकते हैं, Flask और Flask-WTF के उदाहरण के साथ।

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

आपके दफ़्तर में एक डाक-कक्ष है जो आपकी 'बाहर जाने वाली' ट्रे में रखी हर हस्ताक्षरित पर्ची पर अमल करता है, और पर्चियों पर भरोसा करता है क्योंकि वे आपकी ट्रे में हैं, जहाँ केवल आप पहुँच सकते हैं। एक मेहमान आपसे जाते-जाते एक बंद लिफ़ाफ़ा ट्रे में डालने को कहता है। उसके अंदर पर्ची है: 'मेरे सभी पार्सलों का डिलीवरी पता बदल दो।' डाक-कक्ष उसे आपकी ट्रे में देखकर मान लेता है कि आपने लिखी है और उस पर अमल कर देता है। CSRF भी ऐसा ही है: browser आपके बैंक को जाने वाली हर रिक्वेस्ट पर आपकी session cookie लगा देता है, उस पर भी जिसे किसी दूसरी वेबसाइट ने शुरू किया। उपाय एक गुप्त मुहर है जिसे केवल आपके अपने दफ़्तर के पेज पर्ची पर लगा सकते हैं, ताकि डाक-कक्ष बिना मुहर वाली पर्चियाँ ठुकरा दे।

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

Ambient credentials
browser किसी साइट को जाने वाली रिक्वेस्ट पर cookies और HTTP authentication जोड़ देते हैं, चाहे उसे किसी भी पेज ने शुरू किया हो। evil.example से app.example को जाने वाली रिक्वेस्ट भी app.example की session cookie ले जाती है, जब तक cookie का SameSite attribute उसे न रोके।
Cookie का SameSite attribute
Strict हर cross-site रिक्वेस्ट पर cookie रोक लेता है, Lax उसे केवल top-level GET navigation पर भेजता है, और None हमेशा भेजता है (और Secure माँगता है)। Chromium बिना attribute वाली cookies को Lax मानता है; Firefox और Safari नहीं, और 'same-site' में पड़ोसी subdomains भी आते हैं।
Synchronizer token
session से जुड़ा एक random मान, जिसे सर्वर हर form या पेज में रखता है और हर असुरक्षित रिक्वेस्ट पर माँगता है। दूसरी साइट उसे पढ़ नहीं सकती, इसलिए वैध रिक्वेस्ट नहीं गढ़ सकती। Flask-WTF का CSRFProtect इसे csrf_token() से जारी करता है और csrf_token field या X-CSRFToken header जाँचता है।
Fetch Metadata (Sec-Fetch-Site)
आधुनिक browsers हर रिक्वेस्ट में Sec-Fetch-Site: cross-site, same-site, same-origin या none भेजते हैं, और पेज इसे गढ़ नहीं सकते। जो असुरक्षित methods same-origin नहीं हैं उन्हें ठुकराने से सर्वर पर एक ही जाँच में CSRF रुक जाता है।
GET पर state बदलना
GET पर डेटा बदलने वाला route किसी <img> tag या link से चलाया जा सकता है, और SameSite=Lax top-level GET navigation पर cookie फिर भी भेजता है। request.values पढ़ने से form body के साथ query string भी स्वीकार हो जाती है।

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

चरण 1

पीड़ित लॉग-इन है

उसके पास app.example की session cookie है, जो SameSite=None के साथ, या ऐसे browser में बिना attribute के भेजी जाती है जो डिफ़ॉल्ट रूप से Lax नहीं अपनाता।

चरण 2

वह हमलावर के नियंत्रण वाला पेज खोलता है

उसमें एक छिपा form है जो https://app.example/account/email पर भेजा जाता है और पेज लोड होते ही अपने-आप submit हो जाता है। हानिरहित टेस्ट मान email=test@example.com भेजने वाला form यह मामला दिखाता है।

चरण 3

browser cookie के साथ रिक्वेस्ट भेजता है

रिक्वेस्ट पीड़ित का session ले जाती है, और सर्वर को ऐसा कुछ नहीं दिखता जिससे पता चले कि form किसी दूसरी साइट पर था।

चरण 4

सर्वर खाते में बदलाव कर देता है

email पता हमलावर का हो जाता है, जो फिर password reset करके खाता हथिया लेता है। 2008 में Zeller और Felten ने ING Direct, YouTube और The New York Times पर इसी तरह की CSRF खामियाँ दिखाई थीं।

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

कमज़ोर कार्यान्वयन
# app.py: session cookie ही एकमात्र सबूत है कि यूज़र यह चाहता था
import os

from flask import Flask, abort, request, session

from .db import db

app = Flask(__name__)
app.config.update(
    SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_SAMESITE="None",
)


# GET भी चलता है, और request.values query string भी पढ़ता है
@app.route("/account/email", methods=["GET", "POST"])
def change_email():
    user_id = session.get("user_id")
    if user_id is None:
        abort(401)
    db.execute("UPDATE users SET email = ? WHERE id = ?", (request.values["email"], user_id))
    return {"status": "ok"}
सुरक्षित और सुदृढ़ फ़िक्स
# app.py: token, SameSite cookie और Fetch Metadata एक साथ
import os

from flask import Flask, abort, request, session
from flask_wtf.csrf import CSRFProtect

from .db import db

app = Flask(__name__)
app.config.update(
    SECRET_KEY=os.environ["FLASK_SECRET_KEY"],
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
)
# हर POST को csrf_token चाहिए; forms {{ csrf_token() }} दिखाते हैं, fetch() X-CSRFToken भेजता है
csrf = CSRFProtect(app)


# दूसरी साइट से शुरू हुई writes route चलने से पहले ठुकरा दी जाती हैं
@app.before_request
def reject_cross_site_writes():
    if request.method not in ("GET", "HEAD", "OPTIONS"):
        if request.headers.get("Sec-Fetch-Site", "same-origin") not in ("same-origin", "none"):
            abort(403)


# केवल POST, डेटा form body से
@app.post("/account/email")
def change_email():
    user_id = session.get("user_id")
    if user_id is None:
        abort(401)
    db.execute("UPDATE users SET email = ? WHERE id = ?", (request.form["email"], user_id))
    return {"status": "ok"}

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

स्रोत

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