OWASP Top Ten

OWASP Top 10 adalah dokumen standar yang meningkatkan kesadaran developer mengenai risiko keamanan paling kritis pada aplikasi web.


A01:2021 - Broken Access Control

Kontrol akses yang rusak terjadi ketika penyerang dapat mengakses fungsionalitas atau data yang seharusnya tidak dapat mereka akses. Ini adalah kerentanan paling umum dan kritis.

Contoh Skenario: Seorang pengguna biasa bisa mengakses halaman admin hanya dengan menebak URL, atau melihat data pengguna lain dengan mengubah ID di parameter URL.

Contoh Kode (Go)

Misalkan kita memiliki sebuah handler untuk melihat profil pengguna berdasarkan ID.

Kode Rentan:

package main

import (
	"fmt"
	"net/http"
)

// User struct
type User struct {
	ID   string
	Name string
	Role string
}

// Simple in-memory user database
var users = map[string]User{
	"1": {ID: "1", Name: "Alice", Role: "user"},
	"2": {ID: "2", Name: "Bob", Role: "user"},
	"3": {ID: "3", Name: "Admin", Role: "admin"},
}

// Handler ini rentan karena tidak memeriksa siapa yang membuat permintaan.
func GetUserProfile(w http.ResponseWriter, r *http.Request) {
	// Ambil ID pengguna dari URL, misalnya /user?id=1
	userID := r.URL.Query().Get("id")

	// Langsung ambil data dari database berdasarkan ID dari input
	user, ok := users[userID]
	if !ok {
		http.Error(w, "User not found", http.StatusNotFound)
		return
	}

	// Tampilkan data pengguna
	fmt.Fprintf(w, "User ID: %s, Name: %s", user.ID, user.Name)
}

func main() {
	http.HandleFunc("/user", GetUserProfile)
	http.ListenAndServe(":8080", nil)
}

Kelemahan: Pengguna mana pun (termasuk yang tidak login) dapat melihat profil pengguna lain hanya dengan mengubah nilai id di URL (misalnya, http://localhost:8080/user?id=2).

Kode Aman:

Pada versi aman, kita akan memeriksa sesi pengguna yang sedang login dan memastikan mereka hanya bisa melihat profilnya sendiri, kecuali jika mereka adalah admin.

package main

import (
	"fmt"
	"net/http"
)

// ... (struct User dan variabel users sama seperti di atas) ...

// Fungsi middleware sederhana untuk otentikasi
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Di aplikasi nyata, Anda akan memverifikasi token sesi/JWT
		// Di sini kita simulasikan dengan header
		sessionUserID := r.Header.Get("X-User-ID")
		if sessionUserID == "" {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		// Tambahkan ID pengguna ke konteks request
		ctx := r.Context()
		r = r.WithContext(context.WithValue(ctx, "userID", sessionUserID))
		next.ServeHTTP(w, r)
	}
}


// Handler yang sudah aman
func GetUserProfileSecure(w http.ResponseWriter, r *http.Request) {
	requestedUserID := r.URL.Query().Get("id")
	sessionUserID := r.Context().Value("userID").(string) // Ambil dari konteks

	sessionUser, _ := users[sessionUserID]
	
	// Periksa hak akses:
	// 1. Apakah pengguna mencoba mengakses datanya sendiri?
	// 2. Apakah pengguna adalah admin?
	if requestedUserID != sessionUserID && sessionUser.Role != "admin" {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	user, ok := users[requestedUserID]
	if !ok {
		http.Error(w, "User not found", http.StatusNotFound)
		return
	}

	fmt.Fprintf(w, "User ID: %s, Name: %s", user.ID, user.Name)
}

func main() {
	http.HandleFunc("/secure/user", authMiddleware(GetUserProfileSecure))
	http.ListenAndServe(":8080", nil)
}

A02:2021 - Cryptographic Failures

Kerentanan ini berkaitan dengan kegagalan dalam melindungi data sensitif. Ini mencakup penggunaan algoritma kriptografi yang lemah, manajemen secret (kunci, password) yang buruk, atau tidak mengenkripsi data sensitif sama sekali.

Contoh Skenario: Menyimpan password pengguna dalam bentuk plain text di database atau menggunakan algoritma hashing yang sudah usang seperti MD5.

Contoh Kode (Go)

Kode Rentan:

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
)

func RegisterUser(db *sql.DB, username, password string) error {
	// KESALAHAN BESAR: Menyimpan password sebagai plain text
	_, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, password)
	return err
}

Kelemahan: Jika database bocor, semua password pengguna akan terekspos.

Kode Aman:

Gunakan algoritma hashing yang kuat dan modern seperti bcrypt atau Argon2 untuk menyimpan password.

package main

import (
	"database/sql"
	"golang.org/x/crypto/bcrypt"
	_ "github.com/mattn/go-sqlite3"
)

