Keamanan API Laravel: 9 Celah yang Sering Terlewat dan Solusinya

API sering dibuat terburu-buru karena aplikasi mobile harus segera rilis. Login dengan token sudah jalan, data tampil di aplikasi, dan semua terlihat beres. Masalahnya baru ketahua...

Keamanan API Laravel: 9 Celah yang Sering Terlewat dan Solusinya

API sering dibuat terburu-buru karena aplikasi mobile harus segera rilis. Login dengan token sudah jalan, data tampil di aplikasi, dan semua terlihat beres. Masalahnya baru ketahuan berbulan-bulan kemudian: seseorang mengganti angka /api/orders/1021 menjadi 1022 dan bisa melihat pesanan pelanggan lain lengkap dengan alamat dan nomor teleponnya. Tidak ada yang "dibobol"; API-nya memang tidak pernah memeriksa kepemilikan data.

Daftar OWASP API Security Top 10 menempatkan masalah semacam ini di posisi pertama. Artikel ini membahas celah API yang paling sering ditemukan di aplikasi bisnis dan cara menutupnya di Laravel, lengkap dengan kode. Jika Anda belum punya autentikasi token, mulai dulu dari tutorial membuat REST API dengan Laravel Sanctum.

1. Periksa Kepemilikan Data di Setiap Endpoint

Celah ini disebut Broken Object Level Authorization (BOLA). Middleware auth:sanctum hanya memastikan request membawa token yang sah, bukan bahwa pemilik token berhak atas data yang diminta.

// RENTAN — siapa pun yang login bisa membaca pesanan siapa saja
public function show(int $id)
{
    return new OrderResource(Order::findOrFail($id));
}

// AMAN — query dimulai dari relasi milik user yang login
public function show(Request $request, int $id)
{
    $order = $request->user()->orders()->findOrFail($id); // 404 bila bukan miliknya
    return new OrderResource($order);
}

Pola "mulai dari relasi user" lebih sulit terlewat dibanding menambahkan where('user_id', ...) manual di setiap query. Untuk aturan yang lebih rumit, misalnya admin cabang boleh melihat pesanan cabangnya, pakai Policy dan panggil Gate::authorize('view', $order).

Ingat juga bahwa ID berurutan memudahkan penyerang menebak data. Memakai UUID membantu, tetapi bukan pengganti pengecekan kepemilikan.

2. Kirim Hanya Field yang Dibutuhkan

Mengembalikan model mentah seperti return $user; berisiko membocorkan kolom yang tidak seharusnya terlihat, misalnya remember_token, catatan internal, atau harga modal. Aplikasi mobile mungkin tidak menampilkannya, tetapi siapa pun bisa melihat respons JSON lewat proxy atau DevTools. Gunakan API Resource dan tulis field secara eksplisit:

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'    => $this->id,
            'name'  => $this->name,
            'price' => $this->price,
            'stock' => $this->stock,
            // harga_modal dan supplier_id sengaja tidak dikirim
            'cost'  => $this->when($request->user()?->is_admin, $this->harga_modal),
        ];
    }
}

Tambahkan juga protected $hidden = ['password', 'remember_token']; di model sebagai lapisan pengaman bila suatu saat ada developer yang lupa memakai Resource.

3. Validasi Input dan Cegah Mass Assignment

Request API sama tidak tepercayanya dengan form web. Jangan pernah meneruskan $request->all() ke create() atau update(); penyerang bisa menambahkan field seperti "status": "lunas" atau "user_id": 1.

public function update(Request $request, int $id)
{
    $order = $request->user()->orders()->findOrFail($id);

    $data = $request->validate([
        'catatan'      => ['nullable', 'string', 'max:500'],
        'alamat_kirim' => ['required', 'string', 'max:255'],
    ]);

    $order->update($data); // status, total, user_id tidak bisa diubah lewat endpoint ini
    return new OrderResource($order);
}

4. Batasi Jumlah Data per Request

Parameter per_page tanpa batas memungkinkan satu request menarik puluhan ribu baris sekaligus. Selain membebani server, ini memudahkan pencurian data massal.

$perPage = min(max((int) $request->input('per_page', 20), 1), 100);
return ProductResource::collection(Product::paginate($perPage));

5. Rate Limiting per Pengguna dan per Endpoint

Di Laravel 11, throttling API diaktifkan di bootstrap/app.php, sedangkan batasnya didefinisikan di AppServiceProvider:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->throttleApi(); // memakai limiter bernama "api"
})

// app/Providers/AppServiceProvider.php
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

// Endpoint mahal atau sensitif diberi batas sendiri
RateLimiter::for('otp', fn (Request $request) => Limit::perMinute(3)->by($request->ip()));
Route::post('/otp/send', [OtpController::class, 'send'])->middleware('throttle:otp');

Endpoint yang mengirim SMS, WhatsApp, atau email wajib diberi batas ketat, karena setiap request bisa langsung menjadi biaya.

6. Atur CORS Seperlunya

