FlawOpen

flawopen.com/xss/Python

● CWE-79 · उच्च
सुरक्षा अनुसंधान · FlawOpen

Python में क्रॉस-साइट स्क्रिप्टिंग (XSS) (Flask, Jinja2, Django)

यूज़र इनपुट को Markup, mark_safe या render_template_string को दी गई f-स्ट्रिंग में रखने से किसी विज़िटर का टेक्स्ट दूसरे यूज़र्स के ब्राउज़र में उनके सेशन के साथ JavaScript बनकर चलता है (CWE-79)। Flask, Jinja2 और Django कहाँ अपने-आप एस्केप करते हैं, किन चार जगहों पर नहीं करते, और रिच टेक्स्ट को सुरक्षित रूप से कैसे अनुमति दें।

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

एक सुरक्षित फ़ॉर्म वाली छपाई की दुकान सोचिए: खाली जगह में लिखा पाठ सामान्य पाठ की तरह छपता है। लेकिन अगर कर्मचारी छपाई शुरू होने से पहले ग्राहक की पर्ची को प्रिंटर के निर्देश-पन्ने में चिपका दे, तो वह पर्ची छपाई का आदेश बन सकती है। render_template_string को दी गई Python f-string यही करती है: वह इनपुट को Jinja2 के स्रोत-पाठ में डाल देती है, इससे पहले कि autoescape उसे variable की तरह संभाले। Markup और mark_safe “HTML जाँचा जा चुका है” वाली मुहर जैसे हैं; वे जाँच करते ही नहीं। इनपुट को template variable रहने दें और autoescape को काम करने दें, या Django के छोटे HTML अंश के लिए format_html उपयोग करें।

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

Jinja2 autoescape
Flask .html, .htm, .xml और .xhtml टेम्पलेट तथा render_template_string के लिए Jinja2 autoescaping चालू करता है, इसलिए {{ q }} <img ...> बन जाता है। अलग से बना jinja2.Environment() तब तक autoescape बंद रखता है जब तक आप autoescape=select_autoescape() न दें।
Markup / |safe / mark_safe
तीनों किसी स्ट्रिंग को पहले से सुरक्षित HTML के रूप में चिह्नित करते हैं, और टेम्पलेट इंजन उसे एस्केप करना बंद कर देता है। ये डेवलपर का वादा हैं, सैनिटाइज़र नहीं: Markup(user_input) या mark_safe(user_input) यूज़र ने जो भी लिखा उसे छाप देता है।
render_template_string(f"...")
f-स्ट्रिंग Jinja2 के पार्स करने से पहले ही मान को टेम्पलेट सोर्स में डाल देती है, इसलिए autoescape उसे कभी वेरिएबल नहीं मानता। यही लाइन सर्वर-साइड टेम्पलेट इंजेक्शन भी है: {{ 7*7 }} जैसा मान Jinja2 हल कर देता है।
django.utils.html.format_html
str.format की तरह HTML बनाता है, लेकिन पहले हर आर्ग्युमेंट को conditional_escape से एस्केप करता है और फिर नतीजे को सुरक्षित चिह्नित करता है। Python कोड में छोटा HTML टुकड़ा बनाने का Django का मान्य तरीका यही है, mark_safe(f"...") के बजाय।
nh3.clean()
रिच टेक्स्ट को एस्केपिंग नहीं, सैनिटाइज़र चाहिए। nh3.clean() (Rust का ammonia) टैग और एट्रिब्यूट की अनुमति-सूची रखता है और <script>, इवेंट हैंडलर तथा javascript: URL हटा देता है। Bleach अब deprecated है; nh3 उसका सक्रिय उत्तराधिकारी है।

फ्रेमवर्क मैपिंग

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

चरण 1

इनपुट रिक्वेस्ट में आता है