func RegisterUserSecure(db *sql.DB, username, password string) error {
	// Generate hash dari password dengan cost yang aman
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}

	// Simpan hash-nya, bukan password asli
	_, err = db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, string(hashedPassword))
	return err
}

func LoginUser(db *sql.DB, username, password string) bool {
	var hashedPassword string
	row := db.QueryRow("SELECT password_hash FROM users WHERE username = ?", username)
	if err := row.Scan(&hashedPassword); err != nil {
		return false // User tidak ditemukan
	}
	
	// Bandingkan password yang diberikan dengan hash yang tersimpan
	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
	return err == nil
}

A03:2021 - Injection

Injection terjadi ketika data yang tidak dipercaya dikirim ke interpreter sebagai bagian dari perintah atau query. Yang paling umum adalah SQL Injection, tetapi bisa juga terjadi pada OS Command, LDAP, atau NoSQL.

Contoh Skenario: Penyerang menyisipkan query SQL berbahaya melalui form input untuk mencuri atau memanipulasi data di database.

Contoh Kode (Go)

Kode Rentan:

package main

import (
	"database/sql"
	"fmt"
	"net/http"
	_ "github.com/mattn/go-sqlite3"
)

func GetProduct(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		category := r.URL.Query().Get("category")

		// RENTAN: Menggabungkan string untuk membuat query SQL
		query := fmt.Sprintf("SELECT name FROM products WHERE category = '%s'", category)
		rows, err := db.Query(query)
		if err != nil {
			http.Error(w, "Query failed", http.StatusInternalServerError)
			return
		}
		// ... proses hasilnya ...
	}
}

Kelemahan: Jika seorang penyerang memasukkan '; DROP TABLE users; -- pada parameter category, query-nya akan menjadi SELECT name FROM products WHERE category = ''; DROP TABLE users; --'.

Kode Aman:

Selalu gunakan parameterized queries (juga dikenal sebagai prepared statements). Dengan cara ini, input dari pengguna diperlakukan sebagai data, bukan sebagai bagian dari perintah SQL.

package main

import (
	"database/sql"
	"net/http"
	_ "github.com/mattn/go-sqlite3"
)

func GetProductSecure(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		category := r.URL.Query().Get("category")

		// AMAN: Gunakan placeholder (?) untuk input pengguna
		rows, err := db.Query("SELECT name FROM products WHERE category = ?", category)
		if err != nil {
			http.Error(w, "Query failed", http.StatusInternalServerError)
			return
		}
		// ... proses hasilnya ...
	}
}

A04:2021 - Insecure Design

Ini adalah kategori baru yang berfokus pada kelemahan pada level desain atau arsitektur aplikasi. Ini bukan kesalahan implementasi, melainkan kelemahan fundamental dalam cara sistem dirancang.

Contoh Skenario: Sebuah situs e-commerce tidak membatasi berapa kali pengguna bisa mencoba memasukkan kode diskon. Akibatnya, penyerang bisa melakukan brute-force untuk menemukan kode diskon yang valid.

Contoh Kode (Go)

Desain Rentan:

package main

import (
	"net/http"
)

var validCoupons = map[string]bool{"DISCOUNT10": true}

// Tidak ada pembatasan percobaan
func ApplyCoupon(w http.ResponseWriter, r *http.Request) {
	couponCode := r.FormValue("coupon")
	if validCoupons[couponCode] {
		w.Write([]byte("Coupon applied!"))
	} else {
		w.Write([]byte("Invalid coupon."))
	}
}

Kelemahan: Tidak ada yang menghentikan penyerang untuk mengirim ribuan permintaan dengan kode kupon yang berbeda-beda.

Desain Aman:

Terapkan rate limiting pada fungsionalitas yang sensitif.

package main

import (
	"net/http"
	"sync"
	"time"
	"golang.org/x/time/rate"
)

// Simpan rate limiter untuk setiap IP
var (
	visitors = make(map[string]*rate.Limiter)
	mu       sync.Mutex
)

func getVisitor(ip string) *rate.Limiter {
	mu.Lock()
	defer mu.Unlock()

	limiter, exists := visitors[ip]
	if !exists {
		// Izinkan 1 permintaan setiap 3 detik, dengan burst size 3
		limiter = rate.NewLimiter(rate.Every(3*time.Second), 3)
		visitors[ip] = limiter
	}
	return limiter
}

func ApplyCouponSecure(w http.ResponseWriter, r *http.Request) {
	limiter := getVisitor(r.RemoteAddr)
	if !limiter.Allow() {
		http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
		return
	}
	
	couponCode := r.FormValue("coupon")
	// ... logika validasi kupon ...
}

A05:2021 - Security Misconfiguration

