31 Mar 2026 • Admin KhalimZone
Security Best Practices di CI4
Security Best Practices di CI4 — Proteksi Aplikasi Dari Serangan
Security adalah bukan opsional — ini mandatory. Gue udah liat banyak aplikasi CI4 yang dibikin tanpa mikir security, hasilnya vulnerable dari berbagai jenis attack. SQL injection, XSS, CSRF, password cracking — semuanya bisa terjadi.
Di artikel ini, gue bakal kasih praktik security yang paling penting dan sering terlewat. Ini bukan teori, ini best practices dari real-world production apps.
SQL injection adalah vulnerability paling berbahaya. Jangan pernah concatenate user input langsung ke SQL query.
// ❌ SANGAT DANGEROUS — SQL Injection!
$email = $this->request->getVar('email');
$user = $this->db->query("SELECT * FROM users WHERE email = '$email'");
// ✅ BAGUS — gunakan query builder
$email = $this->request->getVar('email');
$user = $this->db->table('users')
->where('email', $email)
->get()
->getRow();
Query builder otomatis escape dan sanitize input. Atau kalau pakai raw query, selalu gunakan placeholders:
// ✅ Raw query dengan binding (secure)
$email = $this->request->getVar('email');
$user = $this->db->query(
"SELECT * FROM users WHERE email = ?",
[$email]
);
CI4 query builder sudah protect dari SQL injection. Tapi selalu validate & sanitize input di application level juga.
XSS terjadi ketika attacker inject malicious script yang dijalankan di browser user. Selalu escape output!
// ❌ DANGEROUS — XSS vulnerability
<h1><?= $userInput ?></h1>
// ✅ BAGUS — escape dengan esc()
<h1><?= esc($userInput) ?></h1>
// Atau spesifik context-nya:
<h1><?= esc($userInput, 'html') ?></h1>
<img src="<?= esc($url, 'attr') ?>">
<script>var name = "<?= esc($name, 'js') ?>";</script>
Di template, gunakan double curly braces:
<!-- ❌ DANGEROUS -->
<h1>{!! $userInput !!}</h1>
<!-- ✅ BAGUS -- auto-escaped -->
<h1>{{ $userInput }}</h1>
Gunakan {! !} hanya untuk trusted content. Jangan untuk user input!
CSRF memaksa user melakukan action tanpa mereka tahu. CI4 sudah built-in protection, cuma perlu enable.
<!-- ❌ DANGEROUS -- tanpa token -->
<form method="POST" action="/update-profile">
<input type="text" name="name">
<button type="submit">Update</button>
</form>
<!-- ✅ BAGUS -- dengan token -->
<form method="POST" action="/update-profile">
<?= csrf_field() ?>
<input type="text" name="name">
<button type="submit">Update</button>
</form>
Untuk AJAX request, add token ke header:
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
body: JSON.stringify({ name: 'John' })
});
CSRF protection auto-check token. Invalid token = 403 error.
Jangan simpan password plain text atau hash dengan MD5/SHA1. Pakai password_hash().
// ❌ DANGEROUS
$password = md5($userInput);
// ✅ BAGUS -- password_hash()
$hashedPassword = password_hash($userInput, PASSWORD_BCRYPT, [
'cost' => 12,
]);
Saat verifikasi login:
public function login()
{
$email = $this->request->getVar('email');
$password = $this->request->getVar('password');
$user = $this->userModel
->where('email', $email)
->first();
if (!$user || !password_verify($password, $user['password'])) {
return redirect()->back();
}
session()->set('user_id', $user['id']);
}
Gunakan password_verify() untuk cek, bukan string comparison.
Jangan percaya input user. Selalu validate di backend (frontend validation mudah di-bypass).
public function register()
{
if (!$this->validate([
'name' => 'required|string|min_length[3]',
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|min_length[8]',
])) {
return redirect()->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$data = $this->getValidatedInput();
$this->userModel->insert($data);
}
CI4 validation rules powerful — baca docs untuk lengkap list rules.
Jangan transmit sensitive data di HTTP. Selalu pakai HTTPS di production.
// app/Config/App.php
public $baseURL = 'https://example.com';
public $forceGlobalSecureRequests = true;
Untuk API, pakai API Keys atau JWT:
// app/Filters/ApiKeyFilter.php
class ApiKeyFilter implements FilterInterface
{
public function before($request, $arguments = null)
{
$apiKey = $request->getHeaderLine('X-API-Key');
if (!$apiKey || !$this->isValidApiKey($apiKey)) {
return response([
'error' => 'Invalid API key',
], 401);
}
}
private function isValidApiKey($key): bool
{
return hash_equals(
getenv('API_KEY'),
$key
);
}
}
Gunakan hash_equals() untuk timing-safe comparison.
File upload adalah attack vector. Validate file size, type, dan extension.
public function uploadAvatar()
{
if (!$this->validate([
'avatar' => 'uploaded[avatar]|max_size[avatar,2048]|is_image[avatar]',
])) {
return redirect()->back();
}
$file = $this->request->getFile('avatar');
$newName = $file->getRandomName();
// Move ke folder aman
$file->move('/var/uploads', $newName);
$this->userModel->update(session()->get('user_id'), [
'avatar' => $newName,
]);
}
Jangan simpan file di public/ folder. Simpan di folder lain dan serve via controller.
HTTP headers block beberapa attack di browser level.
// app/Filters/SecurityHeadersFilter.php
class SecurityHeadersFilter implements FilterInterface
{
public function after($request, $response, $arguments = null)
{
$response->setHeader('X-Frame-Options', 'DENY');
$response->setHeader('X-Content-Type-Options', 'nosniff');
$response->setHeader('X-XSS-Protection', '1; mode=block');
$response->setHeader('Content-Security-Policy',
"default-src 'self';");
return $response;
}
}
Headers ini simple tapi powerful — block clickjacking, MIME sniffing, XSS.
Security bukan feature — ini foundation. SQL injection, XSS, CSRF — semuanya bisa dicegah dengan praktik tepat. Start dari basic: query builder, escape output, validate input, hash password, HTTPS. Terus upgrade dengan headers, rate limiting, file validation. Remember: security adalah process, bukan destination. Stay alert, keep updated, test regularly. Aplikasi lu jauh lebih aman! 🔒