Template Injection (SSTI) terjadi waktu input dari user ditempel langsung ke dalam template di server, lalu template engine menjalankan isinya. Bayangin surat undangan mail merge: ada template "Halo {nama}," dan mesin mengisi bagian nama. Yang aman: mesin cuma menaruh teks namamu. Yang bahaya: mesin membaca apa pun di kotak nama sebagai perintah, jadi kalau kamu tulis 7*7 di situ, mesinnya betulan menghitung dan mencetak 49.
🧪 Tebakan cepat: di app yang rentan, user mengisi nama dengan
{{7*7}}. Yang muncul di halamanHalo 49!,Halo {{7*7}}!, atau error? Tebak dulu, jawabannya ada di bagian "Jalankan di Lab".
Dua istilah dulu
- Template: cetakan halaman dengan bagian kosong yang diisi data, misalnya
Halo {{ nama }}!.{{ nama }}itu slot yang diganti nilainama. - Template engine: program di server yang membaca template, mengisi slotnya, lalu mengembalikan HTML. Contohnya Jinja2 (Python), Twig (PHP), ERB (Ruby).
Masalahnya muncul kalau kamu bikin template-nya dari input user, bukan mengisi slot dengan input user. Itu dua hal yang beda, dan bedanya menentukan aman atau tidak.
Kode yang rentan
# RENTAN: input user jadi template-nya
@app.route("/greet")
def greet():
nama = request.args.get("nama")
template = f"Halo {nama}!" # nama ditempel ke string template
return render_template_string(template) # engine merender string itu
f"Halo {nama}!"menempelkannamake dalam string yang nanti dirender sebagai template.render_template_string(template)menyuruh Jinja2 merender string itu. Kalaunamaberisi{{7*7}}, string-nya jadiHalo {{7*7}}!, dan Jinja2 melihat{{7*7}}sebagai ekspresi, lalu menghitungnya.- Masalah intinya: input user dijadikan template langsung. Jadi apa pun sintaks template yang dikirim user ikut dieksekusi engine, bukan cuma ditampilkan.
Jalankan di Lab
Di Lab ada halaman Halo <nama>! dengan dua mode. Isi nama dengan {{7*7}}:
| Mode | Yang terjadi | Hasil di halaman |
|---|---|---|
| Render input sebagai template (rentan) | engine melihat {{7*7}} sebagai ekspresi, lalu menghitung |
Halo 49! |
| Kirim input sebagai data (aman) | engine cuma menaruh teks apa adanya | Halo {{7*7}}! |
Jadi jawaban tebakannya: di app yang rentan, muncul Halo 49!. Angka 49 yang muncul itu tanda bahaya: kalau 7*7 saja dievaluasi, berarti engine mau menjalankan ekspresi lain juga. Dari situ penyerang lanjut membaca konfigurasi server, lalu bisa sampai menjalankan perintah sistem. Itu sebabnya SSTI dianggap setara dengan Remote Code Execution (RCE), yaitu penyerang bisa menjalankan kode di server.
Cara memperbaiki: data, bukan template
# AMAN: slot-nya tetap, user cuma mengisi datanya
@app.route("/greet")
def greet():
nama = request.args.get("nama")
return render_template("greet.html", nama=nama)
# greet.html berisi: Halo {{ nama }}!
- Template-nya
greet.html, ditulis developer, tetap, dan gak pernah berubah oleh input user. nama=namamengirim input user sebagai data untuk mengisi slot{{ nama }}.- Kalau user mengirim
{{7*7}}, nilainya cuma teks, jadi yang munculHalo {{7*7}}!, bukan49. Nilai yang masuk ke slot juga di-escape otomatis, jadi tag HTML pun ikut jinak.
Aturan satu kalimat: template ditulis developer, input user cuma mengisi slotnya.
⚠️ Jebakan yang sering ketemu
- Membangun template dari string input.
render_template_string(f"Halo {nama}")menaruh input user ke dalam template. Pakairender_template(...)dengan file tetap, lalu kirim input sebagai variabel. - Mengira
{{7*7}}cuma teks iseng. Begitu49muncul, engine sudah mengeksekusi kode user. Itu bukti app-nya rentan, bukan sekadar bug tampilan. - Mengandalkan filter karakter. Memblokir
{{doang gak cukup, karena engine juga punya sintaks lain seperti{% %}. Perbaikannya di cara render, bukan menyaring tanda kurung. - Mengira semua engine sama. Blade di Laravel dan
{{ }}yang mengisi slot itu aman karena meng-escape. Yang bahaya adalah merender string buatan user sebagai template.
TL;DR: SSTI (Server-Side Template Injection) terjadi saat input user dijadikan template langsung, lalu template engine mengeksekusinya. Kirim {{7*7}} ke app yang rentan, muncul Halo 49!: engine menghitung ekspresinya, dan dari situ bisa sampai Remote Code Execution. Perbaikannya: tulis template tetap (render_template), dan kirim input user sebagai data yang mengisi slot, bukan sebagai template-nya. Jangan andalkan filter karakter.