Memuat berkas data terasa seperti operasi pasif, padahal untuk sebagian format ia bisa bersifat aktif: beberapa mekanisme pemuatan dapat mengeksekusi kode yang tertanam di dalam berkas. Karena berkas data berpindah tangan — dari unduhan, email, atau pipeline yang tidak kita kendalikan sepenuhnya — memahami mekanisme ini menjadi bagian dari literasi NumPy, bukan sekadar urusan administrator sistem.
Bab ini membahas dari mana risiko itu berasal (modul pickle), bagaimana NumPy melindungi kita secara bawaan lewat parameter allow_pickle, dan rangkaian pemeriksaan yang sebaiknya dilakukan setiap kali data pihak lain dimuat.
Dari Mana Risiko Code Execution Berasal #
Format pickle tidak menyimpan data semata; ia menyimpan instruksi untuk membangun kembali objek. Mari kita lihat isi serialisasi sebuah string sederhana:
import numpy as np
import pickle
print(pickle.dumps("Budi"))Output yang Dihasilkan:
b'\x80\x04\x95\x08\x00\x00\x00\x00\x00\x00\x00\x8c\x04Budi\x94.'Output tersebut memperlihatkan opcode biner — \x8c dan \x94 adalah instruksi protokol pickle — bukan sekadar teks. Karena bentuknya instruksi, proses pickle.loads() dapat diminta memanggil fungsi apa pun. Berikut demonstrasi mekanismenya secara benar-benar aman, memakai fungsi print sebagai gantinya:
import numpy as np
import pickle
class Tampilkan:
def __reduce__(self):
return print, ("halo dari pickle",)
byte = pickle.dumps(Tampilkan())
pickle.loads(byte)Output yang Dihasilkan:
halo dari pickleMari kita bedah:
- Baris 4-6 (
__reduce__): method__reduce__mendefinisikan bahwa membangun kembali objek ini berarti memanggilprintdengan argumen"halo dari pickle". - Baris 8 (
pickle.dumps(...)): pasangan “fungsi + argumen” itulah yang tersimpan dalam byte — bukan hanya datanya. - Baris 9 (
pickle.loads(byte)): saat deserialisasi, fungsiprintbenar-benar dipanggil. Pada berkas berbahaya, posisiprintdiganti fungsi apa pun yang tersedia di mesin penerima — inilah inti risiko arbitrary code execution.
allow_pickle pada np.load #
NumPy memakai pickle hanya untuk satu kasus: array berdtype object, yang elemennya bisa berupa objek Python apa pun — dict, instance kelas, atau list campuran. Menyimpan array semacam ini berjalan normal, tetapi NumPy 2.x menolak memuatnya secara bawaan:
import numpy as np
catatan = np.array([{"stasiun": "Jakarta", "suhu": 27.5}], dtype=object)
np.save("catatan.npy", catatan)
try:
b = np.load("catatan.npy")
except ValueError as e:
print("ValueError:", e)
b = np.load("catatan.npy", allow_pickle=True)
print(b.dtype)Output yang Dihasilkan:
ValueError: Object arrays cannot be loaded when allow_pickle=False
objectPenjelasan tiap bagian:
- Baris 3-4 (
dtype=object): array berisi dict Python dibuat dan disimpan;np.savemenerima array object dengan menandai berkasnya sebagai berkas pickle. - Baris 5-8 (try/except):
np.loadbawaan — tanpa parameter apa pun — menolak berkas tersebut dengan pesanObject arrays cannot be loaded when allow_pickle=False. Perlindungan ini aktif sejak bawaan; kita tidak perlu mengaturnya. - Baris 9-10 (
allow_pickle=True): hanya dengan menyatakan izin secara eksplisit array object dimuat. Pengesahan eksplisit ini sengaja dirancang agar keputusan “saya percaya berkas ini” tidak terjadi tanpa disadari.
| Jenis berkas | Perlu allow_pickle=True? |
|---|---|
.npy / .npz berdtype numerik, string, boolean | tidak — muat apa adanya |
.npy berdtype object | ya — dan berarti memercayai isi berkas |
| CSV, teks (loadtxt/genfromtxt) | tidak pernah — penguraian teks tidak mengeksekusi kode |
Pola Memuat yang Aman #
Perlindungan bawaan baru menutup satu pintu. Pemeriksaan isi tetap diperlukan, karena berkas numerik pun bisa berisi data yang tidak layak hitung: nan yang menular, dimensi yang salah, atau nilai di luar rentang wajar. Pola berikut merangkum pemeriksaan minimum setelah np.load:
import numpy as np
suhu = np.array([27.5, 28.1, np.nan, 30.2, 29.8])
np.save("sensor.npy", suhu)
a = np.load("sensor.npy")
print("dtype :", a.dtype)
print("ndim :", a.ndim, "| shape:", a.shape)
print("nan :", np.isnan(a).any())
print("min :", np.nanmin(a), "| max:", np.nanmax(a))
print("di luar rentang:", ((a < -10) | (a > 45)).any())Output yang Dihasilkan:
dtype : float64
ndim : 1 | shape: (5,)
nan : True
min : 27.5 | max: 30.2
di luar rentang: FalseMari kita telaah:
- Baris 5 (
a = np.load(...)): dimuat dengan bawaan penuh — tanpaallow_pickle, tanpammap_mode. - Baris 6-8: dtype dan bentuk diverifikasi lebih dulu; bila program mengharapkan
float641-D, ketidakcocokan harus dihentikan di sini, sebelum data dipakai. - Baris 9-11: kehadiran
nandiperiksa dengannp.isnan, lalunp.nanmin/np.nanmaxmemberi batas nilai yang sah — terlihat satunanpada data contoh, sinyal yang harus ditindaklanjuti sebelum perhitungan lanjutan (perlakuannya diulas pada Chapter Missing Values dan Validasi).