क्रॉस-साइट रिक्वेस्ट फ़ॉर्जरी (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 काSameSiteattribute उसे न रोके। - 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_tokenfield याX-CSRFTokenheader जाँचता है। - Fetch Metadata (Sec-Fetch-Site)
- आधुनिक browsers हर रिक्वेस्ट में
Sec-Fetch-Site: cross-site,same-site,same-originयाnoneभेजते हैं, और पेज इसे गढ़ नहीं सकते। जो असुरक्षित methodssame-originनहीं हैं उन्हें ठुकराने से सर्वर पर एक ही जाँच में CSRF रुक जाता है। - GET पर state बदलना
- GET पर डेटा बदलने वाला route किसी
<img>tag या link से चलाया जा सकता है, औरSameSite=Laxtop-level GET navigation पर cookie फिर भी भेजता है।request.valuesपढ़ने से form body के साथ query string भी स्वीकार हो जाती है।
हमले का चरण-दर-चरण प्रवाह
पीड़ित लॉग-इन है
उसके पास app.example की session cookie है, जो SameSite=None के साथ, या ऐसे browser में बिना attribute के भेजी जाती है जो डिफ़ॉल्ट रूप से Lax नहीं अपनाता।
वह हमलावर के नियंत्रण वाला पेज खोलता है
उसमें एक छिपा form है जो https://app.example/account/email पर भेजा जाता है और पेज लोड होते ही अपने-आप submit हो जाता है। हानिरहित टेस्ट मान email=test@example.com भेजने वाला form यह मामला दिखाता है।
browser cookie के साथ रिक्वेस्ट भेजता है
रिक्वेस्ट पीड़ित का session ले जाती है, और सर्वर को ऐसा कुछ नहीं दिखता जिससे पता चले कि form किसी दूसरी साइट पर था।
सर्वर खाते में बदलाव कर देता है
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"}
इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट
- state केवल POST, PUT, PATCH या DELETE पर बदलें, और form डेटा body (
request.form) से पढ़ें, कभीrequest.valuesया query string से नहीं। - पूरे ऐप के लिए framework का CSRF middleware चालू करें (
CSRFProtect, Django काCsrfViewMiddleware, Spring Security काcsrf()) और हर form व AJAX रिक्वेस्ट में token शामिल करें। - session cookies को
Secure,HttpOnlyऔरSameSite=LaxयाStrictपर सेट करें;SameSite=Noneकेवल उन cookies के लिए रखें जिन्हें cross-site iframes में चलना ज़रूरी है। - जिन असुरक्षित रिक्वेस्ट का
Sec-Fetch-Siteheadercross-siteयाsame-siteहो, उन्हें ठुकराएँ। - CI में ऐसा टेस्ट जोड़ें जो state बदलने वाले हर route को बिना token रिक्वेस्ट भेजे और 400 या 403 की अपेक्षा करे।