Pendahuluan
“API saya sudah pakai Sanctum, berarti sudah aman kan?”
Autentikasi adalah lapisan pertama keamanan — bukan satu-satunya. Bayangkan pintu depan dengan kunci terbaik, tapi jendela belakang dibiarkan terbuka.
Serangan nyata tidak selalu menyerang autentikasi langsung. Mereka mencari celah lain:
- Brute force login ribuan kali per detik
- SQL Injection melalui parameter URL
- Mass assignment yang secara tidak sengaja mengubah field admin
- Input XSS yang mencuri session cookie user lain
Artikel ini adalah panduan keamanan praktis untuk aplikasi Laravel production — lengkap dengan kode yang langsung bisa diimplementasikan.
OWASP Top 10 yang Relevan untuk Laravel Developer
OWASP Top 10 adalah daftar 10 kerentanan web paling kritis. Yang paling relevan untuk Laravel:
| Ancaman | Contoh | Status Laravel Default |
|---|---|---|
| Broken Access Control | User A akses data User B | Perlu Policy |
| Injection (SQL, Command) | ?id=1 OR 1=1 | Terlindungi jika pakai Eloquent |
| Identification Failures | Brute force password | Perlu Rate Limiting |
| XSS | <script> di input user | Terlindungi di Blade {{ }} |
| Security Misconfiguration | APP_DEBUG=true di production | Manual check |
| Mass Assignment | User::create($request->all()) | Perlu $fillable |
Langkah 1: Rate Limiting — Cegah Brute Force
Rate limiting membatasi berapa banyak request yang bisa dikirim dalam periode tertentu:
<?php
// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
// Rate limit untuk API umum: 60 request per menit per user/IP
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->user()?->id ?: $request->ip());
});
// Rate limit ketat untuk login: 5 percobaan per menit per IP
RateLimiter::for('login', function (Request $request) {
return [
Limit::perMinute(5)->by($request->ip()),
// User yang sudah login juga dibatasi berdasarkan email
Limit::perMinute(10)->by($request->input('email')),
];
});
// Rate limit untuk endpoint sensitif (reset password, OTP)
RateLimiter::for('sensitive', function (Request $request) {
return Limit::perHour(3)->by($request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'Terlalu banyak percobaan. Coba lagi dalam 1 jam.',
], 429, $headers);
});
});
}
Terapkan di routes:
// routes/api.php
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:login');
Route::post('/password/email', [PasswordController::class, 'sendReset'])
->middleware('throttle:sensitive');
// Semua route API sudah otomatis kena throttle:api
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
// ...
});
Langkah 2: Mencegah SQL Injection
Eloquent ORM secara default menggunakan prepared statements yang melindungi dari SQL injection dasar. Tapi ada case yang perlu perhatian ekstra:
// ✅ AMAN — Eloquent menggunakan parameter binding
$tasks = Task::where('user_id', $userId)->get();
// SQL: SELECT * FROM tasks WHERE user_id = ? [1]
// ✅ AMAN — whereIn, whereBetween, dll.
$tasks = Task::whereIn('status', ['pending', 'completed'])->get();
// ⚠️ BERBAHAYA — jangan masukkan input user langsung ke DB::raw
$tasks = Task::whereRaw("title LIKE '%{$request->search}%'")->get();
// Jika search = "'; DROP TABLE tasks; --" → catastrophe!
// ✅ AMAN — gunakan binding parameter dengan DB::raw
$tasks = Task::whereRaw("title LIKE ?", ["%{$request->search}%"])->get();
// ✅ AMAN — cara lebih bersih dengan Eloquent
$tasks = Task::where('title', 'like', '%' . $request->search . '%')->get();
Validasi Input Sebelum Query
// Selalu validasi dan sanitize sebelum digunakan
$request->validate([
'search' => 'nullable|string|max:100',
'order_by' => 'nullable|in:created_at,title,priority', // Whitelist!
'per_page' => 'nullable|integer|min:1|max:100',
]);
// Jangan gunakan $request->order_by langsung di orderBy()!
$allowedColumns = ['created_at', 'title', 'priority'];
$orderBy = in_array($request->order_by, $allowedColumns)
? $request->order_by
: 'created_at';
$tasks = Task::orderBy($orderBy)->paginate($request->per_page ?? 15);
Langkah 3: Mencegah XSS
XSS (Cross-Site Scripting) terjadi ketika input user ditampilkan di halaman tanpa di-escape, memungkinkan eksekusi JavaScript berbahaya.
{{-- ✅ AMAN — Blade {{ }} otomatis escape HTML entities --}}
<p>{{ $user->name }}</p>
{{-- Output: <script>alert('xss')</script> --}}
{{-- ❌ BERBAHAYA — {!! !!} tidak escape apapun --}}
<p>{!! $user->bio !!}</p>
{{-- Output: <script>alert('xss')</script> — BAHAYA! --}}
{{-- Gunakan {!! !!} HANYA jika kamu yang generate HTML-nya --}}
{!! $trustedMarkdown !!}
Jika membutuhkan HTML user (editor WYSIWYG), gunakan sanitizer:
composer require ezyang/htmlpurifier
use HTMLPurifier;
use HTMLPurifier_Config;
// Bersihkan HTML sebelum simpan ke database
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$cleanHtml = $purifier->purify($request->content);
Post::create(['content' => $cleanHtml]);
Langkah 4: Mass Assignment Protection
Mass assignment vulnerability terjadi ketika $request->all() digunakan langsung tanpa filter:
// ❌ SANGAT BERBAHAYA
User::create($request->all());
// Jika request berisi: {"name": "Budi", "email": "[email protected]", "is_admin": true}
// User baru akan menjadi admin!
// ✅ AMAN — hanya izinkan field yang terdaftar di $fillable
User::create($request->only(['name', 'email', 'password']));
// ✅ AMAN — gunakan validated() dari Form Request
User::create($request->validated());
// app/Models/User.php
class User extends Model
{
// Whitelist: hanya field ini yang bisa di-mass-assign
protected $fillable = [
'name', 'email', 'password',
];
// Field yang tidak pernah boleh dikembalikan di JSON (hidden dari response)
protected $hidden = [
'password', 'remember_token',
];
}
Langkah 5: Security Headers
Tambahkan HTTP headers yang meningkatkan keamanan browser:
php artisan make:middleware SecurityHeaders
<?php
// app/Http/Middleware/SecurityHeaders.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Strict-Transport-Security (HSTS) — hanya untuk HTTPS production
if (app()->isProduction()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
return $response;
}
}
// bootstrap/app.php (Laravel 11)
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
})
Langkah 6: CORS Production-Ready
Konfigurasi CORS yang aman untuk production (berbeda dari konfigurasi dasar di artikel CORS):
// config/cors.php — Production config
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
// JANGAN gunakan '*' di production!
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', '')),
// Contoh .env: CORS_ALLOWED_ORIGINS=https://app.domain.com,https://admin.domain.com
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'exposed_headers' => ['X-RateLimit-Limit', 'X-RateLimit-Remaining'],
'max_age' => 86400,
// Harus true jika menggunakan Sanctum SPA auth
'supports_credentials' => true,
];
Langkah 7: Logging Aktivitas Mencurigakan
<?php
// app/Http/Middleware/LogSuspiciousActivity.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class LogSuspiciousActivity
{
// Pattern yang mencurigakan
private array $sqlPatterns = [
'/\bunion\b.*\bselect\b/i',
'/\bselect\b.*\bfrom\b/i',
'/drop\s+table/i',
'/1=1|1\'=\'1/i',
];
private array $xssPatterns = [
'/<script/i',
'/javascript:/i',
'/onerror=/i',
'/onload=/i',
];
public function handle(Request $request, Closure $next)
{
$input = $request->all();
$inputStr = json_encode($input);
foreach ([...$this->sqlPatterns, ...$this->xssPatterns] as $pattern) {
if (preg_match($pattern, $inputStr)) {
Log::warning('Suspicious input detected', [
'ip' => $request->ip(),
'url' => $request->fullUrl(),
'method' => $request->method(),
'user_id' => auth()->id(),
'input' => $inputStr,
'pattern' => $pattern,
]);
break;
}
}
return $next($request);
}
}
Security Checklist untuk Production
| Item | Perintah/Cara Cek | Status |
|---|---|---|
APP_DEBUG=false | grep APP_DEBUG .env | ✅ |
APP_ENV=production | grep APP_ENV .env | ✅ |
| HTTPS aktif | Cek redirect HTTP→HTTPS | ✅ |
| Rate limiting aktif | Test dengan curl | ✅ |
$fillable di semua model | Review model files | ✅ |
Tidak ada {!! !!} dengan input user | grep -r "{!!" src | ✅ |
| Env secrets tidak di-commit | .gitignore punya .env | ✅ |
php artisan key:generate sudah dijalankan | APP_KEY tidak kosong | ✅ |
Permission file storage/ benar | ls -la storage/ | ✅ |
X-Frame-Options header aktif | curl -I domain.com | ✅ |
Troubleshooting: Error yang Sering Muncul
Rate limiter tidak bekerja / request selalu lolos
Penyebab: Middleware throttle belum diterapkan, atau RateLimiter::for() tidak dipanggil.
Solusi:
// Pastikan rate limiter terdaftar di AppServiceProvider
// Dan middleware throttle:nama-limiter ada di route
// Untuk test rate limiter:
for i in {1..10}; do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/api/login \
-X POST -H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"wrong"}'
done
# Response ke-6 harus 429
Legitimate user terkena rate limit
Penyebab: Rate limit terlalu ketat, atau key berbasis IP saja (masalah shared IP di kantor/kampus).
Solusi:
// Gunakan kombinasi user ID + IP agar lebih akurat
RateLimiter::for('api', function (Request $request) {
if ($user = $request->user()) {
return Limit::perMinute(120)->by('user:' . $user->id); // User login: lebih longgar
}
return Limit::perMinute(30)->by('ip:' . $request->ip()); // Guest: lebih ketat
});
CORS dan Security Headers konflik
Penyebab: Header CORS ditambahkan dua kali — oleh Laravel dan Nginx.
Solusi:
# Di Nginx, jangan tambahkan CORS header jika Laravel sudah menangani
# HAPUS ini jika ada:
# add_header 'Access-Control-Allow-Origin' '*';
# Biarkan Laravel mengelola CORS sepenuhnya melalui config/cors.php
Pertanyaan yang Sering Diajukan
Apakah Sanctum sudah cukup untuk keamanan API?
Sanctum menangani autentikasi (memverifikasi siapa yang request). Tapi keamanan membutuhkan lapisan lebih: autorisasi (apa yang boleh dilakukan), validasi input (data berbahaya ditolak), dan rate limiting (penyalahgunaan dibatasi). Sanctum adalah fondasi, bukan solusi lengkap.
Bagaimana cara melakukan security audit pada aplikasi Laravel?
Mulai dengan tools otomatis: php artisan security:check (dari package enlightn/enlightn), scan dependency dengan composer audit, dan gunakan Laravel Telescope untuk monitoring request mencurigakan. Untuk audit mendalam, gunakan OWASP ZAP atau hire penetration tester.
Apakah perlu menggunakan WAF (Web Application Firewall)?
Untuk production dengan traffic tinggi atau menyimpan data sensitif, ya. Cloudflare WAF adalah pilihan populer di Indonesia karena mudah di-setup dan gratis untuk tier dasar. WAF menambah lapisan perlindungan sebelum request bahkan sampai ke Laravel.
Bagaimana cara handle data sensitif di log?
Jangan pernah log password, token, atau nomor kartu. Gunakan $hidden di model dan gunakan custom logging di Form Request:
// Otomatis hapus field sensitif dari log
protected function prepareForValidation(): void
{
$this->merge([
'password' => '***HIDDEN***',
]);
}
Kesimpulan
Keamanan bukan fitur yang ditambahkan di akhir — ini adalah mindset yang harus ada sejak awal development.
Ringkasan lapisan keamanan yang sudah kita bangun:
- Rate Limiting — Batasi akses berulang
- SQL Injection Prevention — Selalu gunakan parameter binding
- XSS Prevention — Escape output, sanitize HTML input
- Mass Assignment Protection — Whitelist field dengan
$fillable - Security Headers — Instruksikan browser untuk lebih ketat
- Activity Logging — Pantau aktivitas mencurigakan
Kombinasikan semua ini dengan autentikasi Sanctum yang sudah dipelajari sebelumnya, dan aplikasi Laravel-mu siap menghadapi ancaman nyata!
Ingat: keamanan adalah proses berkelanjutan, bukan checklist satu kali. Pantau terus, update dependency, dan selalu waspada. 🔐