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:

python
import numpy as np
import pickle

print(pickle.dumps("Budi"))

Output yang Dihasilkan:

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

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

text
halo dari pickle

Mari kita bedah:

  1. Baris 4-6 (__reduce__): method __reduce__ mendefinisikan bahwa membangun kembali objek ini berarti memanggil print dengan argumen "halo dari pickle".
  2. Baris 8 (pickle.dumps(...)): pasangan “fungsi + argumen” itulah yang tersimpan dalam byte — bukan hanya datanya.
  3. Baris 9 (pickle.loads(byte)): saat deserialisasi, fungsi print benar-benar dipanggil. Pada berkas berbahaya, posisi print diganti 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:

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

text
ValueError: Object arrays cannot be loaded when allow_pickle=False
object

Penjelasan tiap bagian:

  1. Baris 3-4 (dtype=object): array berisi dict Python dibuat dan disimpan; np.save menerima array object dengan menandai berkasnya sebagai berkas pickle.
  2. Baris 5-8 (try/except): np.load bawaan — tanpa parameter apa pun — menolak berkas tersebut dengan pesan Object arrays cannot be loaded when allow_pickle=False. Perlindungan ini aktif sejak bawaan; kita tidak perlu mengaturnya.
  3. 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 berkasPerlu allow_pickle=True?
.npy / .npz berdtype numerik, string, booleantidak — muat apa adanya
.npy berdtype objectya — 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:

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

text
dtype : float64
ndim  : 1 | shape: (5,)
nan   : True
min   : 27.5 | max: 30.2
di luar rentang: False

Mari kita telaah:

  1. Baris 5 (a = np.load(...)): dimuat dengan bawaan penuh — tanpa allow_pickle, tanpa mmap_mode.
  2. Baris 6-8: dtype dan bentuk diverifikasi lebih dulu; bila program mengharapkan float64 1-D, ketidakcocokan harus dihentikan di sini, sebelum data dipakai.
  3. Baris 9-11: kehadiran nan diperiksa dengan np.isnan, lalu np.nanmin/np.nanmax memberi batas nilai yang sah — terlihat satu nan pada data contoh, sinyal yang harus ditindaklanjuti sebelum perhitungan lanjutan (perlakuannya diulas pada Chapter Missing Values dan Validasi).