Kesalahan konfigurasi keamanan adalah salah satu isu yang paling sering ditemui. Ini bisa berupa apa saja, mulai dari membiarkan cloud storage bucket terbuka untuk publik, menggunakan konfigurasi default yang tidak aman, hingga menampilkan pesan error yang terlalu detail.

Contoh Skenario: Sebuah aplikasi Go berjalan dalam mode debug di lingkungan produksi, yang mungkin membocorkan informasi sensitif melalui pesan error.

Contoh Kode (Go)

Konfigurasi Rentan:

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    // RENTAN: Menggunakan mode default (DebugMode) di produksi
    // Gin akan menampilkan detail error yang lengkap jika terjadi panic
    router := gin.Default() 

    router.GET("/some-endpoint", func(c *gin.Context) {
        // ... kode yang mungkin panic ...
    })

    router.Run(":8080")
}

Kelemahan: Jika terjadi panic, Gin akan merespons dengan stack trace lengkap, yang bisa memberikan informasi internal aplikasi kepada penyerang.

Konfigurasi Aman:

Selalu atur mode aplikasi ke ReleaseMode di lingkungan produksi.

package main

import (
    "github.com/gin-gonic/gin"
    "os"
)

func main() {
    // AMAN: Atur mode berdasarkan environment variable
    env := os.Getenv("APP_ENV")
    if env == "production" {
        gin.SetMode(gin.ReleaseMode)
    }

    router := gin.New() // Gunakan New() untuk kontrol lebih
    router.Use(gin.Logger(), gin.Recovery()) // Tambahkan middleware yang dibutuhkan

    router.GET("/some-endpoint", func(c *gin.Context) {
        // ...
    })

    router.Run(":8080")
}

A06:2021 - Vulnerable and Outdated Components

Kerentanan ini terjadi saat Anda menggunakan library, framework, atau komponen perangkat lunak lain yang memiliki kerentanan keamanan yang sudah diketahui publik.

Contoh Skenario: Menggunakan versi lama dari sebuah library Go yang ternyata memiliki kerentanan Remote Code Execution (RCE).

Contoh Kode (Go)

Ini lebih tentang manajemen dependensi daripada kode spesifik.

Praktik Rentan:

  • Tidak pernah memperbarui file go.mod dan go.sum.

  • Tidak menggunakan tool untuk memindai kerentanan pada dependensi.

Praktik Aman:

  1. Gunakan Go Modules: Go modules membantu mengelola versi dependensi.

  2. Audit Dependensi Secara Berkala: Gunakan tool seperti govulncheck (tool resmi dari Go) untuk memindai proyek Anda.

Cara Menjalankan govulncheck:

# Instal tool-nya
go install golang.org/x/vuln/cmd/govulncheck@latest

# Jalankan di direktori proyek Anda
govulncheck ./...

Tool ini akan menganalisis kode Anda dan melaporkan jika Anda menggunakan fungsi dari paket yang memiliki kerentanan yang diketahui.


A07:2021 - Identification and Authentication Failures

Ini terjadi ketika fungsi yang terkait dengan identifikasi dan autentikasi pengguna tidak diimplementasikan dengan benar, memungkinkan penyerang untuk menyamar sebagai pengguna lain.

Contoh Skenario: Sebuah aplikasi tidak menginvalidasi ID sesi setelah pengguna logout. Penyerang yang mendapatkan ID sesi tersebut bisa menggunakannya kembali untuk mengakses akun pengguna.

Contoh Kode (Go)

Implementasi Rentan:

package main

import (
	"net/http"
	"github.com/gorilla/sessions"
)

var store = sessions.NewCookieStore([]byte("super-secret-key"))

func Login(w http.ResponseWriter, r *http.Request) {
	// ... validasi username & password ...
	session, _ := store.Get(r, "session-name")
	session.Values["authenticated"] = true
	session.Values["userID"] = "123"
	session.Save(r, w)
}

func Logout(w http.ResponseWriter, r *http.Request) {
	// RENTAN: Hanya menghapus flag di sisi klien, tapi sesi mungkin masih valid
	session, _ := store.Get(r, "session-name")
	session.Values["authenticated"] = false
	session.Save(r, w)
}

Kelemahan: Sesi tidak benar-benar dihancurkan di server. Jika ID sesi bocor sebelum cookie di-overwrite, sesi itu masih bisa digunakan.

Implementasi Aman:

Invalidasi sesi sepenuhnya saat logout.

package main

import (
	"net/http"
	"github.com/gorilla/sessions"
)

var store = sessions.NewCookieStore([]byte("a-very-strong-secret-key"))

// ... fungsi Login sama ...

func LogoutSecure(w http.ResponseWriter, r *http.Request) {
	session, _ := store.Get(r, "session-name")
	
	// Hancurkan sesi sepenuhnya
	session.Options.MaxAge = -1 
	session.Save(r, w)
}

