Menguasai empat pilar secure coding: input validation dengan allowlist, output encoding sesuai konteks, authorization check yang tidak bisa dilewati, dan data handling aman; ditutup praktik security code review pada potongan kode nyata sehingga kalian bisa langsung memberi komentar PR yang konstruktif

Setelah di episode 3 kita menguasai kosakata OWASP Top 10, episode ini turun ke level baris kode: secure coding — praktik menulis kode yang tidak menciptakan kerentanan sejak awal. Ini skill inti AppSec engineer karena dua alasan: review kalian hanya kredibel jika bisa menunjukkan cara memperbaiki, dan developer hanya akan mengikuti pola aman jika polanya konkret, bukan slogan.
Kita bahas empat pilar: input validation, output encoding, authentication/authorization di level kode, dan data handling. Lalu kita praktikkan semuanya dalam satu sesi code review.
Prinsip dasarnya satu kalimat: validasi di sisi server, dengan allowlist.
Denylist (blokir ', --, <script>) adalah perang yang kalah: varian encoding (%27, %3Cscript%3E), double encoding, dan unicode tricks selalu selangkah lebih cepat. Allowlist mendefinisikan apa yang boleh dan menolak sisanya:
import { z } from "zod";
const usernameSchema = z
.string()
.min(3)
.max(30)
.regex(/^[a-zA-Z0-9_.]+$/, "hanya huruf, angka, titik, underscore");
usernameSchema.parse(req.body.username);Perhatikan validasinya berlapis: panjang, karakter, lalu tipe. Untuk field terstruktur (tanggal, angka, enum), validasi rentangnya juga: umur harus bilangan bulat 0–150, jumlah transfer harus positif dan ≤ saldo.
Validasi harus dilakukan setelah input dinormalisasi (decoded, path-resolved). Klasiknya path traversal:
input : report.pdf
filter: tolak "../" → lolos
input : %2e%2e%2fetc/passwd → setelah decode jadi ../etc/passwdJika aplikasi menerima nama file, normalisasi dulu (path.resolve), baru pastikan hasil akhir masih di dalam direktori yang diizinkan — bukan sekadar memfilter substring ...
Note
Validasi bukan pengganti parameterized query atau encoding — ia lapisan pertama dari defense in depth. Input yang lolos validasi tetap harus lewat jalur aman saat diproses.
Input validation mengurangi sampah masuk; output encoding memastikan data dieksekusi sebagai data, bukan kode, di tempat tujuannya. Kuncinya: encoding bergantung konteks keluaran, bukan jenis input.
| Konteks Keluaran | Encoding | Contoh Bahaya |
|---|---|---|
| HTML body | HTML entity escape | <img src=x onerror=alert(1)> |
| Atribut HTML | Attribute escape + kutip ganda | Break out via " onmouseover= |
| JavaScript | JSON serialize, hindari inline | </script><script>... |
| URL | Percent-encoding | Parameter injection |
| CSS | CSS escape | Eksekusi via expression() |
Framework modern (React, Vue) melakukan auto-escaping untuk kasus umum — inilah alasan mereka jauh lebih aman daripada template string manual. Bahayanya ada di lubang-lubang escape hatch:
// Aman: auto-escaped sebagai teks
<p>{userBio}</p>
// Rentan: bypass escaping
<div dangerouslySetInnerHTML={{ __html: userBio }} />Jika kalian memang butuh render HTML user-generated (komentar rich-text), gunakan sanitizer yang matang — DOMPurify — dengan allowlist tag, dan tetap review konfigurasinya saat update versi.
AuthN menjawab "siapa kamu"; AuthZ menjawab "boleh kamu melakukan ini?". Kesalahan fatal yang paling sering saya temui: menganggap autentikasi sukses berarti otorisasi otomatis sah.
app.delete("/api/projects/:id", requireAuth, async (req, res) => {
const project = await db.project.findById(req.params.id);
if (!project) return res.status(404).json({ error: "not found" });
// Cek kepemilikan — WAJIB, jangan pernah diasumsikan dari route
if (project.ownerId !== req.user.id && req.user.role !== "admin") {
return res.status(403).json({ error: "forbidden" });
}
await db.project.delete(project.id);
res.status(204).end();
});Aturan praktisnya: setiap akses ke resource milik orang lain wajib punya check eksplisit. Middleware requireAuth hanya memverifikasi identitas — dia tidak tahu siapa pemilik project tersebut. Pola gagal klasiknya adalah middleware route-level saja tanpa object-level check → IDOR instan (A01).
Untuk aplikasi kompleks, pertimbangkan library policy (CASL, Oso, Permit) agar aturan otorisasi terpusat dan bisa diuji — tersebar if di ratusan handler adalah resep celah.
Data sensitif butuh perlakuan khusus di seluruh siklus hidupnya:
// BOCOR: token ikut tercatat
logger.info("login attempt", req.headers.authorization);
// AMAN: metadata saja
logger.info("login attempt", { userId: user.id, ip: req.ip });Warning
Log adalah artefak yang paling sering lupa diamankan tapi paling banyak dibaca saat insiden — oleh penyerang juga. Audit seluruh panggilan logger di code path yang menyentuh request body/header sebelum fitur rilis.
Sekarang gabungkan keempat pilar. Temukan minimal tiga masalah di kode berikut sebelum membaca analisisnya:
app.post("/api/profile", async (req, res) => {
const { bio, website } = req.body;
await db.query(
`UPDATE profiles SET bio='${bio}', site='${website}' WHERE id=${req.query.id}`
);
await cache.set(`profile:${req.query.id}`, { bio, website });
console.log(`profile ${req.query.id} updated by with payload ${bio}`);
res.json({ ok: true });
});Analisisnya:
id dari query string tanpa cek kepemilikan → ambil dari req.user, bukan input.bio disimpan mentah → sanitasi/encode saat render, validasi panjang.Empat temuan dari sepuluh baris — dan inilah realitas kode production di banyak organisasi. Perhatikan juga cara menuliskannya di komentar PR: tunjukkan baris, jelaskan dampaknya satu kalimat, lalu sertakan diff perbaikan. Developer merespons solusi, bukan vonis.
isAdmin saja belum cukup jika resource-nya milik tenant lain.Inti yang harus dibawa pulang:
Di episode 5 kita perdalam pilar ketiga: Authentication & Session Management — password hashing yang benar, session hardening, MFA/TOTP, dan OAuth/OIDC beserta jebakan-jebakan JWT-nya. Sampai jumpa di episode 5!