Ringkasan Utama
Insiden Hugging Face Juli 2026 bermula dari dataset berbahaya yang menjalankan kode lewat remote-code loader dan template injection pada konfigurasinya. Dari worker, penyerang naik ke node lalu ke credential cluster. Tim defender perlu memperlakukan dataset dan config unggahan sebagai program tak tepercaya, mengisolasi worker, membatasi admission, dan merotasi token.
Pada 16 Juli 2026 Hugging Face menerbitkan disclosure singkat yang mungkin dibaca banyak orang di dunia ML sebagai masalah pihak lain. Sebuah dataset, benda yang di-download setiap training script tanpa pikir panjang, dipakai untuk menjalankan kode di dalam infrastruktur platform itu sendiri. Saya membaca postingannya dua kali sebelum sadar apa yang tidak ada di dalamnya: tidak ada CVE, tidak ada tanggal pasti, tidak ada versi library. Detailnya tipis, dan bagian yang berguna justru bentuk serangannya.
Ini bukan cerita ulang insiden, dan saya tidak punya informasi dari dalam sistem Hugging Face. Saya membaca disclosure-nya, release notes dan docs tool yang terlibat, serta dokumentasi Kubernetes dan Jinja untuk kontrol yang relevan. Setiap klaim di bawah bisa ditelusuri ke salah satu halaman itu, dan di mana disclosure diam, saya bilang diam, bukan mengisinya sendiri. Tidak ada payload exploit di sini, hanya sisi defender.
Apa kata disclosure, dan apa yang tidak disebut
Postingan itu menjelaskan sebuah dataset berbahaya yang menyalahgunakan dua jalur eksekusi kode dalam pemrosesan dataset: remote-code dataset loader dan template injection pada konfigurasi dataset. Penyerang menjalankan kode di sebuah processing worker, naik ke akses level node, mengambil credential cloud dan cluster, lalu bergerak lateral ke beberapa cluster internal. Hugging Face menyebut pergerakan itu terjadi selama akhir pekan dan bahwa mereka mendeteksi serta merespons pada awal pekan penerbitan.
Dampak yang dilaporkan lebih sempit daripada jalurnya. Penyerang mendapat akses tanpa izin ke sebagian kecil dataset internal dan beberapa service credential. Hugging Face mengatakan tidak menemukan bukti perubahan pada model, dataset, atau Spaces publik, dan container image serta package yang dipublikasikan sudah diverifikasi bersih. Mereka masih menilai apakah data partner atau pelanggan terdampak dan akan menghubungi pihak terkait secara langsung.
Ada dua detail yang layak mendapat perhatian lebih. Kampanye ini dijalankan oleh autonomous agent framework, dengan ribuan aksi lewat sandbox berumur pendek dan command-and-control di layanan publik, dan model di baliknya tidak diketahui. Lalu insiden ini pertama kali terdeteksi lewat AI-assisted detection, ketika triase berbasis LLM mengkorelasikan sinyal-sinyalnya. Yang tidak ada juga penting untuk seberapa jauh Anda boleh menyimpulkan: tidak ada tanggal, nama library, nomor versi, maupun cloud provider. Apa pun yang lebih spesifik dari paragraf ini adalah spekulasi, termasuk dari saya.
Dataset adalah input bagi sebuah program, dan sebagian input adalah program
Pembacaan yang berguna bukan 'dataset itu berbahaya'. Melainkan bahwa setiap tahap antara upload dan node adalah tempat data ditafsirkan, dan setiap penafsiran memberi sesuatu kepada pengunggah. Jika disusun sebagai rantai, serangan ini tidak lagi terlihat eksotis. Tiap baris di bawah adalah tahap yang disebut atau tersirat dalam disclosure, apa yang dipercaya tahap itu, dan kontrol yang akan mempersempitnya.
| Tahap | Yang dipercaya | Kontrol yang mempersempit |
|---|---|---|
| Dataset loader | Bahwa kode loading milik repository aman dieksekusi | Jangan pernah opt in ke remote code; tentukan sendiri format packaged-nya |
| Konfigurasi dataset | Bahwa string di config adalah data, bukan template yang dievaluasi | Tanpa template engine untuk config unggahan; sandbox hanya jika tak terhindarkan |
| Processing worker | Bahwa kode yang jalan di pod tetap di dalam pod | Pod Security Admission dan admission policy pada namespace worker |
| Node dan identitas cloud | Bahwa container tidak bisa membaca credential cloud milik node | Hardening metadata service, role node dengan least privilege |
| Cluster internal | Bahwa credential satu cluster tidak berlaku di cluster lain | Identitas per cluster, credential berumur pendek, rotasi saat ada kecurigaan |
Baca kolom kanan sebagai daftar pertanyaan untuk pipeline Anda sendiri, bukan klaim tentang apa yang kurang di Hugging Face. Disclosure tidak menyebut kontrol mana yang absen, hanya bahwa setelahnya mereka menutup jalur eksekusi kode, membangun ulang node, dan menambah admission control yang lebih ketat. Polanya juga bukan hal baru di ML: dokumentasi Hub Hugging Face sendiri menyatakan bahwa pickle, format default bobot PyTorch, bisa mengeksekusi kode sembarang saat di-load, dan bahwa scanner mereka tidak 100 persen andal. Dataset ternyata punya sifat yang sama.
Remote-code loader: opt-in adalah keputusan kepercayaan
Dataset loading script adalah Python yang dikirim penulis dataset bersama datanya. Dokumentasi datasets Hugging Face di branch main menyatakan bahwa demi keamanan library tidak menjalankan script ini secara default dan Anda harus mengirim trust_remote_code=True untuk memuat dataset yang membutuhkannya. Flag itu adalah seluruh batasnya: seseorang, atau sebuah pipeline, berkata 'jalankan apa yang repository ini suruh'. Di processing worker yang menelan dataset milik orang lain, kalimat itulah kerentanannya.
# 1. Find every place your own code opts in to running a dataset's code.
grep -rnE "trust_remote_code\s*=\s*True" --include="*.py" .
# 2. Know which datasets release each worker image really has. Release notes
# for 4.0.0 list "Remove scripts altogether" and say trust_remote_code is
# no longer supported - but the main docs page still describes scripts.
# Believe the installed package, not either page.
pip show datasets | grep -i "^Version"
# 3. A third-party dataset with data files needs no code at all. Name the
# packaged builder yourself so a repo-supplied script is never consulted.
python - <<'EOF'
from datasets import load_dataset
# Wrong: the repo decides how it gets loaded (script, builder, config).
# ds = load_dataset("some-org/some-dataset")
# Right: you decide the format; the repo only supplies bytes.
ds = load_dataset(
"parquet",
data_files="hf://datasets/some-org/some-dataset/data/train-00000-of-00001.parquet",
split="train",
)
print(ds.num_rows)
EOFAda ganjalan yang tidak bisa saya selesaikan dari halaman yang saya baca. Release notes 4.0.0 library datasets mencantumkan 'Remove scripts altogether' sebagai breaking change dan menyatakan trust_remote_code tidak lagi didukung, tetapi halaman docs branch main masih menjelaskan script dan flag itu. Disclosure juga tidak menyebut apakah library itu terlibat, atau versi mana yang dipakai worker. Jadi saran jujurnya membosankan: cek versi terpasang di setiap worker image, dan jangan berasumsi image lama atau dependency yang di-pin punya perilaku yang lebih baru.
Dua aturan praktis menyusul. Pertama, cari opt-in itu di kode Anda sendiri, karena flag yang diset sekali di sebuah helper diam-diam berlaku untuk setiap dataset yang lewat. Kedua, pindahkan keputusan format keluar dari repository: jika datanya Parquet, JSON, atau CSV, muat dengan packaged builder dan path file, sehingga tidak ada jalur di mana script milik pengunggah ikut dipertimbangkan. Blok di atas melakukan keduanya. Saya belum menjalankannya di stack Hugging Face maupun stack siapa pun, jadi uji dulu pada dataset yang Anda kendalikan.
Pasang grep itu di CI sebagai pemeriksaan yang gagal untuk perubahan apa pun yang menambah trust_remote_code=True. Reviewer yang sekilas membaca diff akan meloloskan satu keyword argument tambahan; build merah tidak akan.
Template injection pada konfigurasi: jalur kedua
Jalur kedua adalah template injection pada konfigurasi dataset. Disclosure menyebut kelasnya, bukan engine-nya, jadi saya tidak akan menebak engine mana yang terlibat. Mekanisme umumnya sama di mana-mana: field config dimaksudkan sebagai string, seseorang meneruskannya ke template renderer untuk mengisi nama atau path, dan bahasa template yang bisa menjangkau atribut dan fungsi menjadi jalan untuk menjalankan kode. Jinja adalah contoh Python yang umum, dan dokumentasinya berguna karena terus terang soal batas perbaikannya.
# Wrong: a string from an uploaded config becomes a program.
from jinja2 import Environment
def render_name(cfg: dict, row: dict) -> str:
return Environment().from_string(cfg["name_template"]).render(row=row)
# Better: Jinja's sandbox. Insecure access raises SecurityError, but the docs
# say plainly that "The sandbox alone is not a solution for perfect security."
from jinja2.exceptions import SecurityError
from jinja2.sandbox import ImmutableSandboxedEnvironment
SANDBOX = ImmutableSandboxedEnvironment()
def render_name_sandboxed(cfg: dict, row: dict) -> str:
try:
return SANDBOX.from_string(cfg["name_template"]).render(row=row)
except SecurityError:
raise ValueError("template rejected") # reject the dataset, do not retry
# Best: no template engine at all. Substitution over a fixed key list cannot
# reach an attribute, a filter or a function, because there is nothing to reach.
ALLOWED_KEYS = ("id", "split", "language")
def render_name_plain(cfg: dict, row: dict) -> str:
out = cfg["name_pattern"]
for key in ALLOWED_KEYS:
out = out.replace("@" + key + "@", str(row.get(key, "")))
return outDocs sandbox Jinja menyatakan sandbox bisa dipakai untuk merender template tak tepercaya, bahwa akses tak aman memunculkan SecurityError, dan, dalam napas yang sama, bahwa exception lain bisa muncul saat render sehingga pemanggil harus menangkapnya. Mereka juga menulis 'The sandbox alone is not a solution for perfect security.' ImmutableSandboxedEnvironment adalah varian lebih ketat yang memblokir perubahan pada list, set, dan dict bawaan. Kode di atas menunjukkan tiga tingkat: versi yang menimbulkan insiden, versi sandbox, dan versi tanpa template engine sama sekali.
Saya memilih yang ketiga. Daftar key substitusi yang tetap tidak bisa menjangkau atribut, filter, atau fungsi, jadi tidak ada sandbox untuk dijebol dan tidak ada yang harus terus di-patch. Biayanya nyata: Anda kehilangan conditional dan loop di config, dan pengguna yang menyukainya akan mengeluh. Jika tetap memakai template engine, perlakukan SecurityError sebagai alasan menolak seluruh dataset, jangan pernah mencoba ulang dengan environment yang lebih longgar.
Dari worker pod ke node: di sinilah blast radius ditentukan
Eksekusi kode di processing worker itu buruk tetapi terbatas. Yang mengubahnya jadi insiden adalah langkah berikutnya: akses level node, lalu credential cloud dan cluster, lalu pergerakan lateral. Kontrolnya semua native Kubernetes dan tidak ada yang eksotis. Pod Security Admission, stable sejak Kubernetes v1.25, menerapkan Pod Security Standards per namespace lewat label. Mode enforce menolak pod, sedangkan audit dan warn hanya mencatat atau memberi tahu client. Satu detail dari docs yang sering menjebak: enforce berlaku pada objek Pod, bukan Deployment atau Job, dan halamannya memperingatkan agar service account controller tidak dikecualikan karena itu mengecualikan siapa pun yang bisa membuat workload.
# Namespace that runs untrusted-input workers. Pod Security Admission, stable
# since Kubernetes v1.25. enforce rejects the Pod; warn only tells the client.
apiVersion: v1
kind: Namespace
metadata:
name: dataset-workers
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
---
# Your own rule on top, with ValidatingAdmissionPolicy (stable since v1.30).
# This one refuses any Pod that mounts a hostPath volume. My example, not the docs'.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: no-hostpath.example.com
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "!has(object.spec.volumes) || !object.spec.volumes.exists(v, has(v.hostPath))"
message: "hostPath volumes are not allowed in dataset-workers"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: no-hostpath-binding.example.com
spec:
policyName: no-hostpath.example.com
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: dataset-workersLevel Baseline dari Pod Security Standards melarang jembatan pod-ke-host yang paling penting di sini: ia mewajibkan hostNetwork, hostPID, dan hostIPC tidak diset atau false, mewajibkan securityContext.privileged tidak diset atau false, dan melarang volume hostPath sepenuhnya. ValidatingAdmissionPolicy, stable sejak v1.30, memungkinkan Anda menambah aturan sendiri dalam CEL, dengan validationActions Deny, Warn, atau Audit. Manifest di atas menerapkan Baseline pada namespace worker, mengaudit terhadap Restricted, dan menambah aturan hostPath yang merupakan contoh saya sendiri, bukan dari docs.
# If the nodes are EC2 instances: require IMDSv2 and keep the PUT response hop
# limit at 1 (the default), so a process inside a container cannot mint a
# metadata token. A request without a valid token then gets 401.
# Hugging Face did not say which cloud it runs on - this is one provider's lever.
aws ec2 modify-instance-metadata-options \
--instance-id i-0123456789abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1Identitas cloud milik node adalah separuh lainnya. Disclosure menyebut credential diambil tetapi tidak dari mana, jadi saya tidak bisa mengatakan metadata service terlibat. Jika node Anda instance EC2, AWS mendokumentasikan bahwa IMDSv2 berbasis session, bahwa request tanpa token valid mendapat 401 setelah token diwajibkan, dan bahwa hop limit response PUT secara default 1 pada level protokol IP. Mewajibkan token dan membiarkan hop limit di 1 adalah tuasnya, seperti pada blok kedua di atas. AWS juga mencatat Anda mungkin menaikkan hop limit demi kompatibilitas dengan layanan container, dan itulah setelan yang perlu diaudit.
Trade-off, dan apa yang tidak diperbaiki
Setiap kontrol di atas punya biaya, dan berpura-pura sebaliknya adalah cara checklist semacam ini ditinggalkan. Menerapkan Baseline di namespace worker akan menolak pod sah yang memakai hostPath untuk scratch volume atau device plugin, jadi Anda akan menghabiskan seminggu mencarinya. Menolak remote code berarti sebagian dataset lama tidak bisa di-ingest, dan jawabannya adalah meminta penulisnya mengonversi ke format standar, bukan membuat pengecualian. Menghapus templating dari config mematahkan fitur yang dipakai seseorang.
Ada juga batas pada apa yang bisa Anda simpulkan dari disclosure. Ia tidak menyebut apakah loader yang dieksploitasi ada di library open-source, fork internal, atau kode pemrosesan khusus. Tidak ada versi, tanggal, atau provider. Hugging Face juga menyatakan penilaian atas data partner dan pelanggan masih berjalan. Defender yang menyalin daftar kontrol dari disclosure sesingkat ini sedang menebak kontrol mana yang akan berpengaruh, dan sebaiknya mengatakannya di design review.
Jangan membaca 'tidak ada bukti perubahan pada model, dataset, atau Spaces publik' sebagai 'tidak ada yang perlu dilakukan'. Itu pernyataan tentang apa yang diverifikasi Hugging Face di sisinya. Token Anda sendiri, dataset yang Anda cache, dan pipeline apa pun yang menarik dari platform selama periode insiden tetap menjadi tanggung jawab Anda untuk ditinjau, dan saran disclosure sendiri adalah merotasi access token dan memeriksa aktivitas akun terbaru.
Satu catatan lagi soal forensik. Hugging Face mengatakan analisisnya awalnya terhalang guardrail pada model komersial hosted, sehingga mereka menjalankan analisis dengan model open-weight zai-org/GLM-5.2 di infrastruktur sendiri, menjaga data dan credential penyerang tetap di lingkungan mereka. Mereka menyarankan model self-hosted yang mumpuni sudah diperiksa sebelum insiden. Itu keputusan perencanaan dengan biaya, hardware, dan reviu sendiri, bukan sesuatu yang diimprovisasi saat Anda membutuhkannya.
Apa yang diubah Hugging Face, dan padanannya untuk Anda
Daftar remediation di disclosure adalah template yang lebih baik daripada kebanyakan post-mortem vendor karena konkret. Tabel berikut memetakan tiap butir ke apa yang bisa dilakukan tim yang menjalankan ingestion dataset atau model sendiri. Kolom kanan adalah terjemahan saya, bukan dari Hugging Face.
| Hugging Face melakukan | Padanannya untuk Anda |
|---|---|
| Menutup jalur eksekusi kode dataset yang dipakai untuk akses awal | Hapus trust_remote_code dan templating config dari setiap jalur ingestion |
| Membersihkan foothold di cluster terdampak dan membangun ulang node yang terkompromi | Bangun ulang node dari image, jangan dibersihkan; anggap node yang pernah menjalankan kode tak tepercaya sudah hangus |
| Mencabut dan merotasi credential terdampak, lalu memulai rotasi pencegahan yang lebih luas | Rotasi berurutan menurut blast radius, mulai dari apa pun yang bisa dibaca worker atau node |
| Menambah guardrail dan admission control cluster yang lebih ketat | Pod Security Admission plus ValidatingAdmissionPolicy pada namespace worker |
| Memperbaiki deteksi dan alerting untuk sinyal severity tinggi | Beri alert saat worker pod memunculkan shell, mengakses alamat metadata, atau memanggil cluster lain |
Jika Anda memakai Hugging Face, instruksi disclosure sendiri adalah merotasi access token sebagai tindakan pencegahan, meninjau aktivitas terbaru di akun, dan melaporkan kekhawatiran ke security@huggingface.co. Jika Anda menjalankan ingestion sendiri, rotasi dengan urutan ini, karena urutan itulah yang membatasi kerusakan bila Anda terpotong di tengah jalan.
- Credential yang bisa dibaca worker langsung: environment variable, secret yang di-mount, dan token service account.
- Credential yang bisa dibaca node: cloud role, instance profile, dan secret registry atau image pull.
- Credential cluster yang bisa dipakai identitas worker atau node terhadap cluster lain, termasuk kubeconfig dan bearer token.
- Access token pribadi dan CI untuk Hub atau sumber dataset mana pun yang ditarik pipeline, diterbitkan dengan scope tersempit yang masih berfungsi.
- Sisanya, sebagai pencegahan, setelah empat langkah pertama selesai dan nilai lama terkonfirmasi mati, bukan sekadar diganti.
Yang dikerjakan hari Senin
Semua ini tidak butuh proyek. Cukup satu sore, daftar tempat pipeline Anda menelan data yang bukan Anda tulis, dan kesiapan untuk mengetahui bahwa ternyata ada.
- Jalankan grep trust_remote_code di setiap repository dan notebook yang memberi makan worker bersama, dan catat versi datasets terpasang di setiap worker image.
- Daftar setiap field config yang sampai ke template renderer. Untuk masing-masing, putuskan: hapus engine, sandbox, atau pastikan field itu tak pernah diisi pengguna.
- Beri label namespace ingestion dengan Pod Security Admission enforce di baseline serta warn dan audit di restricted, lalu baca isi warning selama seminggu sebelum mengetatkan.
- Periksa apa yang bisa dijangkau pod dari dalam: endpoint metadata node, API server cluster lain, dan credential yang di-mount. Jika di EC2, wajibkan IMDSv2 dan pastikan hop limit.
- Tulis urutan rotasi Anda dan siapa yang bisa menjalankannya jam 03.00. Checklist yang belum pernah dilatih adalah dokumen, bukan kontrol.
Lalu baca ulang disclosure Hugging Face dengan pipeline Anda sendiri di kepala, dan tandai tiap kalimat sebagai 'kami terpapar', 'kami tidak', atau 'kami tidak tahu'. Tumpukan ketiga adalah hasil sebenarnya.
Pelajaran yang saya ambil adalah aturan, bukan daftar: apa pun yang diunggah orang asing lalu dibuka oleh sebuah program adalah program, apa pun ekstensi filenya. Loader, template config, dan bobot model semuanya termasuk. Tentukan sendiri formatnya, jangan beri interpreter apa pun untuk dievaluasi, anggap worker pada akhirnya akan menjalankan sesuatu yang bermusuhan, dan pastikan apa yang bisa dijangkaunya dari sana kecil, berumur pendek, dan bisa dirotasi.
Sumber dan bacaan lanjutan
- Hugging Face: Security incident disclosure, July 2026
- Hugging Face datasets 4.0.0 release notes (breaking changes)
- Hugging Face datasets docs: create a dataset loading script
- Dokumentasi Jinja: the sandbox
- Kubernetes docs: Pod Security Admission
- Kubernetes docs: Pod Security Standards
- Kubernetes docs: ValidatingAdmissionPolicy
- AWS docs: use the Instance Metadata Service to access instance metadata
- Hugging Face Hub docs: pickle scanning