Tips Tambahan:

  • Terapkan multi-factor authentication (MFA).

  • Gunakan rate limiting pada halaman login untuk mencegah brute-force.

  • Jangan pernah mengekspos ID sesi di URL.


A08:2021 - Software and Data Integrity Failures

Kerentanan ini berfokus pada kegagalan dalam memverifikasi integritas perangkat lunak dan data. Contoh klasiknya adalah insecure deserialization.

Contoh Skenario: Sebuah aplikasi melakukan deserialization data dari sumber yang tidak terpercaya (misalnya, cookie atau input API) tanpa validasi. Penyerang bisa memanipulasi data yang diserialisasi untuk mengeksekusi kode di server.

Contoh Kode (Go)

Kode Rentan (Insecure Deserialization):

Meskipun Go tidak rentan terhadap deserialization seperti bahasa lain (Java, PHP), konsepnya tetap berlaku. Misalkan kita menggunakan gob untuk men-decode data dari cookie.

package main

import (
	"bytes"
	"encoding/gob"
	"net/http"
)

type UserPrefs struct {
	Username  string
	IsAdmin   bool // Properti sensitif
}

func LoadPrefs(w http.ResponseWriter, r *http.Request) {
	cookie, err := r.Cookie("prefs")
	if err != nil {
		//...
		return
	}

	// RENTAN: Langsung decode data dari cookie tanpa verifikasi
	var prefs UserPrefs
	decoder := gob.NewDecoder(bytes.NewReader([]byte(cookie.Value)))
	decoder.Decode(&prefs)

	if prefs.IsAdmin {
		// Berikan akses admin
	}
}

Kelemahan: Penyerang bisa membuat cookie prefs mereka sendiri, men-set IsAdmin menjadi true, lalu mengirimkannya ke server untuk mendapatkan hak akses admin.

Kode Aman:

Jangan pernah percaya data dari klien untuk keputusan keamanan. Jika perlu mengirim state, tanda tangani data tersebut menggunakan HMAC (Hash-based Message Authentication Code).

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
)

var hmacSecret = []byte("super-strong-secret-for-hmac")

func SavePrefsSecure(w http.ResponseWriter, r *http.Request) {
	// ...
	prefsData := []byte(`{"username":"alice","isAdmin":false}`)
	
	// Buat HMAC signature
	h := hmac.New(sha256.New, hmacSecret)
	h.Write(prefsData)
	signature := hex.EncodeToString(h.Sum(nil))

	// Simpan data + signature di cookie
	http.SetCookie(w, &http.Cookie{
		Name:  "prefs",
		Value: string(prefsData) + "." + signature,
	})
}

func LoadPrefsSecure(w http.ResponseWriter, r *http.Request) {
	// ... ambil cookie ...
	
	// Pisahkan data dan signature
	parts := strings.Split(cookie.Value, ".")
	if len(parts) != 2 {
		http.Error(w, "Invalid cookie", http.StatusBadRequest)
		return
	}
	data, receivedSig := parts[0], parts[1]

	// Verifikasi signature
	h := hmac.New(sha256.New, hmacSecret)
	h.Write([]byte(data))
	expectedSig := hex.EncodeToString(h.Sum(nil))

	if !hmac.Equal([]byte(receivedSig), []byte(expectedSig)) {
		http.Error(w, "Tampered cookie!", http.StatusForbidden)
		return
	}

	// Jika signature valid, baru proses datanya
	// ...
}

A09:2021 - Security Logging and Monitoring Failures

Kerentanan ini bukan tentang kode aplikasi secara langsung, tetapi tentang kurangnya kemampuan untuk mendeteksi dan merespons serangan.

Contoh Skenario: Seseorang mencoba melakukan brute-force pada halaman login, tetapi tidak ada log yang mencatat percobaan login yang gagal, sehingga tidak ada peringatan (alert) yang terpicu dan admin tidak menyadari adanya serangan.

Contoh Kode (Go)

Praktik Logging yang Buruk:

package main

import (
	"log"
	"net/http"
)

func LoginHandler(w http.ResponseWriter, r *http.Request) {
	username := r.FormValue("username")
	password := r.FormValue("password")

	// ... validasi ...
	
	if !isValid {
		// BURUK: Tidak ada log untuk kegagalan login
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		return
	}
	
	// BAIK: Login berhasil dicatat
	log.Printf("User '%s' logged in successfully", username)
	// ...
}

Praktik Logging yang Baik:

Gunakan structured logging dan catat semua peristiwa keamanan yang relevan, baik yang berhasil maupun yang gagal.

package main

import (
	"net/http"
	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.NewProduction()
	defer logger.Sync()
	
	// ...
}


