SQL injection menyuntikkan perintah ke query; mass assignment jauh lebih halus — penyerang menyuntikkan kolom ke data yang aplikasi simpan dengan sukarela. Kerentanan ini tidak butuh karakter aneh atau tanda kutip licik: cukup satu key ekstra bernama is_admin yang diselipkan ke form yang tampak polos, dan controller yang mempercayai $request->all(). Chapter Mass Assignment pada section Eloquent ORM sudah membahas mekanisme fillable, guarded, dan forceFill(); di sini kita membedah sisinya yang gelap — eksploitasi nyata level HTTP, tangga perlindungannya, dan pola defense in depth yang menempatkan filter di dua lapis.

Skenario Serangan: Field Tersembunyi #

Bayangkan form edit profil dengan dua input biasa: name dan email. Penyerang yang sudah login membuka DevTools, menyuntikkan satu baris sebelum submit:

html
<input type="hidden" name="is_admin" value="1">

Di sisi controller, kode yang tampak wajar menunggu:

php
Route::put('/profil/{id}', function (Illuminate\Http\Request $request, int $id) {
    $pengguna = PenggunaTerbuka::findOrFail($id);

    // VERSI RENTAN: semua input dipercaya
    $pengguna->update($request->all());

    return response()->json([
        'id' => $pengguna->id,
        'name' => $pengguna->name,
        'is_admin' => (bool) $pengguna->fresh()->is_admin,
    ]);
})->name('profil.update');

Model PenggunaTerbuka adalah model ceroboh yang tidak menjaga kolom apa pun — protected $guarded = []; tanpa $fillable. Submit form dengan field tersembunyi itu menghasilkan:

text
HTTP 200 — {"id":4,"name":"Siti","is_admin":true}

Serangan berhasil: pengguna biasa kini admin. Perhatikan bahwa request ini melewati CSRF dengan mulus — token valid, session valid, karena pelakunya memang pengguna yang sah. Mass assignment mengeksploitasi kepercayaan aplikasi pada isi request, bukan asalnya.

Tangga Perlindungan Eloquent #

Perilaku model terhadap key asing berbeda-beda menurut konfigurasinya — kita uji ketiganya dengan input penyerang yang sama (name, email, is_admin):

php
use Illuminate\Database\Eloquent\Model;

class Pengguna extends Model
{
    protected $table = 'users';
    protected $fillable = ['name', 'email', 'password'];
}

class PenggunaTerbuka extends Model
{
    protected $table = 'users';
    protected $guarded = [];
}

$inputPenyerang = ['name' => 'Siti', 'email' => '[email protected]', 'is_admin' => 1];

// skenario 1: model ber-whitelist
Pengguna::findOrFail($siti->id)->update($inputPenyerang);

// skenario 2: model tanpa proteksi
PenggunaTerbuka::findOrFail($siti->id)->update($inputPenyerang);

// skenario 3: forceFill pada model ber-whitelist
Pengguna::findOrFail($siti->id)->forceFill(['is_admin' => 1])->save();

Output yang Dihasilkan:

text
=== 1. Whitelist fillable yang benar ===
is_admin di database: false (is_admin dibuang diam-diam)

=== 2. Model tanpa proteksi ($guarded = []) ===
is_admin di database: true (SERANGAN BERHASIL!)

=== 3. forceFill mem-bypass fillable ===
is_admin di database: true (bypass whitelist!)

Tiga keadaan ini memetakan tangga perlindungan secara utuh:

Konfigurasi modelNasib key is_admin
$fillable berisi kolom formDibuang diam-diam — key tidak ada di whitelist
$guarded = [] (atau $fillable terlalu luas)Tersimpan — serangan berhasil
forceFill() / User::unguard()Tersimpan — melewati semua pemeriksaan

Kasus keempat yang belum terlihat di atas: model tanpa $fillable apa pun dan $guarded yang tetap default ['*'] akan melempar MassAssignmentException — penolakan paling eksplisit yang justru sehat, karena bug ketik pun langsung ketahuan. Perilaku ini sudah kita buktikan di chapter Mass Assignment . Di Laravel 13, daftar whitelist juga dapat dideklarasikan lewat atribut #[Fillable(['name', 'email', 'password'])] di atas class model — semantiknya sama persis dengan property.

Dua strategi whitelist pun tersedia untuk tim. Pendekatan $fillable yang kita pakai di atas menyatakan “hanya kolom ini yang terbuka” per model — cocok untuk model aktif yang sering menerima form. Sebaliknya, strategi lock-down dengan $guarded = ['*'] menutup semuanya secara bawaan, dan hanya model yang memang mengekspos form yang diberi $fillable — pilihan yang cocok untuk aplikasi dengan banyak model internal yang tidak pernah disentuh input user.

Surface API: Klien JSON Bisa Mengirim Key Ekstra #

Form HTML bukan satu-satunya pintu. Klien API mengirim body JSON, dan $request->all() menyatukan semuanya tanpa membedakan asal. Route versi rentan yang sama diuji dengan body JSON murni:

text
=== Klien JSON API mengirim key ekstra is_admin ===
HTTP 200 — {"id":4,"is_admin":true}

Tidak ada field tersembunyi yang perlu disuntikkan lewat DevTools — cukup menambah satu key di JSON payload. Ini membuat PUT/PATCH endpoint menjadi surface mass assignment yang paling mudah diserang, justru karena terlihat seperti “sekadar data”.

Pertahanan Berlapis: only() Dulu, fillable Terakhir #

Perbaikannya menutup dua lapis sekaligus — controller hanya mengambil kolom yang memang bagian form, dan model tetap memegang whitelist:

php
Route::put('/profil/{id}', function (Illuminate\Http\Request $request, int $id) {
    $pengguna = Pengguna::findOrFail($id);

    // VERSI AMAN: hanya kolom form yang diambil
    $pengguna->update($request->only(['name', 'email']));

    return response()->json([
        'id' => $pengguna->id,
        'name' => $pengguna->name,
        'is_admin' => (bool) $pengguna->fresh()->is_admin,
    ]);
})->name('profil.update');

Output yang Dihasilkan (serangan form dan JSON yang sama):

text
HTTP 200 — {"id":4,"name":"Siti","is_admin":false}
HTTP 200 — {"id":4,"is_admin":false}

$request->only() memfilter input lebih awal sehingga key asing tidak pernah menyentuh model; $fillable menjadi benteng terakhir bila lapis pertama lupa diterapkan. Dua lapis ini tidak saling menggantikan — saat developer baru menambahkan kolom ke only() tanpa sengaja memasukkan is_admin, whitelist model masih menahannya. Perilaku lengkap mass assignment di Eloquent terdokumentasi di dokumentasi Eloquent .

Mass assignment menjaga kolom. Pertanyaan berikutnya: bagaimana kita menjaga rahasia yang menentukan siapa pengguna itu — passwordnya? Kita bahas hashing, kebijakan kompleksitas, dan rate limit di chapter Password Security .