Injection Perintah dan Template

Injection di luar SQL: command injection pada shell dan SSTI pada template engine.

Daftar isi

Injection tidak berhenti pada SQL. Setiap tempat yang menyerahkan teks dari luar kepada penafsir (interpreter) membawa risiko yang sama, dan dua penafsir yang paling sering ditemui adalah shell sistem operasi serta template engine di sisi server. Topik ini membahas command injection pada shell dan server-side template injection (SSTI) pada template engine, keduanya direproduksi dengan Python di lab lokal beserta perbaikan strukturalnya. Penyebabnya sama dengan SQL injection, yaitu data yang tercampur dengan kode, sehingga pola perbaikannya pun berada di titik yang sama.

Command injection pada shell#

Shell membaca dan menafsirkan perintah, termasuk karakter khusus seperti titik koma yang memisahkan dua perintah. Apabila aplikasi merangkai masukan ke dalam string perintah lalu menyerahkannya kepada shell, masukan tersebut ikut ditafsirkan. Skrip berikut menyimulasikan fitur pemantauan host yang lazim memanggil ping, dengan echo sebagai penggantinya agar lab berjalan tanpa jaringan. Pola kerjanya identik dengan aplikasi nyata yang memanggil alat baris perintah dari masukan pengguna.

python command-injection.py
# command-injection.py — masukan dirangkai ke perintah yang dijalankan shell.
import subprocess


def balas_ping(host):
    # rentan: string perintah dibangun dari masukan lalu diserahkan ke shell
    perintah = f"echo host:{host}"
    hasil = subprocess.run(perintah, shell=True, capture_output=True, text=True)
    return hasil.stdout.strip()


print("Panggilan normal:")
print(balas_ping("127.0.0.1"))
print()
print("Panggilan dengan payload ; echo PWNED:")
print(balas_ping("127.0.0.1; echo PWNED"))
text Keluaran
Panggilan normal:
host:127.0.0.1

Panggilan dengan payload ; echo PWNED:
host:127.0.0.1
PWNED

Keluaran panggilan kedua memuat dua baris, dan baris PWNED membuktikan bahwa perintah kedua ikut tereksekusi. Titik koma pada payload tidak pernah menjadi bagian dari alamat host karena shell memotongnya menjadi dua perintah terpisah. Nilai host juga tidak pernah diperiksa oleh aplikasi karena seluruh string perintah diteruskan kepada shell dalam sekali panggil, sehingga pemisahan tugas antara pengelolaan nilai dan penafsiran perintah hilang sama sekali. Pada aplikasi nyata, perintah kedua itu dapat berupa apa pun yang berhak dijalankan proses aplikasi, sehingga dampaknya mengikuti hak akses proses tersebut. Serangan sejenis tercantum dalam kategori A05 Injection pada OWASP Top 10:2025, dan lembar panduan pertahanannya tersedia di referensi topik ini.

Perbaikan: tanpa shell, lalu shlex.quote#

Perbaikan strukturalnya adalah tidak melibatkan shell sama sekali. Fungsi subprocess.run menerima daftar argumen, dan apabila argumen shell dibiarkan False, daftar itu diteruskan langsung ke program tujuan tanpa penafsiran karakter khusus. Titik koma, tanda kutip, maupun $() kehilangan maknanya karena tidak ada shell yang membacanya. Skrip berikut menggunakan payload yang sama dengan versi sebelumnya.

python command-aman.py
# command-aman.py — argumen diteruskan tanpa shell, dan shlex.quote bila shell wajib.
import shlex
import subprocess


def balas_ping_aman(host):
    # shell=False: daftar argumen diteruskan langsung ke program, tanpa shell
    hasil = subprocess.run(["echo", f"host:{host}"], shell=False, capture_output=True, text=True)
    return hasil.stdout.strip()


payload = "127.0.0.1; echo PWNED"

print("Tanpa shell (payload menjadi satu argumen biasa):")
print(balas_ping_aman(payload))
print()
print(f"shlex.quote(payload) = {shlex.quote(payload)}")
text Keluaran
Tanpa shell (payload menjadi satu argumen biasa):
host:127.0.0.1; echo PWNED

shlex.quote(payload) = '127.0.0.1; echo PWNED'

Kali ini payload tampil apa adanya sebagai satu argumen, dan tidak ada perintah kedua yang berjalan. Apabila shell memang tidak terhindarkan, misalnya alat tujuan hanya menyediakan sintaks pipeline, fungsi shlex.quote membungkus nilai menjadi satu kutipan aman sehingga karakter khususnya diperlakukan sebagai teks. Kedua lapisan itu tetap menyisakan satu langkah pencegahan di depannya, yaitu memvalidasi bentuk masukan sebelum digunakan, misalnya alamat host diperiksa dengan modul ipaddress dari pustaka standar.