सर्च बॉक्स, प्रोफ़ाइल फ़ॉर्म या लिंक पैरामीटर सर्वर को q, bio या next भेजता है। पीड़ित को तैयार किया हुआ लिंक भेजा जा सकता है, या मान सेव होकर प्रोफ़ाइल देखने वाले हर व्यक्ति को दिखता है।

चरण 2

मान एस्केपिंग से बच निकलता है

f-स्ट्रिंग उसे टेम्पलेट सोर्स में रख देती है, Markup() या mark_safe() उसे भरोसेमंद बता देते हैं, या वह href में पहुँचता है, जहाँ < और > एस्केप करना काफ़ी नहीं। हानिरहित टेस्ट मान <img src=x onerror=alert(1)> बिना बदले निकल जाता है।

चरण 3

ब्राउज़र उसे मार्कअप की तरह पढ़ता है

रिस्पॉन्स में <img src=x onerror=alert(1)> असली एलिमेंट के रूप में होता है। अगर इमेज resource लोड नहीं होता, तो onerror चलता है, और लिंक में javascript: URL क्लिक करने पर चलता है।

चरण 4

स्क्रिप्ट वेबसाइट बनकर चलती है

दिखाए गए Flask/Jinja और Django paths में निकला markup application origin पर चलता है। Untrusted values को autoescaped template data positions में रखें; HTML केवल context-appropriate sanitization के बाद स्वीकारें।

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

कमज़ोर कार्यान्वयन
# views.py: नीचे का हर हैंडलर रिक्वेस्ट टेक्स्ट को HTML बनकर पेज में जाने देता है
from django.utils.safestring import mark_safe
from flask import Flask, render_template_string, request
from markupsafe import Markup

app = Flask(__name__)


# f-स्ट्रिंग टेम्पलेट सोर्स बनाती है, इसलिए Jinja2 q को कभी एस्केप नहीं करता
@app.get("/search")
def search():
    q = request.args.get("q", "")
    return render_template_string(f"<h1>Results for {q}</h1>")


# Markup() Jinja2 से कहता है कि bio भरोसेमंद HTML है
@app.get("/profile")
def profile():
    bio = request.args.get("bio", "")
    return render_template_string("<div class='bio'>{{ bio }}</div>", bio=Markup(bio))


# f-स्ट्रिंग पर mark_safe name को HTML के रूप में छाप देता है
def greeting(name):
    return mark_safe(f"<p>Hello, {name}</p>")


# autoescape href में javascript: URL को नहीं रोकता
@app.get("/continue")
def continue_link():
    url = request.args.get("next", "/")
    return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
सुरक्षित और सुदृढ़ फ़िक्स
# views.py: मान टेम्पलेट वेरिएबल रहते हैं; रिच टेक्स्ट सैनिटाइज़ होता है
import nh3
from django.utils.html import format_html
from flask import Flask, render_template_string, request

app = Flask(__name__)


# q टेम्पलेट वेरिएबल है, इसलिए autoescape उसे एनकोड करता है
@app.get("/search")
def search():
    q = request.args.get("q", "")
    return render_template_string("<h1>Results for {{ q }}</h1>", q=q)


# HTML पर भरोसा करने से पहले nh3 टैग अनुमति-सूची लागू करता है
@app.get("/profile")
def profile():
    bio = request.args.get("bio", "")
    return render_template_string("<div class='bio'>{{ bio|safe }}</div>", bio=nh3.clean(bio))


# format_html नतीजे को सुरक्षित चिह्नित करने से पहले हर आर्ग्युमेंट एस्केप करता है
def greeting(name):
    return format_html("<p>Hello, {}</p>", name)


# लिंक के लक्ष्य के रूप में सिर्फ़ अपनी साइट के पाथ मान्य हैं
@app.get("/continue")
def continue_link():
    url = request.args.get("next", "/")
    if not url.startswith("/") or url.startswith("//"):
        url = "/"
    return render_template_string('<a href="{{ url }}">Continue</a>', url=url)

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

स्रोत