Injeksi XML External Entity (XXE, CWE-611): cara kerja dan cara mencegahnya
Parser XML yang mematuhi DTD sebuah dokumen akan mengambil entitas SYSTEM dari URL file:// atau http:// dan menempelkan hasilnya ke data yang di-parse, sehingga dokumen yang diunggah bisa membaca file server atau membuat server memanggil alamat internal. Bagaimana entitas eksternal dan ekspansi entitas bekerja, pengaturan parser mana yang menyebabkannya, dan bagaimana defusedxml dengan forbid_dtd=True mematikannya, dengan contoh Flask.
Penjelasan Sederhana (ELI5)
Sebuah percetakan mengisi surat berformat. Surat seorang pelanggan berbunyi: 'Yth. [sisipkan di sini isi laci 4 lemari arsip Anda]'. Pencetak yang teliti hanya menyisipkan kata-kata yang diberikan pelanggan. Pencetak yang ceroboh pergi ke laci 4, yang berisi kontrak pelanggan lain, lalu mencetaknya ke dalam surat yang dikirim balik lewat pos. XXE adalah pencetak yang ceroboh itu: dokumen XML menyuruh parser mengambil sebuah file atau alamat web lalu menyisipkannya, dan parser menurut. Perbaikannya adalah menolak setiap surat yang berisi instruksi 'sisipkan dari tempat lain'.
Konsep Kunci & Istilah
- DTD dan entitas
- Document Type Definition di dalam
<!DOCTYPE ...>bisa mendeklarasikan entitas, yaitu potongan teks bernama yang disubstitusi parser di setiap tempat&nama;muncul. Entitas internal berisi teks apa adanya; entitas eksternal menunjuk ke tempat lain. - Entitas eksternal
<!ENTITY x SYSTEM "file:///etc/hostname">menyuruh parser membaca URL itu dan menaruh isinya di tempat&x;dipakai. Dengan URLhttp://, yang mengirim request adalah server, sehingga XXE berubah menjadi pemalsuan request sisi server.- Ekspansi entitas (Billion Laughs)
- Entitas yang masing-masing merujuk entitas lain sepuluh kali mengembangkan dokumen satu kilobyte menjadi gigabyte teks di memori (CWE-776). Tidak perlu akses eksternal, jadi hanya menolak DTD atau membatasi ekspansi yang bisa menghentikannya.
- Pengaturan default parser
- Perilakunya bergantung pada library: lxml 5 secara default hanya me-resolve entitas internal, tetapi
resolve_entities=Truedanload_dtd=Truemembuka celahnya lagi;DocumentBuilderFactorydi Java me-resolve entitas eksternal kecualidisallow-doctype-decldisetel; dan .NET butuhDtdProcessing.Prohibit. - defusedxml
- Paket Python yang membungkus parser standar dan melempar
EntitiesForbidden,ExternalReferenceForbidden, atauDTDForbiddenalih-alih memproses konstruksi berbahaya.forbid_dtd=Truemenolak setiap dokumen yang memiliki DOCTYPE.
Alur Serangan Langkah demi Langkah
Penyerang mengunggah dokumen XML
Dokumen itu mendeklarasikan entitas eksternal dan merujuknya di field yang dikembalikan server. Entitas yang menunjuk ke file:///etc/hostname menggambarkan kasus pembacaan.
Parser memproses DTD
Karena aplikasi mengaktifkan pemuatan DTD dan resolusi entitas, parser membaca file atau URL yang dirujuk.
Isi file menjadi data
Teks itu masuk ke elemen customer, dan endpoint mengembalikannya dalam respons JSON.
Penyerang membaca file atau menjangkau host internal
File konfigurasi, kunci, dan metadata cloud bisa dibaca dengan cara ini. Pada 2014 Reginaldo Silva menerima bounty 33.500 dolar AS dari Facebook untuk XXE di penanganan OpenID-nya yang bisa membaca file di server mereka.
Kode Sumber: Rentan vs Aman
# invoices.py: parser menuruti apa pun yang diminta DTD unggahan
from flask import Flask, request
from lxml import etree
app = Flask(__name__)
# entitas di-resolve, DTD dimuat, dan akses jaringan diizinkan
@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: dokumen ber-DTD ditolak sebelum di-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
# tanpa DOCTYPE, tanpa entitas, tanpa rujukan eksternal; body dibatasi 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")}
Daftar Periksa Penguatan Sistem Rekayasa
- Parse XML tak tepercaya dengan
defusedxmldi Python, atau dengan DTD dimatikan di bahasa lain:disallow-doctype-declbernilai true di Java,DtdProcessing.Prohibitdi .NET, libxml2 tanpaXML_PARSE_NOENTdanXML_PARSE_DTDLOAD. - Jangan pernah memberi lxml
resolve_entities=True,load_dtd=True, atauno_network=Falseuntuk input yang tidak Anda kendalikan, dan jalankan Bandit (B320 menandailxml.etree) di CI. - Batasi ukuran request (
MAX_CONTENT_LENGTH) dan tolak setiap dokumen yang memuat DOCTYPE. - Perlakukan file SVG, SAML, DOCX, dan XLSX yang diunggah sebagai XML dan parse dengan pengaturan diperkeras yang sama.
- Tambahkan tes CI yang mengirim dokumen dengan entitas eksternal yang menunjuk ke
file:///etc/hostnamedan mengharapkan 400.