Aplikasi sering perlu menyimpan atau menerima objek Python dalam bentuk bytes yang bisa dibangun kembali — cache, antrian tugas, atau data dari layanan lain. Proses membangun ulang objek dari data mentah disebut deserialization, dan di sinilah bahayanya: membangun objek bisa berarti menjalankan kode. Format yang mampu menyimpan perilaku — paling terkenal pickle — membuat penyerang cukup mengirim satu payload untuk mengeksekusi perintah apa pun di server kita. Chapter ini mendemonstrasikan mekanismenya secara aman, lalu menunjukkan alternatif yang benar.
pickle Menjalankan Kode #
Serangan pickle berputar di satu method ajaib: __reduce__(). Saat pickle.loads() membangun ulang objek, method ini boleh menentukan callable beserta argument-nya yang akan dieksekusi langsung. Demo berikut tidak melakukan kerusakan apa pun — hanya print — tetapi print bisa diganti os.system atau koneksi balik ke server penyerang:
import pickle
class MuatanJahat:
def __reduce__(self):
# fungsi ini dijalankan OTOMATIS oleh pickle.loads()
return (print, ("Kode penyerang tereksekusi di server Anda!",))
data_jahat = pickle.dumps(MuatanJahat())
print("Data pickle (potongan):", data_jahat[:24], "...")
print("Korban menjalankan pickle.loads():")
pickle.loads(data_jahat)Output yang Dihasilkan:
Data pickle (potongan): b'\x80\x04\x95G\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x05' ...
Korban menjalankan pickle.loads():
Kode penyerang tereksekusi di server Anda!Mari kita bedah kejadiannya:
- Baris 5-7 (
__reduce__): method ini mengembalikan pasangan callable dan argument — di siniprintdengan satu string.picklemenyimpan resep tersebut di dalam bytes. - Baris 10 (
pickle.dumps(...)): hasil serialisasi tampak seperti bytes biasa, tidak berbahaya dilihat matanya. Payload nyata justru sering disembunyikan di balik base64 atau di dalam file sesi cache. - Baris 14 (
pickle.loads(data_jahat)): pembangunan ulang objek mengeksekusi__reduce__tanpa bertanya — string penyerang tercetak. Tidak ada yang memanggilprintsecara eksplisit; eksekusinya melekat pada deserialization.
Dampaknya tidak berhenti di satu proses: cache Redis yang dibagikan, file antrean tugas, dan data session adalah jalur favorit payload pickle masuk ke server.
Alternatif Aman: json #
Untuk data yang cukup berbentuk dictionary, list, angka, dan string, json adalah pengganti yang aman secara desain: json.loads() hanya membangun struktur data, tidak pernah mengeksekusi apa pun:
import json
data_json = '{"nama": "Budi", "peran": "admin"}'
hasil = json.loads(data_json)
print("Hasil json.loads:", hasil, "-", type(hasil).__name__)Output yang Dihasilkan:
Hasil json.loads: {'nama': 'Budi', 'peran': 'admin'} - dictInput yang sama, jika dijahiti payload pickle, akan gagal sebagai JSON tidak sah — tidak ada jalur eksekusi kode sama sekali. Konsekuensinya JSON tidak bisa menyimpan perilaku, dan itu justru kualitas yang kita cari untuk data dari luar.
YAML: safe_load versus load #
Format YAML punya sejarah serupa. Standarnya memungkinkan tag seperti !!python/object/apply: yang membangun objek Python, sehingga file YAML tak terpercaya bisa menjadi pintu eksekusi kode. PyYAML menutup pintu ini lewat yaml.safe_load():
import yaml
payload = "!!python/object/apply:print [Kode dari YAML tereksekusi]"
try:
yaml.safe_load(payload)
except yaml.YAMLError as e:
print("safe_load menolak payload ->", type(e).__name__)Output yang Dihasilkan:
safe_load menolak payload -> ConstructorErrorFungsi safe_load() hanya mengizinkan tipe data dasar (dictionary, list, string, angka, tanggal), sehingga tag eksekusi kode ditolak dengan ConstructorError. Sejak PyYAML versi 6, fungsi yaml.load() bahkan mengharuskan kita menyebut Loader secara eksplisit — pilih yaml.SafeLoader atau yaml.FullLoader yang keduanya menolak object tag berbahaya; hanya yaml.UnsafeLoader yang mengeksekusinya, dan namanya sudah menyatakan risikonya sendiri.
Menandatangani Payload #
Sisa satu kekhawatiran: bagaimana jika kita memang harus menerima data terstruktur dari pihak lain dan ingin memastikan data tersebut tidak dimanipulasi di tengah jalan? Pola standarnya adalah menandatangani payload dengan HMAC — kunci rahasia dibagi kedua pihak, hmac.new(kunci, payload_bytes, hashlib.sha256) menghasilkan tanda tangan, dan penerima memverifikasi tanda tangan itu sebelum memproses payload. Tanda tangan tidak menjadikan data aman untuk dieksekusi, tetapi menjamin hanya pemegang kunci yang bisa membuatnya. Gabungkan: format inert seperti JSON untuk bentuknya, HMAC untuk asalnya, dan pickle hanya untuk data tepercaya internal — kombinasi yang kita pakai juga saat memvalidasi input pada Chapter Validasi dan Sanitasi Input.