CORS menentukan domain mana yang boleh memanggil API Anda dari browser. Nilai 'allowed_origins' => ['*'] memang membuat error CORS hilang, tetapi juga mengizinkan situs mana pun memanggil API dari browser pengunjungnya. Di Laravel 11, terbitkan file konfigurasinya lalu isi domain frontend Anda saja:

php artisan config:publish cors
// config/cors.php
'paths'           => ['api/*'],
'allowed_origins' => ['https://app.tokoanda.com'],
'supports_credentials' => false,

CORS hanya berlaku untuk browser. Aplikasi mobile dan server tidak terpengaruh, jadi CORS bukan pengganti autentikasi.

7. Verifikasi Signature Webhook

Endpoint webhook dari payment gateway atau layanan pengiriman biasanya tidak memakai login. Tanpa verifikasi, siapa pun bisa mengirim request palsu "pembayaran berhasil" dan pesanan langsung berstatus lunas. Hampir semua penyedia mengirim signature HMAC; verifikasi sebelum memproses apa pun:

public function handle(Request $request)
{
    $expected = hash_hmac('sha256', $request->getContent(), config('services.partner.webhook_secret'));

    if (! hash_equals($expected, (string) $request->header('X-Signature'))) {
        abort(401, 'Signature tidak valid');
    }

    // Proses idempoten: abaikan event yang sudah pernah diproses
    $event = WebhookEvent::firstOrCreate(['event_id' => $request->input('id')]);
    if (! $event->wasRecentlyCreated) {
        return response()->noContent();
    }

    // ... update status pesanan
}

Gunakan hash_equals(), bukan ==, agar perbandingan tidak bocor lewat perbedaan waktu eksekusi. Nama header dan algoritma signature berbeda tiap penyedia, jadi cocokkan dengan dokumentasinya. Contoh penerapan dengan Midtrans ada di artikel integrasi payment gateway Midtrans di Laravel.

8. Kelola Token dengan Disiplin

  • Beri masa berlaku. Token yang tidak pernah kedaluwarsa tetap bisa dipakai bertahun-tahun setelah ponsel karyawan hilang.
  • Satu token per perangkat. Pengguna bisa mencabut akses satu perangkat tanpa logout dari semuanya.
  • Batasi kemampuan token. Token untuk integrasi laporan cukup bisa membaca, tidak perlu bisa menghapus.
  • Jangan taruh token di URL. Query string tercatat di log server dan riwayat browser. Kirim lewat header Authorization: Bearer ....

9. Pesan Error yang Konsisten dan Tidak Membocorkan Detail

Pastikan APP_DEBUG=false di produksi agar error 500 tidak mengirim stack trace dalam JSON. Gunakan juga status code yang tepat: 401 untuk belum login, 403 untuk tidak berhak, 404 untuk data yang tidak ada atau bukan milik pengguna, 422 untuk validasi, dan 429 untuk terlalu banyak request. Klien jadi mudah menangani error, dan penyerang tidak mendapat petunjuk tambahan.

Checklist Keamanan API

AreaPertanyaanCara menguji
Kepemilikan dataApakah setiap endpoint {id} memeriksa pemilik?Pakai token user A untuk membuka ID milik user B: harus 404/403
Field responsApakah ada kolom internal yang ikut terkirim?Periksa JSON mentah di Postman
Mass assignmentBisakah field status/total diubah lewat request?Tambahkan field "status":"lunas" pada request update
PaginasiApakah per_page dibatasi?Kirim per_page=100000
Rate limitApakah endpoint login, OTP, dan pencarian dibatasi?Kirim request beruntun: harus muncul 429
CORSApakah origin dibatasi?Cek header Access-Control-Allow-Origin
WebhookApakah signature diverifikasi dan event idempoten?Kirim webhook tanpa signature: harus 401
TokenApakah token punya masa berlaku dan bisa dicabut?Cek kolom expires_at di tabel token

Sebagian besar pengujian di atas bisa dijalankan dengan Postman dalam satu sore. Lakukan sebelum aplikasi mobile dirilis, dan ulangi setiap ada endpoint baru. Lapisan database juga perlu diperiksa; baca panduan mencegah SQL injection dan checklist keamanan Laravel untuk melengkapinya.

Ingin langsung mempraktikkannya?

Buat akun gratis untuk mengunduh contoh aplikasi Laravel, menyimpan tutorial favorit, dan mencoba kursus. Gratis, bisa daftar dengan akun Google.

Daftar Gratis Lihat katalog aplikasi
Yudhi
Ditulis oleh
Yudhi
Founder & Lead Developer, GudangCode

Yudhi adalah founder GudangCode dan developer Laravel dengan pengalaman membangun puluhan sistem informasi bisnis siap pakai — mulai dari POS, HRIS, hingga aplikasi manajemen. Ia menulis panduan dan artikel di GudangCode untuk membantu developer Indonesia menjalankan, memahami, dan men-deploy source code Laravel dengan benar.

Laravel PHP MySQL Sistem Informasi Bisnis
Lihat semua artikel Yudhi
Keamanan Api Keamanan & Best Practice
Bacaan Paling Dicari
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →