Belajar Application Security Engineer - Secure Coding
Episode 4 of 28

Belajar Application Security Engineer - Secure Coding

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

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Pilar 1: Input Validation

Prinsip dasarnya satu kalimat: validasi di sisi server, dengan allowlist.

Allowlist vs Denylist

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:

Validasi username dengan allowlist + Zod
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.

Canonicalization

Validasi harus dilakukan setelah input dinormalisasi (decoded, path-resolved). Klasiknya path traversal:

Path traversal melewati validasi naif
input : report.pdf
filter: tolak "../"        → lolos
input : %2e%2e%2fetc/passwd → setelah decode jadi ../etc/passwd

Jika 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.

Pilar 2: Output Encoding

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 KeluaranEncodingContoh Bahaya
HTML bodyHTML entity escape&lt;img src=x onerror=alert(1)&gt;
Atribut HTMLAttribute escape + kutip gandaBreak out via " onmouseover=
JavaScriptJSON serialize, hindari inline&lt;/script&gt;&lt;script&gt;...
URLPercent-encodingParameter injection
CSSCSS escapeEksekusi 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:

Lubang XSS di framework modern
// Aman: auto-escaped sebagai teks
&lt;p&gt;{userBio}&lt;/p&gt;
 
// Rentan: bypass escaping
&lt;div dangerouslySetInnerHTML={{ __html: userBio }} /&gt;

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.

Pilar 3: Authentication & Authorization di Level Kode

AuthN menjawab "siapa kamu"; AuthZ menjawab "boleh kamu melakukan ini?". Kesalahan fatal yang paling sering saya temui: menganggap autentikasi sukses berarti otorisasi otomatis sah.

Pola Authorization yang Benar

Object-level authorization check
app.delete("/api/projects/:id", requireAuth, async (req, res) =&gt; {
  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 &amp;&amp; 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.

Pilar 4: Data Handling

Data sensitif butuh perlakuan khusus di seluruh siklus hidupnya:

  1. In transit: TLS 1.2+, HSTS header.
  2. At rest: enkripsi AES-256-GCM untuk PII/kartu; password pakai Argon2id/bcrypt (bukan SHA256 polos).
  3. Di log: never log password, token, nomor kartu penuh. Redaksi sebelum write.
  4. Di error: stack trace ke monitoring, pesan generik ke user.
  5. Retensi: hapus data yang masa simpannya habis — data tak dibutuhkan adalah liability.
Logging aman vs bocor
// 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.

Praktik: Security Code Review Mini

Sekarang gabungkan keempat pilar. Temukan minimal tiga masalah di kode berikut sebelum membaca analisisnya:

routes/profile.ts — cari 3+ kerentanan
app.post("/api/profile", async (req, res) =&gt; {
  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:

  1. SQL injection (baris 3-5): string interpolation langsung → ganti parameterized query.
  2. IDOR (baris 3): id dari query string tanpa cek kepemilikan → ambil dari req.user, bukan input.
  3. Stored XSS potensial (baris 6): bio disimpan mentah → sanitasi/encode saat render, validasi panjang.
  4. Log bocor (baris 7): payload user masuk log → log ID saja.

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.

Common Pitfalls

  • Validasi hanya di frontend: semua kontrol klien hanyalah UX; penyerang bicara langsung ke API.
  • Encoding ganda/di tempat salah: encode saat simpan membuat data rusak; encode tepat saat output.
  • Regex untuk parsing HTML/XML: regex tidak bisa parse konteks bersarang; gunakan parser/sanitizer.
  • Role check tanpa object check: isAdmin saja belum cukup jika resource-nya milik tenant lain.

Penutup

Inti yang harus dibawa pulang:

  • Empat pilar: allowlist validation, context-aware output encoding, object-level authorization, dan data handling sepanjang lifecycle.
  • Encode berdasarkan konteks keluaran, bukan jenis input; waspadai escape hatch framework.
  • Setiap resource milik orang lain wajib punya check eksplisit — middleware auth bukan otorisasi.
  • Review yang baik: baris + dampak + diff perbaikan.

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!

Belajar Application Security Engineer - Secure Coding | Belajar Application Security Engineer