SSTI pada template engine#

Template engine merender templat dengan mengevaluasi ekspresi yang ditandai pembatasnya, misalnya {{ ... }} pada Jinja2. Kerentanannya muncul ketika teks templat itu sendiri disusun dari masukan, sehingga pembatas yang dikirim pengguna ikut dievaluasi pada waktu render. Serangan ini disebut server-side template injection (SSTI) karena evaluasinya berlangsung di server. Skrip berikut membandingkan dua cara menampilkan nama pengguna pada templat Jinja2.

python ssti-jinja.py
# ssti-jinja.py — template dirangkai dari masukan vs masukan lewat variabel.
from jinja2 import Template

nama = "{{ 7 * 7 }}"

print("Rentan: template disusun dari masukan, lalu dirender")
print(Template(f"Halo {nama}").render())
print()
print("Aman: masukan dikirim sebagai nilai variabel template")
print(Template("Halo {{ nama }}").render(nama=nama))
text Keluaran
Rentan: template disusun dari masukan, lalu dirender
Halo 49

Aman: masukan dikirim sebagai nilai variabel template
Halo {{ 7 * 7 }}

Keluaran pertama menampilkan 49 karena payload {{ 7 * 7 }} ikut menjadi bagian templat dan dievaluasi sebagai ekspresi, sedangkan keluaran kedua menampilkan payload apa adanya karena masukan dikirim sebagai nilai variabel yang sudah jadi. Ekspresi perkalian hanya contoh tak berbahaya, sedangkan templat yang sepenuhnya dikendalikan penyerang dapat menelusuri objek internal Python hingga menemukan fungsi berbahaya, sehingga SSTI berujung pada eksekusi perintah di server. Perbaikannya konsisten dengan bagian sebelumnya, yaitu teks templat bersifat tetap dan data masuk lewat variabel, sedangkan fitur yang memang perlu memberi pengguna kontrol atas templat menggunakan lingkungan sandbox milik Jinja2 bernama SandboxedEnvironment.

Pendeteksian pada audit mengikuti pola keluaran di atas. Masukan diberi teks pembeda seperti zqz{{ 7 * 7 }}zqz, lalu halaman yang dirender diperiksa: angka 49 di antara penanda berarti ekspresi dievaluasi, sedangkan teks yang tampil apa adanya berarti masukan hanya menjadi data. Bentuk error juga membantu mengenali engine yang digunakan, karena setiap template engine melempar pesan yang berbeda ketika pembatasnya tidak seimbang. Pengujian semacam ini cukup dilakukan pada fitur yang mengubah teks templat, dan hasil positifnya diperlakukan sebagai temuan dengan prioritas tinggi.

Kesalahan umum#

Kesalahan pertama adalah memblokir titik koma pada masukan lalu merasa aman. Payload tetap lolos melalui &&, $(), tanda kutip balik, maupun baris baru yang sama-sama ditafsirkan shell, sehingga perbaikan yang sah tetap berada pada shell=False. Daftar hitam karakter memiliki nasib yang sama dengan daftar hitam tag pada XSS, yaitu selalu tertinggal dari variasi payload berikutnya. Kesalahan kedua adalah memanggil shell=True demi kenyamanan, misalnya untuk menggunakan pipeline, padahal pipeline dapat ditulis dalam Python atau dibangun dengan beberapa panggilan subprocess yang disambungkan.

Kesalahan ketiga adalah menyangkutkan SSTI hanya pada fitur yang tampak teknis. Pola ini sering muncul pada fitur yang tampak jinak seperti penyunting templat pesan email atau nama tag yang dicetak melalui templat, sehingga semua titik penyusunan teks templat perlu dicari pada saat audit. Langkah awalnya murah, yaitu mencari pemanggilan shell=True dan penyusunan templat dari string pada kode, lalu setiap temuannya ditelusuri sampai ke sumber masukannya. Temuan yang sama berlaku untuk kedua pola sekaligus karena keduanya berbagi akar penyebab, yaitu data yang menyentuh penafsir tanpa pemisahan yang jelas.

Ringkasan#

  • Command injection terjadi karena shell menafsirkan karakter khusus pada perintah yang dirangkai dari masukan, dan perbaikannya adalah meneruskan daftar argumen dengan shell=False.
  • Apabila shell tidak terhindarkan, shlex.quote membungkus nilai agar karakter khususnya diperlakukan sebagai teks.
  • SSTI terjadi karena teks templat disusun dari masukan sehingga pembatas ekspresi ikut dievaluasi, dan perbaikannya adalah templat tetap dengan data lewat variabel.
  • Kedua kerentanan berbagi pola dengan SQL injection, yaitu data yang diperlakukan sebagai kode oleh penafsirnya.

Referensi#