1. Pengenalan Keamanan PHP
Keamanan aplikasi web adalah aspek yang sangat penting dalam pengembangan PHP. Serangan terhadap aplikasi web bisa menyebabkan kebocoran data, kerugian finansial, dan kerusakan reputasi. Sebagai developer, Anda bertanggung jawab untuk melindungi aplikasi dan data pengguna.
Ancaman Umum (OWASP Top 10)
| Ancaman | Penjelasan | Tingkat Bahaya |
|---|---|---|
| SQL Injection | Menyisipkan kode SQL berbahaya melalui input pengguna | π΄ Kritis |
| Cross-Site Scripting (XSS) | Menyisipkan skrip berbahaya di halaman web | π΄ Kritis |
| Cross-Site Request Forgery (CSRF) | Memaksa pengguna menjalankan aksi tanpa izin | π‘ Tinggi |
| Broken Authentication | Lemahnya sistem autentikasi (password, session) | π΄ Kritis |
| Sensitive Data Exposure | Kebocoran data sensitif (password, kartu kredit) | π΄ Kritis |
| File Upload Vulnerability | Upload file berbahaya yang bisa dieksekusi | π‘ Tinggi |
| Security Misconfiguration | Konfigurasi server/aplikasi yang tidak aman | π‘ Tinggi |
Prinsip Dasar Keamanan
Jangan pernah mempercayai input dari pengguna! Semua data yang datang dari luar (form, URL, API, cookie, header) harus divalidasi dan disanitasi sebelum diproses atau disimpan. Ini adalah prinsip paling fundamental dalam keamanan aplikasi web.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β ALUR KEAMANAN INPUT β β β β ββββββββββββ β β β User β Input dari form/URL/API β β β (Client) β βββββββββββββββββββββββ β β ββββββββββββ β β β ββββββββΌβββββββ β β β 1. VALIDASI β β β β - Tipe data β β β β - Panjang β β β β - Format β β β β - Whitelist β β β ββββββββ¬βββββββ β β ββββββββΌβββββββ β β β 2. SANITASI β β β β - htmlspecialcharsβ β β β - strip_tags β β β β - filter_var β β β ββββββββ¬βββββββ β β ββββββββΌβββββββ β β β 3. ESCAPE β β β β - Prepared β β β β Statementsβ β β β - Context β β β β aware β β β ββββββββ¬βββββββ β β ββββββββΌβββββββ β β β 4. PROSES β β β β (aman!) β β β ββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. SQL Injection
SQL Injection adalah jenis serangan di mana penyerang menyisipkan kode SQL berbahaya melalui input pengguna. Ini bisa menyebabkan manipulasi database, kebocoran data, atau bahkan penghapusan seluruh database.
Contoh SQL Injection (BERBAHAYA!)
<?php // β SANGAT BERBAHAYA! β Raw query tanpa sanitasi $username = $_POST['username']; // Bisa jadi: ' OR '1'='1' -- $password = $_POST['password']; // Bisa jadi: apapun $query = "SELECT * FROM users WHERE username='$username' AND password='$password'"; // Hasil query: // SELECT * FROM users WHERE username='' OR '1'='1' --' AND password='apapun' // '--' komentar di SQL, semua setelahnya diabaikan! // Hasil: SEMUA data user terbocor! // Contoh serangan lain yang bisa menghapus database: // username: '; DROP TABLE users; -- // Query: SELECT * FROM users WHERE username=''; DROP TABLE users; --' $result = mysqli_query($conn, $query); // π Bencana! ?>
Solusi: Prepared Statements (AMAN)
<?php
// β
AMAN β Menggunakan PDO dengan prepared statements
// Koneksi PDO
$dsn = "mysql:host=localhost;dbname=myapp;charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, "root", "", $options);
} catch (PDOException $e) {
die("Koneksi gagal: " . $e->getMessage());
}
// ===== Prepared Statement dengan named parameters =====
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND status = :status");
$stmt->execute([
':username' => $username,
':status' => 'active',
]);
$user = $stmt->fetch();
// ===== Prepared Statement dengan positional parameters =====
$stmt = $pdo->prepare("SELECT * FROM posts WHERE user_id = ? AND status = ? ORDER BY created_at DESC");
$stmt->execute([$userId, 'published']);
$posts = $stmt->fetchAll();
// ===== INSERT dengan prepared statement =====
$stmt = $pdo->prepare("
INSERT INTO users (nama, email, password, role)
VALUES (:nama, :email, :password, :role)
");
$stmt->execute([
':nama' => $nama,
':email' => $email,
':password' => password_hash($password, PASSWORD_DEFAULT),
':role' => 'user',
]);
$newUserId = $pdo->lastInsertId();
// ===== UPDATE dengan prepared statement =====
$stmt = $pdo->prepare("UPDATE users SET nama = :nama, email = :email WHERE id = :id");
$stmt->execute([
':nama' => $namaBaru,
':email' => $emailBaru,
':id' => $userId,
]);
echo "{$stmt->rowCount()} baris diupdate";
// ===== DELETE dengan prepared statement =====
$stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id AND user_id = :user_id");
$stmt->execute([
':id' => $postId,
':user_id' => $userId, // Pastikan hanya owner yang bisa hapus!
]);
// ===== LIKE query (tetap aman) =====
$search = "%$keyword%";
$stmt = $pdo->prepare("SELECT * FROM posts WHERE title LIKE :search OR content LIKE :search");
$stmt->execute([':search' => $search]);
$results = $stmt->fetchAll();
?>
Menggunakan MySQLi Prepared Statements
<?php
// β
AMAN β Menggunakan MySQLi dengan prepared statements
$conn = new mysqli("localhost", "root", "", "myapp");
$conn->set_charset("utf8mb4");
// Prepared statement
$stmt = $conn->prepare("SELECT * FROM users WHERE email = ? AND status = ?");
$stmt->bind_param("ss", $email, $status); // "ss" = 2 string parameters
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
// INSERT
$stmt = $conn->prepare("INSERT INTO users (nama, email) VALUES (?, ?)");
$stmt->bind_param("ss", $nama, $email);
$stmt->execute();
echo "User ID: " . $stmt->insert_id;
$stmt->close();
$conn->close();
?>
3. Cross-Site Scripting (XSS)
XSS (Cross-Site Scripting) adalah serangan di mana penyerang menyisipkan kode JavaScript berbahaya yang kemudian dieksekusi di browser pengguna lain. XSS bisa mencuri cookie, session token, atau mengubah tampilan halaman.
3 Jenis XSS
| Jenis | Cara Kerja | Contoh |
|---|---|---|
| Stored XSS | Kode berbahaya disimpan di database | Komentar berisi script |
| Reflected XSS | Kode berbahaya dikirim via URL/parameter | Search query berisi script |
| DOM-based XSS | Manipulasi DOM di client-side | URL fragment berisi script |
Contoh dan Pencegahan XSS
<?php
// β BERBAHAYA β Output langsung tanpa escaping
$nama = $_GET['nama']; // Bisa jadi: <script>alert('XSS')</script>
echo "Halo, $nama!"; // Script berbahaya dieksekusi!
// β BERBAHAYA β Menampilkan konten dari database tanpa escaping
$comment = $row['comment']; // Isi dari database
echo "<p>$comment</p>"; // XSS jika comment berisi script!
// ========================================
// β
SOLUSI 1: htmlspecialchars() (PENTING!)
// ========================================
// Selalu gunakan saat menampilkan data dari user/input di HTML
$nama = htmlspecialchars($_GET['nama'], ENT_QUOTES, 'UTF-8');
echo "Halo, $nama!"; // Aman! Script ditampilkan sebagai teks
// Opsi lengkap htmlspecialchars
$safe = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// ========================================
// β
SOLUSI 2: Filter output berdasarkan konteks
// ========================================
// Untuk konten HTML (mengizinkan tag tertentu)
$cleanHtml = strip_tags($userContent, '<p><b><i><u><a><ul><ol><li>');
// Untuk atribut HTML
$safeAttr = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
echo "<input value='{$safeAttr}'>";
// Untuk URL
$safeUrl = filter_var($userUrl, FILTER_SANITIZE_URL);
if (filter_var($safeUrl, FILTER_VALIDATE_URL)) {
echo "<a href='" . htmlspecialchars($safeUrl) . "'>Link</a>";
}
// Untuk JavaScript context
$safeJs = json_encode($userInput, JSON_HEX_TAG | JSON_HEX_AMP);
echo "<script>var data = {$safeJs};</script>";
// ========================================
// β
SOLUSI 3: Content Security Policy (CSP)
// ========================================
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;");
// ========================================
// β
SOLUSI 4: HTTPOnly cookies
// ========================================
session_set_cookie_params([
'httponly' => true, // JavaScript tidak bisa akses cookie
'secure' => true, // Hanya HTTPS
'samesite' => 'Strict',
]);
session_start();
?>
<!-- Dalam template Blade Laravel: --}}
<!-- {{ $nama }} otomatis escape dengan htmlspecialchars --!!>
<!-- {!! $nama !!} output tanpa escape (HANYA untuk konten terpercaya!) --!!>
Fungsi Helper untuk Sanitasi
<?php
// Fungsi helper untuk sanitasi
class Security
{
/**
* Sanitasi output HTML
*/
public static function escapeHtml(string $input): string
{
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* Sanitasi untuk digunakan dalam atribut HTML
*/
public static function escapeAttr(string $input): string
{
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* Sanitasi URL
*/
public static function sanitizeUrl(string $url): string
{
$url = filter_var($url, FILTER_SANITIZE_URL);
// Hanya izinkan http:// dan https://
if (!preg_match('/^https?:\/\//', $url)) {
return '#';
}
return htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
}
/**
* Hapus semua tag HTML
*/
public static function stripAllTags(string $input): string
{
return strip_tags($input);
}
/**
* Sanitasi untuk output JSON
*/
public static function escapeJson($data): string
{
return json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}
}
// Penggunaan
echo Security::escapeHtml($userInput);
echo '<a href="' . Security::sanitizeUrl($userUrl) . '">Link</a>';
echo '<input value="' . Security::escapeAttr($value) . '">';
?>
4. Cross-Site Request Forgery (CSRF)
CSRF adalah serangan yang memaksa pengguna yang sudah login untuk menjalankan aksi yang tidak diinginkan (seperti mengubah password, menghapus data, atau transfer uang) tanpa sepengetahuan mereka.
Cara Kerja CSRF
Misalkan user sudah login di bank.com. Penyerang membuat halaman palsu yang mengirim form POST ke bank.com untuk transfer uang. Ketika user mengunjungi halaman palsu tersebut, form otomatis terkirim karena browser mengirim cookie session bank.com!
Pencegahan CSRF dengan Token
<?php
// ===== Membuat CSRF Token =====
// Memulai session
session_start();
// Generate CSRF token (unik per session)
function generateCsrfToken(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token';
}
// Memverifikasi CSRF token
function verifyCsrfToken(string $token): bool
{
if (empty($_SESSION['csrf_token'])) {
return false;
}
// Gunakan hash_equals untuk timing-safe comparison
return hash_equals($_SESSION['csrf_token'], $token);
}
// Regenerate token setelah digunakan (one-time token)
function regenerateCsrfToken(): void
{
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<!-- ===== Form HTML dengan CSRF Token ===== -->
<form action="proses.php" method="POST">
<!-- Sisipkan token di form -->
<input type="hidden" name="csrf_token" value="<?php echo generateCsrfToken(); ?>">
<label>Nama:</label>
<input type="text" name="nama" required>
<label>Email:</label>
<input type="email" name="email" required>
<button type="submit">Simpan</button>
</form>
<?php
// ===== Memproses form dengan verifikasi CSRF =====
// File: proses.php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. Verifikasi CSRF token terlebih dahulu
$token = $_POST['csrf_token'] ?? '';
if (!verifyCsrfToken($token)) {
die("β Akses ditolak! CSRF token tidak valid. Silakan coba lagi.");
}
// 2. Regenerate token setelah verifikasi (one-time use)
regenerateCsrfToken();
// 3. Proses data form (setelah CSRF terverifikasi)
$nama = htmlspecialchars(trim($_POST['nama']));
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
echo "β
Data berhasil disimpan: $nama ($email)";
}
?>
CSRF di Framework (Laravel)
<!-- Laravel sudah include CSRF protection otomatis! -->
<!-- Blade template -->
<form method="POST" action="/profile">
@csrf <!-- Blade directive, otomatis generate token --!!>
<input type="text" name="nama">
<button type="submit">Simpan</button>
</form>
<!-- Untuk AJAX requests -->
<script>
// Ambil token dari meta tag
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': token,
'Content-Type': 'application/json',
},
body: JSON.stringify({ key: 'value' })
});
</script>
<!-- Di layout Blade: -->
<meta name="csrf-token" content="{{ csrf_token() }}">
5. Input Validation & Sanitasi
Validasi input adalah lini pertahanan pertama dalam keamanan aplikasi. Selalu validasi di sisi server β jangan hanya mengandalkan validasi di browser (client-side).
<?php
// ===== filter_var() β Fungsi validasi bawaan PHP =====
// Validasi email
$email = "user@example.com";
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo "Email valid β
";
} else {
echo "Email tidak valid β";
}
// Validasi URL
$url = "https://beebanelabs.pages.dev";
if (filter_var($url, FILTER_VALIDATE_URL)) {
echo "URL valid β
";
}
// Validasi IP address
$ip = "192.168.1.1";
if (filter_var($ip, FILTER_VALIDATE_IP)) {
echo "IP valid β
";
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
echo "IPv4 valid β
";
}
// Validasi integer dalam range
$umur = 25;
if (filter_var($umur, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 150]])) {
echo "Umur valid β
";
}
// Validasi boolean
$aktif = filter_var("true", FILTER_VALIDATE_BOOLEAN); // true
$aktif = filter_var("1", FILTER_VALIDATE_BOOLEAN); // true
$aktif = filter_var("yes", FILTER_VALIDATE_BOOLEAN); // true
$aktif = filter_var("0", FILTER_VALIDATE_BOOLEAN); // false
// Sanitasi β membersihkan input
$input = "<script>alert('xss')</script>Hello & World";
$clean = filter_var($input, FILTER_SANITIZE_STRING); // deprecated PHP 8.1
$clean = filter_var($input, FILTER_SANITIZE_FULL_SPECIAL_CHARS); // PHP 8.1+
// ===== Validasi Manual yang Komprehensif =====
class Validator
{
private array $errors = [];
public function validate(array $data, array $rules): array
{
foreach ($rules as $field => $ruleList) {
$value = $data[$field] ?? null;
$ruleArray = explode('|', $ruleList);
foreach ($ruleArray as $rule) {
$this->applyRule($field, $value, $rule);
}
}
return $this->errors;
}
private function applyRule(string $field, $value, string $rule): void
{
[$name, $param] = array_pad(explode(':', $rule, 2), 2, null);
switch ($name) {
case 'required':
if (empty($value) && $value !== '0') {
$this->errors[$field][] = "{$field} wajib diisi.";
}
break;
case 'string':
if (!is_string($value)) {
$this->errors[$field][] = "{$field} harus berupa teks.";
}
break;
case 'email':
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->errors[$field][] = "{$field} format email tidak valid.";
}
break;
case 'min':
if (is_string($value) && strlen($value) < (int)$param) {
$this->errors[$field][] = "{$field} minimal {$param} karakter.";
} elseif (is_numeric($value) && $value < (int)$param) {
$this->errors[$field][] = "{$field} minimal {$param}.";
}
break;
case 'max':
if (is_string($value) && strlen($value) > (int)$param) {
$this->errors[$field][] = "{$field} maksimal {$param} karakter.";
}
break;
case 'numeric':
if (!is_numeric($value)) {
$this->errors[$field][] = "{$field} harus berupa angka.";
}
break;
case 'in':
$allowed = explode(',', $param);
if (!in_array($value, $allowed)) {
$this->errors[$field][] = "{$field} harus salah satu dari: " . implode(', ', $allowed);
}
break;
}
}
public function fails(): bool
{
return !empty($this->errors);
}
}
// Menggunakan validator
$validator = new Validator();
$errors = $validator->validate($_POST, [
'nama' => 'required|string|min:3|max:100',
'email' => 'required|email',
'umur' => 'required|numeric|min:17|max:100',
'status' => 'required|in:active,inactive',
]);
if ($validator->fails()) {
foreach ($errors as $field => $messages) {
foreach ($messages as $msg) {
echo "β $msg<br>";
}
}
} else {
echo "β
Semua input valid!";
}
?>
6. Password Hashing
Jangan pernah menyimpan password dalam bentuk teks biasa (plain text)! Selalu gunakan hashing yang aman. PHP menyediakan fungsi built-in yang sangat mudah digunakan.
Cara SALAH Menyimpan Password
<?php
// β SALAH β Plain text (TIDAK ADA ENKRIPSI!)
$password = "rahasia123";
$pdo->prepare("INSERT INTO users (password) VALUES (?)")->execute([$password]);
// β SALAH β MD5 (SANGAT LEMAH! Bisa di-brute-force dalam detik)
$password = md5("rahasia123"); // "2c17f3a5b5e5417c0ecd0b9c7e2e1d3e"
// Tabel rainbow MD5 sudah tersedia di internet!
// β SALAH β SHA1 (LEMAH!)
$password = sha1("rahasia123");
// β SALAH β SHA256 tanpa salt (LEMAH!)
$password = hash('sha256', "rahasia123");
?>
Cara BENAR Menyimpan Password
<?php
// β
BENAR β Menggunakan password_hash() dan password_verify()
// ===== Registrasi: Hash password sebelum disimpan =====
$password = $_POST['password']; // "rahasia123"
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// Hasil: "$2y$10$X8vL5kR5.j7C4Z3N9a2QG.O7X8Vl5kR5j7C4Z3N9a2QG.O7"
// Otomatis mengandung: algoritma, cost factor, salt, dan hash
// Simpan ke database
$stmt = $pdo->prepare("INSERT INTO users (nama, email, password) VALUES (?, ?, ?)");
$stmt->execute([$nama, $email, $hashedPassword]);
// ===== Login: Verifikasi password =====
$email = $_POST['email'];
$password = $_POST['password'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
// Password cocok! Login berhasil β
session_start();
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['nama'];
header("Location: /dashboard");
exit;
} else {
// Password salah β
$error = "Email atau password salah!";
}
// ===== Upgrade hashing algorithm (saat user login) =====
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$newHash, $user['id']]);
}
// ===== Password strength validation =====
function validatePasswordStrength(string $password): array
{
$errors = [];
if (strlen($password) < 8) {
$errors[] = "Password minimal 8 karakter";
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = "Password harus mengandung huruf besar";
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = "Password harus mengandung huruf kecil";
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = "Password harus mengandung angka";
}
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
$errors[] = "Password harus mengandung karakter khusus";
}
// Cek password umum
$commonPasswords = ['password', '12345678', 'qwerty123', 'admin123'];
if (in_array(strtolower($password), $commonPasswords)) {
$errors[] = "Password terlalu umum, pilih yang lebih unik";
}
return $errors;
}
// Menggunakan
$errors = validatePasswordStrength($_POST['password']);
if (!empty($errors)) {
foreach ($errors as $err) {
echo "β $err<br>";
}
}
?>
7. Session Security
Session yang tidak aman bisa dicuri atau di-hijack oleh penyerang. Berikut best practices untuk mengamankan session PHP.
<?php
// ===== Konfigurasi Session yang Aman =====
// Harus dipanggil SEBELUM session_start()
session_set_cookie_params([
'lifetime' => 3600, // Expired dalam 1 jam
'path' => '/',
'domain' => 'example.com',
'secure' => true, // Hanya kirim via HTTPS
'httponly' => true, // Tidak bisa diakses JavaScript
'samesite' => 'Strict', // Mencegah CSRF
]);
session_start();
// ===== Regenerate Session ID =====
// Regenerate setelah login (mencegah session fixation)
function loginUser(array $user): void
{
session_regenerate_id(true); // true = hapus session lama
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['nama'];
$_SESSION['role'] = $user['role'];
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['login_time'] = time();
}
// ===== Validasi Session =====
function isSessionValid(): bool
{
if (empty($_SESSION['user_id'])) {
return false;
}
// Cek IP address (opsional, bisa bermasalah jika IP berubah)
if ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
destroySession();
return false;
}
// Cek User Agent
if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
destroySession();
return false;
}
// Cek timeout (auto-logout setelah 30 menit tidak aktif)
$timeout = 1800; // 30 menit
if (time() - $_SESSION['login_time'] > $timeout) {
destroySession();
return false;
}
// Perpanjang waktu session
$_SESSION['login_time'] = time();
return true;
}
// ===== Logout yang Aman =====
function destroySession(): void
{
$_SESSION = [];
// Hapus cookie session
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
}
// ===== Proteksi Brute Force Login =====
function checkLoginAttempts(string $email): bool
{
$key = "login_attempts:" . md5($email);
$attempts = $_SESSION[$key] ?? ['count' => 0, 'last_attempt' => 0];
// Reset setelah 15 menit
if (time() - $attempts['last_attempt'] > 900) {
$_SESSION[$key] = ['count' => 0, 'last_attempt' => 0];
return true;
}
// Maksimal 5 percobaan
if ($attempts['count'] >= 5) {
$remaining = 900 - (time() - $attempts['last_attempt']);
echo "β Terlalu banyak percobaan. Coba lagi dalam {$remaining} detik.";
return false;
}
return true;
}
function recordLoginAttempt(string $email): void
{
$key = "login_attempts:" . md5($email);
$attempts = $_SESSION[$key] ?? ['count' => 0, 'last_attempt' => 0];
$attempts['count']++;
$attempts['last_attempt'] = time();
$_SESSION[$key] = $attempts;
}
?>
8. File Upload Security
File upload adalah salah satu fitur paling berbahaya jika tidak diamankan. Penyerang bisa meng-upload file PHP berbahaya yang kemudian dieksekusi di server.
<?php
function secureFileUpload(array $file): ?string
{
// ===== 1. Cek error upload =====
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException("Upload gagal: error code {$file['error']}");
}
// ===== 2. Batasi ukuran file (misal: 2MB) =====
$maxSize = 2 * 1024 * 1024; // 2MB
if ($file['size'] > $maxSize) {
throw new RuntimeException("File terlalu besar. Maksimal 2MB.");
}
// ===== 3. Whitelist tipe file yang diizinkan =====
$allowedTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
];
// Deteksi tipe file menggunakan finfo (BUKAN dari filename!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!array_key_exists($mimeType, $allowedTypes)) {
throw new RuntimeException("Tipe file tidak diizinkan: $mimeType");
}
// ===== 4. Cek ekstensi file =====
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($extension, $allowedExtensions)) {
throw new RuntimeException("Ekstensi file tidak diizinkan: .$extension");
}
// ===== 5. Validasi konten gambar =====
if (str_starts_with($mimeType, 'image/')) {
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
throw new RuntimeException("File bukan gambar yang valid!");
}
}
// ===== 6. Generate nama file acak (jangan pakai nama asli!) =====
$newFilename = bin2hex(random_bytes(16)) . '.' . $extension;
// ===== 7. Simpan ke direktori yang aman =====
// Pastikan direktori upload TIDAK bisa mengeksekusi PHP!
$uploadDir = __DIR__ . '/uploads/';
// Buat .htaccess untuk mencegah eksekusi PHP di folder upload
$htaccess = $uploadDir . '/.htaccess';
if (!file_exists($htaccess)) {
file_put_contents($htaccess, "php_flag engine off\nRemoveHandler .php .phtml\n");
}
$destination = $uploadDir . $newFilename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException("Gagal menyimpan file.");
}
return $newFilename;
}
// ===== Menggunakan =====
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
try {
$filename = secureFileUpload($_FILES['avatar']);
echo "β
File berhasil diupload: $filename";
} catch (RuntimeException $e) {
echo "β Error: " . $e->getMessage();
}
}
?>
9. Security Headers
HTTP security headers adalah lapisan pertahanan tambahan yang dikirim dari server ke browser untuk mencegah berbagai jenis serangan.
<?php
// ===== Security Headers β tambahkan di awal setiap request =====
// 1. Content Security Policy (CSP) β mencegah XSS
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';");
// 2. X-Content-Type-Options β mencegah MIME sniffing
header("X-Content-Type-Options: nosniff");
// 3. X-Frame-Options β mencegah clickjacking
header("X-Frame-Options: DENY");
// Atau: "SAMECONTENT" (hanya bisa di-iframe oleh domain sendiri)
// 4. X-XSS-Protection β tambahan XSS filter (legacy browser)
header("X-XSS-Protection: 1; mode=block");
// 5. Strict-Transport-Security (HSTS) β paksa HTTPS
header("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload");
// 6. Referrer-Policy β kontrol informasi referrer
header("Referrer-Policy: strict-origin-when-cross-origin");
// 7. Permissions-Policy β kontrol fitur browser
header("Permissions-Policy: camera=(), microphone=(), geolocation=()");
// 8. Cache-Control β mencegah cache data sensitif
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
// ===== PHP Configuration yang Aman (php.ini) =====
// expose_php = Off β Jangan tampilkan versi PHP di header
// display_errors = Off β Jangan tampilkan error di production
// log_errors = On β Log error ke file
// error_log = /var/log/php_errors.log
// allow_url_fopen = Off β Mencegah remote file inclusion
// allow_url_include = Off β Mencegah remote file inclusion
// session.cookie_httponly = 1 β Cookie tidak bisa diakses JS
// session.cookie_secure = 1 β Cookie hanya via HTTPS
// session.use_strict_mode = 1 β Tolak uninitialized session ID
?>
β
Gunakan prepared statements untuk semua query database
β
Escape output dengan htmlspecialchars()
β
Validate & sanitize semua input dari user
β
Gunakan password_hash() untuk password
β
Implementasi CSRF token di semua form
β
Konfigurasi security headers
β
Regenerate session ID setelah login
β
Batasi file upload dengan whitelist
β
HTTPS everywhere β selalu gunakan SSL/TLS
β
Update PHP ke versi terbaru secara berkala
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang PHP Security: