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:

python
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:

text
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:

  1. Baris 5-7 (__reduce__): method ini mengembalikan pasangan callable dan argument — di sini print dengan satu string. pickle menyimpan resep tersebut di dalam bytes.
  2. 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.
  3. Baris 14 (pickle.loads(data_jahat)): pembangunan ulang objek mengeksekusi __reduce__ tanpa bertanya — string penyerang tercetak. Tidak ada yang memanggil print secara 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:

python
import json

data_json = '{"nama": "Budi", "peran": "admin"}'
hasil = json.loads(data_json)
print("Hasil json.loads:", hasil, "-", type(hasil).__name__)

Output yang Dihasilkan:

text
Hasil json.loads: {'nama': 'Budi', 'peran': 'admin'} - dict

Input 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():

python
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:

text
safe_load menolak payload -> ConstructorError

Fungsi 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.