● CWE-611 · OWASP A05:2021

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 URL http://, 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=True dan load_dtd=True membuka celahnya lagi; DocumentBuilderFactory di Java me-resolve entitas eksternal kecuali disallow-doctype-decl disetel; dan .NET butuh DtdProcessing.Prohibit.
defusedxml
Paket Python yang membungkus parser standar dan melempar EntitiesForbidden, ExternalReferenceForbidden, atau DTDForbidden alih-alih memproses konstruksi berbahaya. forbid_dtd=True menolak setiap dokumen yang memiliki DOCTYPE.

Alur Serangan Langkah demi Langkah

Langkah 1

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.

Langkah 2

Parser memproses DTD

Karena aplikasi mengaktifkan pemuatan DTD dan resolusi entitas, parser membaca file atau URL yang dirujuk.

Langkah 3

Isi file menjadi data

Teks itu masuk ke elemen customer, dan endpoint mengembalikannya dalam respons JSON.

Langkah 4

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

IMPLEMENTASI RENTAN
# 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")}
PERBAIKAN AMAN & KUAT
# 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

Sumber

← Lihat seluruh direktori keamanan Semua panduan kerentanan →