Authorization #
Authorization menjawab pertanyaan yang berbeda dari authentication: bukan siapa kamu, tapi apa yang boleh kamu lakukan. Setelah sistem memverifikasi identitas pengguna, sistem harus memutuskan apakah identitas itu memiliki hak untuk melakukan operasi yang diminta — membaca data tertentu, memodifikasi resource, mengakses halaman admin, atau menghapus entri.
Banyak sistem yang autentikasinya kuat tapi authorizationnya lemah. Mereka memastikan bahwa hanya user yang sudah login bisa mengakses API — tapi lupa memverifikasi apakah user yang sudah login itu berhak mengakses resource spesifik yang diminta. Hasilnya adalah celah yang disebut Insecure Direct Object Reference (IDOR): user A bisa mengakses data milik user B hanya dengan mengubah angka di URL.
Authorization yang benar bukan hanya tentang role dan permission di level tinggi. Ia tentang memastikan setiap operasi, pada setiap resource, divalidasi terhadap identitas yang melakukannya — di level yang paling granular.
Model Authorization: Memilih Pendekatan yang Tepat #
Ada beberapa model authorization yang umum digunakan, masing-masing cocok untuk konteks yang berbeda. Memilih yang tepat sangat bergantung pada kompleksitas kebutuhan bisnis.
Role-Based Access Control (RBAC) #
RBAC adalah model paling umum: user diberi satu atau beberapa role, dan role menentukan permission yang dimiliki. Mudah dipahami, mudah diimplementasikan, cocok untuk sebagian besar aplikasi.
Struktur RBAC:
flowchart LR
User["User"] -->|"memiliki"| Role["Role(s)"]
Role -->|"memiliki"| Permission["Permission(s)"]
Contoh:
User Ali → Role: "editor"
Role "editor" → Permissions: ["articles:create", "articles:edit_own",
"articles:publish", "comments:moderate"]
User Budi → Role: "viewer"
Role "viewer" → Permissions: ["articles:read", "comments:read"]
User Admin → Role: "admin"
Role "admin" → Permissions: ["*"] (semua permission)// Implementasi RBAC yang bersih dan dapat dikembangkan
type Permission string
const (
ArticlesRead Permission = "articles:read"
ArticlesCreate Permission = "articles:create"
ArticlesEditOwn Permission = "articles:edit_own"
ArticlesEditAny Permission = "articles:edit_any"
ArticlesDeleteOwn Permission = "articles:delete_own"
ArticlesDeleteAny Permission = "articles:delete_any"
ArticlesPublish Permission = "articles:publish"
UsersRead Permission = "users:read"
UsersManage Permission = "users:manage"
AdminAccess Permission = "admin:access"
)
// Role definitions — single source of truth
var rolePermissions = map[string]map[Permission]bool{
"viewer": {
ArticlesRead: true,
UsersRead: true,
},
"author": {
ArticlesRead: true,
ArticlesCreate: true,
ArticlesEditOwn: true,
ArticlesDeleteOwn: true,
UsersRead: true,
},
"editor": {
ArticlesRead: true,
ArticlesCreate: true,
ArticlesEditOwn: true,
ArticlesEditAny: true,
ArticlesDeleteOwn: true,
ArticlesPublish: true,
UsersRead: true,
},
"admin": allPermissions(), // semua permission
}
// allPermissions: semua permission yang dikenal (setara "p for p in Permission")
func allPermissions() map[Permission]bool {
return map[Permission]bool{
ArticlesRead: true, ArticlesCreate: true, ArticlesEditOwn: true,
ArticlesEditAny: true, ArticlesDeleteOwn: true, ArticlesDeleteAny: true,
ArticlesPublish: true, UsersRead: true, UsersManage: true, AdminAccess: true,
}
}
func getUserPermissions(user User) map[Permission]bool {
// Kumpulkan semua permission dari semua role user
permissions := map[Permission]bool{}
for _, role := range user.Roles {
for p := range rolePermissions[role] {
permissions[p] = true
}
}
return permissions
}
func hasPermission(user User, permission Permission) bool {
return getUserPermissions(user)[permission]
}
// Middleware untuk endpoint
func requirePermission(permission Permission) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !currentUser(r).IsAuthenticated {
writeJSON(w, http.StatusUnauthorized,
map[string]string{"error": "Authentication diperlukan"})
return
}
if !hasPermission(currentUser(r), permission) {
writeJSON(w, http.StatusForbidden,
map[string]string{"error": "Permission tidak mencukupi"})
return
}
next.ServeHTTP(w, r)
})
}
}
// Penggunaan di endpoint
http.Handle("/admin/users", requirePermission(UsersManage)(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, listUsers())
})))
http.Handle("/articles", requirePermission(ArticlesCreate)(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// ...
})))
Attribute-Based Access Control (ABAC) #
ABAC membuat keputusan berdasarkan kombinasi atribut: atribut user (department, level, lokasi), atribut resource (classification, owner, status), dan atribut environment (waktu, IP, device). Lebih fleksibel dari RBAC tapi lebih kompleks.
// ABAC — keputusan berdasarkan atribut, bukan hanya role
func canAccessDocument(user User, doc Document, action string, ctx map[string]any) (bool, string) {
// Evaluasi apakah user boleh melakukan action pada document
// berdasarkan kombinasi atribut.
if ctx == nil {
ctx = map[string]any{}
}
// Policy: dokumen confidential hanya bisa dibaca oleh
// user dari departemen yang sama atau level manager ke atas
if doc.Classification == "confidential" && action == "read" {
if user.Department != doc.Department {
if user.Level < "manager" {
return false, "Dokumen confidential terbatas pada departemen"
}
}
}
// Policy: dokumen hanya bisa diedit oleh pemilik atau editor
if action == "edit" {
if user.ID != doc.OwnerID {
if !contains(user.Roles, "editor") {
return false, "Hanya pemilik atau editor yang bisa mengedit"
}
}
}
// Policy: tidak ada yang bisa menghapus dokumen yang sudah dipublish
// kecuali admin
if action == "delete" && doc.Status == "published" {
if !contains(user.Roles, "admin") {
return false, "Dokumen yang sudah dipublish tidak bisa dihapus"
}
}
// Policy: akses di luar jam kerja hanya untuk dokumen non-sensitif
if hour, ok := ctx["hour"].(int); ok && (hour < 8 || hour > 18) {
if doc.Classification == "confidential" || doc.Classification == "restricted" {
return false, "Dokumen sensitif tidak bisa diakses di luar jam kerja"
}
}
return true, ""
}
Relationship-Based Access Control (ReBAC) #
ReBAC menentukan akses berdasarkan relasi antar entitas — cocok untuk sistem di mana permission mengikuti struktur hierarki atau graph. Google Zanzibar (yang dipakai Google Drive, YouTube, dll) adalah implementasi ReBAC yang terkenal.
Contoh ReBAC untuk sistem berbagi dokumen:
flowchart LR
doc_report["doc:report"] -->|"owner"| user_ali["user:ali"]
doc_report -->|"editor"| user_budi["user:budi"]
doc_report -->|"viewer"| group_marketing["group:marketing"]
group_marketing -->|"member"| user_citra["user:citra"]Pertanyaan: bisakah user:citra melihat doc:report?
Traversal graph:
flowchart LR
user_citra["user:citra"] -->|"member of"| group_marketing["group:marketing"]
group_marketing -->|"viewer of"| doc_report["doc:report"]
→ Ya, citra bisa melihat (viewer access via group membership)
Pertanyaan: bisakah user:budi mengedit doc:report?
user:budi ──editor of──→ doc:report
→ Ya, langsung
Pertanyaan: bisakah user:budi menghapus doc:report?
user:budi hanya editor, bukan owner
→ Tidak (delete butuh owner atau admin)Prinsip Paling Penting: Deny by Default #
Deny by default berarti: jika tidak ada aturan yang secara eksplisit mengizinkan, akses ditolak. Ini adalah prinsip yang paling fundamental dan paling sering dilanggar dalam implementasi authorization.
// ANTI-PATTERN: allow by default (izinkan kecuali ada yang melarang)
func canAccess(user User, resource Resource) bool {
if user.IsBanned {
return false
}
if resource.IsPrivate && user.ID != resource.OwnerID {
return false
}
return true // default: izinkan
}
// Masalah: jika ada kondisi baru yang belum di-handle (resource baru,
// new role, new status), the default is to ALLOW.
// Bug baru → celah keamanan baru
// BENAR: deny by default
func canAccessSafe(user User, resource Resource) bool {
// Hanya izinkan jika ADA kondisi yang eksplisit mengizinkan
if resource.IsPublic {
return true
}
if user.ID == resource.OwnerID {
return true
}
if contains(user.Roles, "admin") {
return true
}
if resource.SharedWith != nil && contains(resource.SharedWith, user.ID) {
return true
}
return false // default: TOLAK
}
// Dengan deny by default:
// Resource baru yang belum ada aturannya → otomatis tertolak
// Role baru yang belum dikonfigurasi → tidak punya akses apapun
// Bug baru → gagal aman (fail secure), bukan gagal terbuka
flowchart TD
A[Request masuk] --> B{User terautentikasi?}
B --> |Tidak| C[401 Unauthorized]
B --> |Ya| D{"Ada rule eksplisit\nyang mengizinkan?"}
D --> |Tidak| E["403 Forbidden\nDeny by Default"]
D --> |Ya — cek semua kondisi| F{"Semua kondisi\nterpenuhi?"}
F --> |Tidak| E
F --> |Ya| G[Proses request ✓]
G --> H["Audit log:\nSiapa, apa, kapan, resource mana"]IDOR: Celah Authorization yang Paling Umum #
Insecure Direct Object Reference (IDOR) adalah ketika aplikasi mengekspos identifier internal (ID database, path file) dan tidak memvalidasi bahwa user yang meminta berhak mengakses resource dengan identifier tersebut.
// ANTI-PATTERN: tidak ada validasi ownership
// GET /orders/{orderID}
func getOrder(w http.ResponseWriter, r *http.Request) {
orderID := parseID(r)
order := findOrderByID(orderID)
writeJSON(w, http.StatusOK, order)
// User bisa mengakses order siapapun hanya dengan mengubah order_id:
// GET /orders/1001 → GET /orders/1002 → GET /orders/1003
}
// ANTI-PATTERN yang lebih halus: hanya cek role, tidak cek ownership
// PUT /orders/{orderID}
func updateOrder(w http.ResponseWriter, r *http.Request) {
orderID := parseID(r)
order := findOrderByID(orderID)
// Permission EDIT_OWN ada, tapi tidak dicek apakah ini MILIKNYA!
applyOrderUpdate(order, r)
writeJSON(w, http.StatusOK, order)
}
// BENAR: validasi ownership secara eksplisit
func getOrderSafe(w http.ResponseWriter, r *http.Request) {
orderID := parseID(r)
order := findOrderByID(orderID)
// Cek ownership SEBELUM mengembalikan data
if order.UserID != currentUser(r).ID {
// Admin bisa lihat semua — cek apakah user adalah admin
if !hasPermission(currentUser(r), PermissionOrdersReadAny) {
http.Error(w, "Forbidden — ini bukan punyamu", http.StatusForbidden)
return
}
}
writeJSON(w, http.StatusOK, order)
}
// Pattern yang lebih bersih: scoped query
func getOrderScoped(w http.ResponseWriter, r *http.Request) {
orderID := parseID(r)
var order *Order
if hasPermission(currentUser(r), PermissionOrdersReadAny) {
order = findOrderByID(orderID)
} else {
// Query sudah dibatasi oleh user_id — tidak mungkin dapat order orang lain
order = findOrderByIDAndUser(orderID, currentUser(r).ID)
}
writeJSON(w, http.StatusOK, order)
}
Pola IDOR yang umum dan cara mitigasinya:
IDOR via ID numerik:
GET /users/123 → bisa akses /users/124
✓ Mitigasi: validasi ownership, atau gunakan UUID
IDOR via file path:
GET /files/uploads/invoice_ali.pdf
→ coba /files/uploads/invoice_budi.pdf
✓ Mitigasi: simpan metadata di DB, serve file melalui endpoint
yang validasi ownership, jangan expose path langsung
IDOR via URL yang predictable:
/reports/monthly-2025-01.pdf
→ /reports/monthly-2025-02.pdf
✓ Mitigasi: generate signed URL atau token yang bound ke user
IDOR via API yang mengembalikan semua data:
GET /api/users → mengembalikan semua user (harusnya hanya profile sendiri)
✓ Mitigasi: scoped query berdasarkan authenticated user
Least Privilege: Hanya Berikan yang Diperlukan #
Principle of Least Privilege (PoLP) menyatakan bahwa setiap entitas (user, service, proses) hanya boleh memiliki akses minimum yang dibutuhkan untuk menjalankan tugasnya — tidak lebih.
Least privilege diterapkan di semua level:
Level user:
→ Editor tidak perlu bisa menghapus user
→ Staff tidak perlu bisa melihat data keuangan
→ API consumer tidak perlu akses ke semua endpoint
Level service (microservices):
→ Service A hanya bisa membaca, tidak menulis ke database B
→ Service notifikasi tidak perlu akses ke data pembayaran
→ Setiap service punya credential database sendiri dengan hak minimal
Level database (seperti dibahas di SQL Injection):
→ App user: SELECT, INSERT, UPDATE, DELETE saja
→ Migration user: + CREATE, ALTER, DROP
→ Backup user: SELECT saja
Level API key:
→ Berikan scope yang spesifik: "read:orders" bukan "full_access"
→ Kunci untuk environment: production key tidak bisa dipakai di staging
// Implementasi scoped API key
type APIKey struct {
ID int
KeyHash string
Name string
UserID int
Scopes []string // ["read:orders", "write:orders"]
ExpiresAt time.Time
LastUsedAt time.Time
CreatedAt time.Time
}
func validateAPIKeyScope(apiKey APIKey, requiredScope string) bool {
// Cek apakah API key memiliki scope yang diperlukan.
if !apiKey.ExpiresAt.IsZero() && apiKey.ExpiresAt.Before(time.Now()) {
return false
}
// Exact match atau wildcard
prefix := strings.Split(requiredScope, ":")[0] + ":*"
for _, scope := range apiKey.Scopes {
if scope == requiredScope || scope == "*" || scope == prefix {
return true
}
}
return false
}
// Middleware untuk endpoint yang butuh scope tertentu
func requireAPIScope(scope string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKey := getCurrentAPIKey(r)
if apiKey == nil {
writeJSON(w, http.StatusUnauthorized,
map[string]string{"error": "API key diperlukan"})
return
}
if !validateAPIKeyScope(*apiKey, scope) {
writeJSON(w, http.StatusForbidden,
map[string]string{"error": "Scope yang diperlukan tidak ada: " + scope})
return
}
next.ServeHTTP(w, r)
})
}
}
// Penggunaan di endpoint
// http.Handle("/api/orders", requireAPIScope("read:orders")(http.HandlerFunc(
// func(w http.ResponseWriter, r *http.Request) {
// // Only API keys with the "read:orders" or "read:*" scope can access
// })))
Authorization di Level Resource: Ownership Validation Pattern #
Ketika permission bergantung pada relasi antara user dan resource (bukan hanya role global), ownership validation harus menjadi pola standar.
// Abstraksi ownership validation yang reusable
type ResourceAccessPolicy interface {
CanRead(user User, resource any) bool
CanEdit(user User, resource any) bool
CanDelete(user User, resource any) bool
}
type ArticleAccessPolicy struct{}
func (ArticleAccessPolicy) CanRead(user User, article *Article) bool {
if article.Status == "published" {
return true // published articles are readable by anyone
}
return article.AuthorID == user.ID || hasPermission(user, PermissionArticlesEditAny)
}
func (ArticleAccessPolicy) CanEdit(user User, article *Article) bool {
if hasPermission(user, PermissionArticlesEditAny) {
return true
}
if hasPermission(user, PermissionArticlesEditOwn) {
return article.AuthorID == user.ID
}
return false
}
func (ArticleAccessPolicy) CanDelete(user User, article *Article) bool {
if hasPermission(user, PermissionArticlesDeleteAny) {
return true
}
if hasPermission(user, PermissionArticlesDeleteOwn) {
return article.AuthorID == user.ID
}
return false
}
func (ArticleAccessPolicy) CanPublish(user User, article *Article) bool {
return hasPermission(user, PermissionArticlesPublish)
}
// Policy registry
var policies = map[reflect.Type]ResourceAccessPolicy{
reflect.TypeOf(Article{}): ArticleAccessPolicy{},
}
func authorize(user User, resource any, action string) bool {
// Cek apakah user boleh melakukan action pada resource.
policy, ok := policies[reflect.TypeOf(resource)]
if !ok {
return false // Deny by default jika tidak ada policy
}
switch action {
case "read":
return policy.CanRead(user, resource)
case "edit":
return policy.CanEdit(user, resource)
case "delete":
return policy.CanDelete(user, resource)
default:
return false // Action tidak dikenal → tolak
}
}
// Penggunaan di endpoint
// PUT /articles/{articleID}
func updateArticle(w http.ResponseWriter, r *http.Request) {
article := findArticleByID(parseID(r))
if !authorize(currentUser(r), article, "edit") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
applyArticleUpdate(article, r)
writeJSON(w, http.StatusOK, article)
}
Privilege Escalation: Celah yang Sering Terlewat #
Privilege escalation terjadi ketika user mendapatkan akses lebih tinggi dari yang seharusnya — entah karena desain yang salah atau karena bug dalam logika authorization.
// ANTI-PATTERN: endpoint yang memungkinkan user mengubah role sendiri
// PUT /users/{userID}
func updateUser(w http.ResponseWriter, r *http.Request) {
userID := parseID(r)
if userID != currentUser(r).ID {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
user := findUserByID(userID)
applyUserUpdate(user, r) // ← users can send {"roles": ["admin"]}!
saveUser(user)
writeJSON(w, http.StatusOK, user)
}
// BENAR: field yang bisa diubah oleh user dibatasi secara eksplisit
func updateUserSafe(w http.ResponseWriter, r *http.Request) {
userID := parseID(r)
if userID != currentUser(r).ID {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
user := findUserByID(userID)
// Whitelist field yang boleh diubah user biasa
allowedFields := map[string]bool{"name": true, "bio": true,
"avatar_url": true, "preferences": true}
updateData := filterFields(body(r), allowedFields)
// Field sensitif hanya bisa diubah oleh admin
if hasPermission(currentUser(r), PermissionUsersManage) {
sensitiveFields := map[string]bool{"roles": true,
"is_active": true, "email_verified": true}
updateData = merge(updateData, filterFields(body(r), sensitiveFields))
}
applyUserUpdate(user, updateData)
saveUser(user)
writeJSON(w, http.StatusOK, user)
}
Pola privilege escalation yang umum:
Mass assignment — objek model di-update langsung dari request body
✓ Mitigasi: whitelist field yang boleh diupdate
Role manipulation — endpoint perubahan profile tidak filter field roles
✓ Mitigasi: roles dan permission hanya bisa diubah oleh admin
Indirect escalation — user membuat entity yang mewarisi permission tinggi
Contoh: user membuat group lalu menambahkan diri ke group admin
✓ Mitigasi: validasi permission saat membuat relasi, bukan hanya saat read
Token scope inflation — JWT yang bisa di-modify
✓ Mitigasi: verifikasi signature JWT, jangan percaya payload tanpa verifikasi
Authorization Logging dan Audit Trail #
Setiap keputusan authorization yang signifikan harus di-log — bukan hanya untuk debugging, tapi untuk compliance, forensik, dan deteksi serangan.
import "log/slog"
var securityLogger = slog.Default().With("logger", "security.authorization")
func logAuthorizationDecision(
userID int,
action, resourceType, resourceID string,
granted bool,
reason string,
) {
// Log setiap keputusan authorization.
securityLogger.Info("Keputusan authorization",
"user_id", userID,
"action", action,
"resource_type", resourceType,
"resource_id", resourceID,
"granted", granted,
"reason", reason,
"timestamp", time.Now().UTC().Format(time.RFC3339),
"request_id", gRequestID,
"ip", remoteIP,
)
}
// Contoh penggunaan di policy
func (ArticleAccessPolicy) CanDelete(user User, article *Article) bool {
if hasPermission(user, PermissionArticlesDeleteAny) {
logAuthorizationDecision(user.ID, "delete", "article", article.ID, true,
"punya permission articles:delete_any")
return true
}
if hasPermission(user, PermissionArticlesDeleteOwn) && article.AuthorID == user.ID {
logAuthorizationDecision(user.ID, "delete", "article", article.ID, true,
"owner dengan permission articles:delete_own")
return true
}
logAuthorizationDecision(user.ID, "delete", "article", article.ID, false,
"permission tidak mencukupi")
return false
}
Apa yang perlu di-log untuk audit trail yang efektif:
Setiap akses ke resource sensitif (data keuangan, data personal)
Setiap keputusan "ditolak" — bisa jadi probe atau serangan
Setiap perubahan permission atau role
Setiap aksi admin yang signifikan (hapus user, ubah konfigurasi)
Format yang berguna untuk analisis:
{
"timestamp": "2025-06-01T14:23:45Z",
"event_type": "authorization",
"user_id": 42,
"user_email": "[email protected]",
"action": "delete",
"resource_type": "article",
"resource_id": "1234",
"granted": false,
"reason": "not owner, missing articles:delete_any",
"ip": "203.x.x.x",
"request_id": "req_abc123"
}
Anti-Pattern yang Harus Dihindari #
// ✗ Anti-pattern 1: authorization hanya di frontend
// Role check hanya di frontend (mis. menyembunyikan tombol atau route) bisa
// dibypass dengan memanipulasi client.
// JavaScript bisa dimanipulasi → ganti role di console → akses admin panel
// Backend harus SELALU validasi authorization, tidak hanya frontend
// ✗ Anti-pattern 2: menggunakan ID yang mudah ditebak tanpa ownership check
// GET /invoices/{invoiceID}/pdf
func downloadInvoice(w http.ResponseWriter, r *http.Request) {
invoice := findInvoiceByID(parseID(r))
sendFile(w, invoice.PDFPath)
// User bisa mencoba /invoices/1, /invoices/2, /invoices/3 untuk download semua invoice
}
// ✓ Solusi: validasi ownership ATAU gunakan signed URL
func downloadInvoiceSafe(w http.ResponseWriter, r *http.Request) {
// hanya invoice milik user ini
invoice := findInvoiceByIDAndUser(parseID(r), currentUser(r).ID)
sendFile(w, invoice.PDFPath)
}
// ────────────────────────────────────────────────────────────────────────────
// ✗ Anti-pattern 3: permission check di beberapa tempat yang tidak konsisten
// Di controller A: cek apakah user adalah owner
// Di controller B (endpoint berbeda tapi resource sama): lupa cek
// → IDOR di controller B
// ✓ Solusi: sentralisasi authorization di policy object atau service
// Authorization logic ada di SATU tempat — semua endpoint pakai itu
// ────────────────────────────────────────────────────────────────────────────
// ✗ Anti-pattern 4: soft delete yang tidak di-filter dalam authorization
// GET /articles/{articleID}
func getArticle(w http.ResponseWriter, r *http.Request) {
article := findArticleByID(parseID(r))
// Artikel yang sudah dihapus (soft delete) masih bisa diakses!
writeJSON(w, http.StatusOK, article)
}
// ✓ Solusi: filter soft deleted records
// article := findArticleByIDAndDeletedAt(parseID(r), nil) // only non-deleted articles
// ────────────────────────────────────────────────────────────────────────────
// ✗ Anti-pattern 5: tidak ada rate limiting pada endpoint yang gagal authorization
// Attacker bisa probe IDOR secara masif tanpa terdeteksi:
// for i in range(1, 1000000): GET /orders/{i}
// ✓ Solusi: log dan rate limit berdasarkan jumlah 403 response per user
Checklist Authorization #
MODEL & DESIGN:
□ Model authorization sudah dipilih (RBAC/ABAC/ReBAC) sesuai kebutuhan
□ Deny by default diterapkan — tidak ada "izinkan kecuali ada yang melarang"
□ Permission didefinisikan dengan granularitas yang cukup (bukan hanya is_admin)
□ Authorization logic terpusat — tidak tersebar di banyak controller
OWNERSHIP VALIDATION:
□ Setiap endpoint yang mengakses resource spesifik memvalidasi ownership
□ Query di-scope berdasarkan authenticated user (bukan filter setelah fetch)
□ Soft-deleted records tidak bisa diakses meski ID diketahui
IDOR PREVENTION:
□ Tidak ada endpoint yang mengembalikan resource berdasarkan ID tanpa ownership check
□ File download melalui endpoint yang validasi akses, bukan path langsung
□ Resource ID di URL tidak memberikan akses otomatis
PRIVILEGE ESCALATION PREVENTION:
□ Field yang bisa diupdate user dibatasi dengan whitelist eksplisit
□ User tidak bisa mengubah role atau permission sendiri
□ Endpoint admin diproteksi dengan permission check yang ketat
API & INTEGRATION:
□ API key memiliki scope yang terbatas
□ Setiap API key di-audit penggunaannya
□ Service-to-service auth juga menggunakan prinsip least privilege
AUDIT & MONITORING:
□ Setiap akses ke resource sensitif di-log
□ Setiap denial authorization di-log
□ Perubahan permission dan role di-log
□ Alert jika ada banyak 403 dari satu user (probe attack)
□ Audit log tidak bisa dimodifikasi oleh user biasa
Ringkasan #
- Deny by default adalah prinsip paling fundamental — jika tidak ada aturan eksplisit yang mengizinkan, tolak. Ini memastikan resource baru atau role baru tidak secara otomatis mendapat akses.
- RBAC cocok untuk sebagian besar aplikasi — user memiliki role, role memiliki permission. Mudah dipahami dan diimplementasikan. ABAC dan ReBAC untuk kebutuhan yang lebih kompleks.
- IDOR adalah celah authorization yang paling umum — selalu validasi bahwa user yang mengakses resource memang berhak atasnya, tidak cukup hanya memastikan user sudah login.
- Sentralisasi authorization logic — jangan tersebar di banyak controller. Satu policy class per resource type, semua controller pakai itu. Inkonsistensi = celah.
- Scoped query lebih aman dari post-fetch filter —
filter_by(id=X, user_id=current_user.id)lebih aman dari query semua lalu cek ownership, karena database langsung memastikan relasi.- Least privilege di semua level — user hanya dapat permission yang diperlukan, API key hanya punya scope yang dibutuhkan, service hanya akses ke resource yang relevan.
- Whitelist field yang bisa diupdate — mass assignment ke model dari request body adalah sumber privilege escalation. Hanya izinkan field yang memang boleh diubah.
- Authorization di frontend hanya untuk UX — semua decision sesungguhnya harus di backend. Frontend yang menyembunyikan tombol bukan authorization yang nyata.
- Audit trail authorization adalah investasi — log setiap denial, setiap akses ke resource sensitif, dan setiap perubahan permission. Data ini kritis untuk forensik dan compliance.
- Rate limit pada 403 response mendeteksi IDOR probing — attacker yang mencoba IDOR secara masif akan menghasilkan banyak 403. Deteksi dan blokir pola ini.