● CWE-611 · OWASP A05:2021

XML External Entity (XXE) इंजेक्शन (CWE-611): यह कैसे काम करता है और इससे कैसे बचें

जो XML parser किसी document के DTD को मानता है, वह file:// या http:// URLs से SYSTEM entities लाकर नतीजे को parse किए डेटा में चिपका देता है, इसलिए अपलोड किया गया document सर्वर की फ़ाइलें पढ़ सकता है या सर्वर से आंतरिक पतों को कॉल करवा सकता है। external entities और entity expansion कैसे काम करते हैं, parser की कौन-सी settings इनकी वजह हैं, और forbid_dtd=True के साथ defusedxml इन्हें कैसे बंद करता है, Flask के उदाहरण के साथ।

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

एक छापाख़ाना फ़ॉर्म-पत्र भरता है। एक ग्राहक के पत्र में लिखा है: 'प्रिय [यहाँ अपनी फ़ाइल-अलमारी की दराज़ 4 की सामग्री डालें]'। सावधान मुद्रक केवल ग्राहक के दिए शब्द डालता है। लापरवाह मुद्रक दराज़ 4 तक जाता है, जिसमें दूसरे ग्राहकों के अनुबंध रखे हैं, और उन्हें पत्र में छापकर डाक से वापस भेज देता है। XXE वही लापरवाह मुद्रक है: XML document parser से कहता है कि कोई फ़ाइल या वेब पता लाकर डाल दो, और parser ऐसा कर देता है। उपाय यह है कि 'कहीं और से डालो' जैसे निर्देश वाले हर पत्र को ठुकरा दिया जाए।

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

DTD और entities
<!DOCTYPE ...> में लिखी Document Type Definition entities घोषित कर सकती है, यानी नाम वाले टेक्स्ट टुकड़े जिन्हें parser हर उस जगह रख देता है जहाँ &नाम; आता है। internal entities में सीधा टेक्स्ट होता है; external entities कहीं और इशारा करती हैं।
External entity
<!ENTITY x SYSTEM "file:///etc/hostname"> parser से कहता है कि वह URL पढ़े और उसकी सामग्री वहाँ रखे जहाँ &x; इस्तेमाल हुआ है। http:// URLs के साथ रिक्वेस्ट सर्वर भेजता है, जिससे XXE सर्वर-साइड रिक्वेस्ट फ़ॉर्जरी बन जाता है।
Entity expansion (Billion Laughs)
ऐसी entities जो दूसरी entities को दस-दस बार बुलाती हैं, एक किलोबाइट के document को memory में गीगाबाइट्स टेक्स्ट में फैला देती हैं (CWE-776)। इसके लिए बाहरी पहुँच ज़रूरी नहीं, इसलिए केवल DTDs ठुकराना या expansion सीमित करना ही इसे रोकता है।
Parser की डिफ़ॉल्ट settings
व्यवहार library पर निर्भर है: lxml 5 डिफ़ॉल्ट रूप से केवल internal entities resolve करता है, पर resolve_entities=True और load_dtd=True छेद फिर खोल देते हैं; Java का DocumentBuilderFactory external entities resolve करता है जब तक disallow-doctype-decl सेट न हो; और .NET को DtdProcessing.Prohibit चाहिए।
defusedxml
एक Python package जो मानक parsers को लपेटता है और ख़तरनाक constructs को process करने के बजाय EntitiesForbidden, ExternalReferenceForbidden या DTDForbidden उठाता है। forbid_dtd=True DOCTYPE वाले हर document को ठुकरा देता है।

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

चरण 1

हमलावर एक XML document अपलोड करता है

वह एक external entity घोषित करता है और उसे ऐसे field में इस्तेमाल करता है जिसे सर्वर वापस लौटाता है। file:///etc/hostname की ओर इशारा करने वाली entity फ़ाइल पढ़ने का मामला दिखाती है।

चरण 2

parser DTD को process करता है

चूँकि ऐप्लिकेशन ने DTD loading और entity resolution चालू कर रखे हैं, parser बताई गई फ़ाइल या URL पढ़ लेता है।

चरण 3

फ़ाइल की सामग्री डेटा बन जाती है

टेक्स्ट customer element में पहुँचता है, और endpoint उसे अपने JSON response में लौटा देता है।

चरण 4

हमलावर फ़ाइलें पढ़ता है या आंतरिक hosts तक पहुँचता है

configuration फ़ाइलें, keys और क्लाउड metadata इसी तरह पढ़े जा सकते हैं। 2014 में Reginaldo Silva को Facebook से 33,500 डॉलर का इनाम मिला, एक ऐसे XXE के लिए जो OpenID handling में था और सर्वरों की फ़ाइलें पढ़ सकता था।

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

कमज़ोर कार्यान्वयन
# invoices.py: अपलोड किया DTD जो माँगे, parser वही करता है
from flask import Flask, request
from lxml import etree

app = Flask(__name__)


# entities resolve, DTDs लोड और network access की अनुमति
@app.post("/invoices/import")
def import_invoice():
    parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=False)
    root = etree.fromstring(request.get_data(), parser)
    return {"customer": root.findtext("customer"), "total": root.findtext("total")}
सुरक्षित और सुदृढ़ फ़िक्स
# invoices.py: DTD वाले documents parse से पहले ठुकरा दिए जाते हैं
from defusedxml import DefusedXmlException
from defusedxml.ElementTree import ParseError, fromstring
from flask import Flask, abort, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1_000_000


# न DOCTYPE, न entities, न external references; body 1 MB तक सीमित
@app.post("/invoices/import")
def import_invoice():
    try:
        root = fromstring(request.get_data(), forbid_dtd=True)
    except (DefusedXmlException, ParseError):
        abort(400)
    return {"customer": root.findtext("customer"), "total": root.findtext("total")}

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

स्रोत

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