func LoginHandlerSecure(logger *zap.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		username := r.FormValue("username")
		// ... validasi ...

		if !isValid {
			// BAIK: Catat kegagalan dengan konteks yang relevan
			logger.Warn("Failed login attempt",
				zap.String("username", username),
				zap.String("source_ip", r.RemoteAddr),
			)
			http.Error(w, "Invalid credentials", http.StatusUnauthorized)
			return
		}

		logger.Info("User logged in successfully",
			zap.String("username", username),
			zap.String("source_ip", r.RemoteAddr),
		)
		// ...
	}
}

A10:2021 - Server-Side Request Forgery (SSRF)

SSRF terjadi ketika penyerang bisa memaksa server untuk membuat permintaan ke lokasi yang tidak diinginkan. Ini bisa digunakan untuk memindai jaringan internal, mengakses layanan internal, atau berinteraksi dengan layanan metadata cloud.

Contoh Skenario: Sebuah fitur "import gambar dari URL" memungkinkan penyerang memasukkan URL seperti http://169.254.169.254/latest/meta-data/ (alamat metadata AWS) dan server akan mengambil data sensitif dari sana.

Contoh Kode (Go)

Kode Rentan:

package main

import (
	"io"
	"net/http"
)

// Mengambil gambar dari URL yang diberikan pengguna
func FetchImage(w http.ResponseWriter, r *http.Request) {
	imageURL := r.URL.Query().Get("url")
	
	// RENTAN: Server membuat request ke URL mana pun yang diberikan pengguna
	resp, err := http.Get(imageURL)
	if err != nil {
		http.Error(w, "Could not fetch image", http.StatusBadRequest)
		return
	}
	defer resp.Body.Close()

	io.Copy(w, resp.Body)
}

Kelemahan: Penyerang bisa memasukkan URL internal seperti http://localhost:8081/admin-stats atau file:///etc/passwd.

Kode Aman:

Terapkan allow-list untuk domain, validasi IP, dan batasi protokol yang diizinkan.

package main

import (
	"io"
	"net"
	"net/http"
	"net/url"
	"time"
	"fmt"
)

var allowedDomains = map[string]bool{
	"example.com": true,
	"images.google.com": true,
}

func FetchImageSecure(w http.ResponseWriter, r *http.Request) {
	imageURL := r.URL.Query().Get("url")

	// 1. Validasi URL
	parsedURL, err := url.ParseRequestURI(imageURL)
	if err != nil {
		http.Error(w, "Invalid URL", http.StatusBadRequest)
		return
	}

	// 2. Batasi protokol
	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
		http.Error(w, "Invalid protocol", http.StatusBadRequest)
		return
	}
	
	// 3. Terapkan allow-list domain
	if !allowedDomains[parsedURL.Hostname()] {
		http.Error(w, "Domain not allowed", http.StatusBadRequest)
		return
	}

	// 4. (Penting) Validasi alamat IP tujuan untuk mencegah bypass DNS
	ips, err := net.LookupHost(parsedURL.Hostname())
	if err != nil {
		http.Error(w, "Could not resolve host", http.StatusBadRequest)
		return
	}
	
	for _, ip := range ips {
		parsedIP := net.ParseIP(ip)
		// Blok alamat private, loopback, dan reserved lainnya
		if parsedIP.IsPrivate() || parsedIP.IsLoopback() {
			http.Error(w, "Access to internal IPs is forbidden", http.StatusBadRequest)
			return
		}
	}
	
	// 5. Gunakan http.Client yang dikonfigurasi dengan aman
	client := &http.Client{
		Timeout: 5 * time.Second,
	}
	resp, err := client.Get(imageURL)
	// ...
}

main.go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"sync"
	"time"

	"github.com/gorilla/sessions"
	_ "github.com/mattn/go-sqlite3"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/time/rate"
)

// --- GLOBAL VARIABLES & CONFIGURATION ---

var (
	db    *sql.DB
	store *sessions.CookieStore
)

// Konfigurasi rate limiter untuk mencegah serangan brute force.
// A04:2021 - Insecure Design: Menerapkan rate limiting pada fungsionalitas sensitif
// adalah contoh dari 'secure design pattern' untuk membatasi penyalahgunaan.
var (
	visitors = make(map[string]*rate.Limiter)
	mu       sync.Mutex
)

// --- MAIN APPLICATION SETUP ---

