Python

πŸ›‘οΈ PHP Security Best Practices

Panduan lengkap keamanan aplikasi PHP β€” SQL injection, XSS, CSRF, input validation, password hashing, session security, dan quiz interaktif dengan contoh kode praktis

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 InjectionMenyisipkan 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 AuthenticationLemahnya sistem autentikasi (password, session)πŸ”΄ Kritis
Sensitive Data ExposureKebocoran data sensitif (password, kartu kredit)πŸ”΄ Kritis
File Upload VulnerabilityUpload file berbahaya yang bisa dieksekusi🟑 Tinggi
Security MisconfigurationKonfigurasi server/aplikasi yang tidak aman🟑 Tinggi

Prinsip Dasar Keamanan

⚠️ Golden Rule 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.

Diagram: Alur Keamanan Input
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             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 β€” SQL Injection (JANGAN LAKUKAN!)
<?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 β€” Prepared Statements (PDO)
<?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 β€” 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 XSSKode berbahaya disimpan di databaseKomentar berisi script
Reflected XSSKode berbahaya dikirim via URL/parameterSearch query berisi script
DOM-based XSSManipulasi DOM di client-sideURL fragment berisi script

Contoh dan Pencegahan XSS

PHP β€” XSS Prevention
<?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 β€” Sanitasi Helper
<?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 β€” CSRF Protection
<?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)

PHP β€” Laravel CSRF
<!-- 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 β€” Input Validation
<?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 β€” Password SALAH (JANGAN!)
<?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 β€” Password Hashing (BENAR)
<?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 β€” Session Security
<?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 β€” Secure File Upload
<?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
<?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
?>
πŸ’‘ Checklist Keamanan PHP

βœ… 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:

Pertanyaan 1: Apa cara TERBAIK untuk mencegah SQL Injection di PHP?

a) Menggunakan addslashes()
b) Menggunakan htmlspecialchars()
c) Menggunakan prepared statements (PDO/MySQLi)
d) Validasi input di sisi client (JavaScript)

Pertanyaan 2: Fungsi PHP apa yang digunakan untuk meng-hash password dengan aman?

a) md5()
b) sha1()
c) base64_encode()
d) password_hash()

Pertanyaan 3: Apa tujuan dari CSRF token dalam form HTML?

a) Mengenkripsi data form sebelum dikirim
b) Memverifikasi bahwa form dikirim dari domain kita sendiri, bukan dari situs lain
c) Mengompresi ukuran data form
d) Mengingat data form yang pernah diisi sebelumnya

Pertanyaan 4: Apa fungsi dari htmlspecialchars() dalam mencegah XSS?

a) Menghapus semua tag HTML dari input
b) Mengonversi karakter khusus HTML menjadi entity (misal: < menjadi &lt;)
c) Mengenkripsi input menjadi format yang tidak bisa dibaca
d) Memblokir semua JavaScript dari berjalan di halaman

Pertanyaan 5: Mengapa kita TIDAK boleh menggunakan md5() untuk meng-hash password?

a) Karena md5() terlalu lambat untuk memproses banyak password
b) Karena md5() menghasilkan hash yang terlalu panjang
c) Karena md5() sangat cepat dihitung sehingga mudah di-brute-force, dan tidak memiliki salt built-in
d) Karena md5() hanya bisa digunakan di Linux, bukan Windows
πŸ” Zoom
100%
🎨 Tema