func main() {
	log.Println("Starting the secure Go application...")

	// A05:2021 - Security Misconfiguration
	// Kunci sesi tidak boleh di-hardcode. Mengambilnya dari environment variable
	// adalah praktik yang aman. Aplikasi harus gagal start jika kunci tidak ada.
	sessionKey := os.Getenv("SESSION_KEY")
	if sessionKey == "" {
		log.Fatal("FATAL: SESSION_KEY environment variable not set. Please set a long, random string.")
	}
	store = sessions.NewCookieStore([]byte(sessionKey))

	// A05:2021 - Security Misconfiguration & A07:2021 - Identification and Authentication Failures
	// Konfigurasi cookie sesi dengan atribut keamanan yang kuat.
	// MaxAge: Sesi akan kedaluwarsa setelah 1 jam.
	// HttpOnly: Mencegah akses cookie dari JavaScript (mitigasi XSS).
	// Secure: Hanya mengirim cookie melalui HTTPS (di-disable untuk development lokal).
	// SameSite: Mitigasi serangan Cross-Site Request Forgery (CSRF).
	store.Options = &sessions.Options{
		Path:     "/",
		MaxAge:   3600, // 1 jam
		HttpOnly: true,
		Secure:   false, // Set `true` di produksi dengan HTTPS
		SameSite: http.SameSiteLaxMode,
	}

	// Inisialisasi database
	var err error
	db, err = sql.Open("sqlite3", "./secureapp.db")
	if err != nil {
		log.Fatalf("Failed to open database: %v", err)
	}
	defer db.Close()
	initDB()

	// A06:2021 - Vulnerable and Outdated Components
	// CATATAN PENTING: Keamanan komponen adalah tentang proses, bukan kode.
	// Pastikan untuk secara rutin memindai dependensi Anda.
	// Jalankan perintah berikut di terminal Anda untuk memeriksa kerentanan:
	// `go install golang.org/x/vuln/cmd/govulncheck@latest`
	// `govulncheck ./...`
	// Ini akan memberitahu Anda jika library yang Anda gunakan (seperti go-sqlite3 atau gorilla/sessions)
	// memiliki kerentanan yang diketahui.

	// Setup HTTP server dan routing
	mux := http.NewServeMux()
	mux.HandleFunc("/register", registerHandler)
	mux.HandleFunc("/login", loginHandler)
	mux.HandleFunc("/logout", logoutHandler)

	// Endpoint yang memerlukan autentikasi
	mux.Handle("/profile", authMiddleware(http.HandlerFunc(viewProfileHandler)))
	mux.Handle("/profile/notes", authMiddleware(http.HandlerFunc(addNoteHandler)))
	
	// Endpoint untuk demonstrasi SSRF yang aman
	mux.Handle("/fetch-avatar", authMiddleware(http.HandlerFunc(fetchAvatarHandler)))


	log.Println("Server is running on http://localhost:8080")
	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

// initDB membuat tabel yang diperlukan jika belum ada.
func initDB() {
	// A03:2021 - Injection
	// Meskipun ini adalah query DDL (Data Definition Language), membiasakan diri untuk tidak
	// membuat query dari input pengguna adalah praktik yang baik. Query ini aman karena statis.
	createUsersTable := `
	CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		username TEXT NOT NULL UNIQUE,
		password_hash TEXT NOT NULL,
		role TEXT NOT NULL
	);`
	createNotesTable := `
	CREATE TABLE IF NOT EXISTS notes (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		user_id INTEGER NOT NULL,
		content TEXT NOT NULL,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
		FOREIGN KEY(user_id) REFERENCES users(id)
	);`

	if _, err := db.Exec(createUsersTable); err != nil {
		log.Fatalf("Failed to create users table: %v", err)
	}
	if _, err := db.Exec(createNotesTable); err != nil {
		log.Fatalf("Failed to create notes table: %v", err)
	}
}

// --- MIDDLEWARE ---

// authMiddleware memeriksa apakah pengguna sudah login sebelum mengakses sebuah endpoint.
func authMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		session, _ := store.Get(r, "secure-session")
		auth, ok := session.Values["authenticated"].(bool)

		if !ok || !auth {
			http.Error(w, "Forbidden", http.StatusForbidden)
			// A09:2021 - Security Logging and Monitoring Failures
			// Catat percobaan akses tidak sah. Ini penting untuk mendeteksi anomali.
			log.Printf("WARN: Unauthorized access attempt to %s from %s", r.URL.Path, r.RemoteAddr)
			return
		}

		// Teruskan ID pengguna dan role melalui context agar aman dan mudah diakses di handler selanjutnya.
		ctx := context.WithValue(r.Context(), "userID", session.Values["userID"])
		ctx = context.WithValue(ctx, "userRole", session.Values["userRole"])
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// --- HANDLERS ---

func registerHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	username := r.FormValue("username")
	password := r.FormValue("password")

	// A02:2021 - Cryptographic Failures
	// Selalu hash password sebelum menyimpannya. bcrypt adalah standar yang kuat.
	// Jangan pernah menyimpan password dalam bentuk plain text.
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		// A05:2021 - Security Misconfiguration
		// Jangan bocorkan detail error internal ke pengguna.
		log.Printf("ERROR: Failed to hash password for user %s: %v", username, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	
	// A03:2021 - Injection
	// Gunakan parameterized query (prepared statements) untuk mencegah SQL Injection.
	// Tanda tanya (?) adalah placeholder yang aman untuk input dari pengguna.
	_, err = db.Exec("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)", username, string(hashedPassword), "user")
	if err != nil {
		log.Printf("ERROR: Failed to register user %s: %v", username, err)
		http.Error(w, "Could not register user", http.StatusInternalServerError)
		return
	}

	// A09:2021 - Security Logging and Monitoring Failures
	// Catat event penting seperti registrasi pengguna baru.
	log.Printf("INFO: New user registered: %s", username)
	fmt.Fprintf(w, "User %s registered successfully", username)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	
	// A04:2021 - Insecure Design
	// Terapkan rate limiting pada endpoint login untuk mencegah brute-force.
	limiter := getVisitor(r.RemoteAddr)
	if !limiter.Allow() {
		http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
		return
	}

	username := r.FormValue("username")
	password := r.FormValue("password")

	var id int
	var hashedPassword string
	var role string
	
	// A03:2021 - Injection
	// Lagi, gunakan parameterized query untuk keamanan.
	row := db.QueryRow("SELECT id, password_hash, role FROM users WHERE username = ?", username)
	err := row.Scan(&id, &hashedPassword, &role)

	if err != nil || bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) != nil {
		// A09:2021 - Security Logging and Monitoring Failures
		// Sangat penting untuk mencatat kegagalan login. Ini adalah indikator utama serangan.
		log.Printf("WARN: Failed login attempt for user '%s' from %s", username, r.RemoteAddr)
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		return
	}

	// Login berhasil
	session, _ := store.Get(r, "secure-session")
	session.Values["authenticated"] = true
	session.Values["userID"] = id
	session.Values["userRole"] = role
	
	// A08:2021 - Software and Data Integrity Failures
	// gorilla/sessions secara otomatis menandatangani (sign) data cookie dengan HMAC
	// menggunakan session key yang kita berikan. Ini mencegah penyerang memodifikasi
	// isi cookie (seperti mengubah userID atau role) tanpa terdeteksi.
	if err := session.Save(r, w); err != nil {
		log.Printf("ERROR: Failed to save session: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	log.Printf("INFO: User '%s' (ID: %d) logged in successfully from %s", username, id, r.RemoteAddr)
	fmt.Fprintf(w, "Welcome %s", username)
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
	session, _ := store.Get(r, "secure-session")

	// A07:2021 - Identification and Authentication Failures
	// Proses logout yang benar adalah dengan menghancurkan sesi, bukan hanya
	// menghapus flag. Mengatur MaxAge ke -1 akan memberitahu browser untuk
	// segera menghapus cookie sesi.
	session.Values["authenticated"] = false
	session.Options.MaxAge = -1
	session.Save(r, w)
	
	log.Printf("INFO: User logged out")
	fmt.Fprintf(w, "Logged out successfully")
}

func viewProfileHandler(w http.ResponseWriter, r *http.Request) {
	// Ambil ID pengguna yang profilnya ingin dilihat dari query parameter
	// Contoh: /profile?id=2
	profileIDStr := r.URL.Query().Get("id")
	profileID, _ := strconv.Atoi(profileIDStr)
	
	// Ambil data pengguna yang sedang login dari context
	sessionUserID := r.Context().Value("userID").(int)
	sessionUserRole := r.Context().Value("userRole").(string)

	// A01:2021 - Broken Access Control
	// Ini adalah logika kontrol akses yang paling penting.
	// Pengguna hanya boleh melihat profilnya sendiri, KECUALI jika dia adalah admin.
	if sessionUserID != profileID && sessionUserRole != "admin" {
		// A09:2021 - Security Logging and Monitoring Failures
		// Catat percobaan pelanggaran kontrol akses.
		log.Printf("WARN: Access control violation. User %d tried to access profile %d.", sessionUserID, profileID)
		http.Error(w, "You are not authorized to view this profile", http.StatusForbidden)
		return
	}

	var username string
	// A03:2021 - Injection
	// Selalu gunakan parameterized query.
	err := db.QueryRow("SELECT username FROM users WHERE id = ?", profileID).Scan(&username)
	if err != nil {
		http.Error(w, "User not found", http.StatusNotFound)
		return
	}

	fmt.Fprintf(w, "<h1>Profile for %s</h1>", username)
	
	// Tampilkan catatan milik pengguna
	rows, err := db.Query("SELECT content, created_at FROM notes WHERE user_id = ?", profileID)
	if err != nil {
		log.Printf("ERROR: could not fetch notes for user %d: %v", profileID, err)
		http.Error(w, "Internal Server Error", 500)
		return
	}
	defer rows.Close()

	fmt.Fprintf(w, "<h3>Notes:</h3><ul>")
	for rows.Next() {
		var content, createdAt string
		rows.Scan(&content, &createdAt)
		fmt.Fprintf(w, "<li>%s (at %s)</li>", content, createdAt)
	}
	fmt.Fprintf(w, "</ul>")
}

func addNoteHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	
	sessionUserID := r.Context().Value("userID").(int)
	noteContent := r.FormValue("note")

	// A01:2021 - Broken Access Control (Implisit)
	// Karena kita mengambil user ID dari sesi yang terotentikasi, kita memastikan
	// pengguna hanya bisa menambahkan catatan untuk akunnya sendiri.
	// A03:2021 - Injection
	// Gunakan parameterized query untuk menyimpan catatan.
	_, err := db.Exec("INSERT INTO notes (user_id, content) VALUES (?, ?)", sessionUserID, noteContent)
	if err != nil {
		log.Printf("ERROR: Failed to add note for user %d: %v", sessionUserID, err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	log.Printf("INFO: User %d added a new note.", sessionUserID)
	fmt.Fprintf(w, "Note added successfully!")
}

func fetchAvatarHandler(w http.ResponseWriter, r *http.Request) {
	avatarURL := r.URL.Query().Get("url")

	// A10:2021 - Server-Side Request Forgery (SSRF)
	// Menerapkan serangkaian validasi ketat pada URL yang diberikan pengguna
	// untuk mencegah server membuat permintaan ke sumber daya internal atau berbahaya.
	
	// 1. Validasi URL harus valid
	parsedURL, err := url.ParseRequestURI(avatarURL)
	if err != nil {
		http.Error(w, "Invalid URL format", http.StatusBadRequest)
		return
	}

	// 2. Batasi hanya protokol HTTP dan HTTPS
	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
		http.Error(w, "Protocol not allowed. Only http and https are permitted.", http.StatusBadRequest)
		return
	}

	// 3. Lakukan resolusi DNS dan periksa IP
	ips, err := net.LookupHost(parsedURL.Hostname())
	if err != nil {
		http.Error(w, "Could not resolve hostname", http.StatusBadRequest)
		return
	}

	for _, ipStr := range ips {
		ip := net.ParseIP(ipStr)
		// Blokir semua alamat IP yang tidak global (private, loopback, dll.)
		if !ip.IsGlobalUnicast() {
			log.Printf("WARN: SSRF attempt blocked. URL %s resolved to non-global IP %s", avatarURL, ipStr)
			http.Error(w, "Request to internal or private IP addresses is forbidden.", http.StatusBadRequest)
			return
		}
	}

	// 4. Buat permintaan dengan timeout yang wajar
	client := &http.Client{
		Timeout: 5 * time.Second,
	}

	resp, err := client.Get(avatarURL)
	if err != nil {
		http.Error(w, "Failed to fetch avatar", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	// 5. Salin respons ke pengguna
	w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
	w.WriteHeader(http.StatusOK)
	if _, err := io.Copy(w, resp.Body); err != nil {
        log.Printf("ERROR: Failed to write avatar response: %v", err)
    }
}


// --- HELPER FUNCTIONS ---

// getVisitor mengelola rate limiter untuk setiap alamat IP.
func getVisitor(ip string) *rate.Limiter {
	mu.Lock()
	defer mu.Unlock()

	limiter, exists := visitors[ip]
	if !exists {
		// Izinkan 1 permintaan setiap 2 detik, dengan burst size 4.
		limiter = rate.NewLimiter(rate.Every(2*time.Second), 4)
		visitors[ip] = limiter
	}
	return limiter
}

/*
--- CARA MENJALANKAN APLIKASI INI ---

1.  Simpan kode ini sebagai `main.go`.

2.  Buka terminal di direktori yang sama dan inisialisasi Go modules:
    `go mod init secureapp`

3.  Tambahkan dependensi yang dibutuhkan:
    `go mod tidy`

4.  Set environment variable untuk kunci sesi (GANTI DENGAN STRING ACAK ANDA SENDIRI):
    - Di Linux/macOS:
      `export SESSION_KEY='a-very-long-and-random-string-for-your-sessions'`
    - Di Windows (Command Prompt):
      `set SESSION_KEY="a-very-long-and-random-string-for-your-sessions"`
    - Di Windows (PowerShell):
      `$env:SESSION_KEY="a-very-long-and-random-string-for-your-sessions"`

5.  Jalankan aplikasi:
    `go run .`

6.  Gunakan `curl` atau Postman untuk berinteraksi dengan API:
    - Register: `curl -X POST -d "username=alice" -d "password=pass123" http://localhost:8080/register`
    - Login: `curl -X POST -d "username=alice" -d "password=pass123" http://localhost:8080/login --cookie-jar cjar`
    - Lihat profil (butuh cookie): `curl http://localhost:8080/profile?id=1 --cookie cjar`
    - Tambah catatan: `curl -X POST -d "note=Ini catatan pertama saya" http://localhost:8080/profile/notes --cookie cjar`
    - Logout: `curl http://localhost:8080/logout --cookie cjar`

*/

Last updated