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:
User ──── memiliki ────→ Role(s)
│
└── memiliki ──→ 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
from enum import Enum
from functools import wraps
class Permission(str, Enum):
# Articles
ARTICLES_READ = "articles:read"
ARTICLES_CREATE = "articles:create"
ARTICLES_EDIT_OWN = "articles:edit_own"
ARTICLES_EDIT_ANY = "articles:edit_any"
ARTICLES_DELETE_OWN = "articles:delete_own"
ARTICLES_DELETE_ANY = "articles:delete_any"
ARTICLES_PUBLISH = "articles:publish"
# Users
USERS_READ = "users:read"
USERS_MANAGE = "users:manage"
# Admin
ADMIN_ACCESS = "admin:access"
# Role definitions — single source of truth
ROLE_PERMISSIONS: dict[str, set[Permission]] = {
"viewer": {
Permission.ARTICLES_READ,
Permission.USERS_READ,
},
"author": {
Permission.ARTICLES_READ,
Permission.ARTICLES_CREATE,
Permission.ARTICLES_EDIT_OWN,
Permission.ARTICLES_DELETE_OWN,
Permission.USERS_READ,
},
"editor": {
Permission.ARTICLES_READ,
Permission.ARTICLES_CREATE,
Permission.ARTICLES_EDIT_OWN,
Permission.ARTICLES_EDIT_ANY,
Permission.ARTICLES_DELETE_OWN,
Permission.ARTICLES_PUBLISH,
Permission.USERS_READ,
},
"admin": {
p for p in Permission # semua permission
},
}
def get_user_permissions(user) -> set[Permission]:
"""Kumpulkan semua permission dari semua role user."""
permissions = set()
for role in user.roles:
permissions |= ROLE_PERMISSIONS.get(role, set())
return permissions
def has_permission(user, permission: Permission) -> bool:
return permission in get_user_permissions(user)
# Decorator untuk endpoint
def require_permission(permission: Permission):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if not current_user.is_authenticated:
return jsonify({'error': 'Authentication required'}), 401
if not has_permission(current_user, permission):
return jsonify({'error': 'Insufficient permissions'}), 403
return f(*args, **kwargs)
return decorated
return decorator
# Penggunaan di endpoint
@app.route('/admin/users')
@require_permission(Permission.USERS_MANAGE)
def manage_users():
return jsonify(User.query.all())
@app.route('/articles', methods=['POST'])
@require_permission(Permission.ARTICLES_CREATE)
def create_article():
# ...
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
def can_access_document(user, document, action, context=None):
"""
Evaluasi apakah user boleh melakukan action pada document
berdasarkan kombinasi atribut.
"""
context = context or {}
# Policy: dokumen confidential hanya bisa dibaca oleh
# user dari departemen yang sama atau level manager ke atas
if document.classification == 'confidential' and action == 'read':
if user.department != document.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 != document.owner_id:
if 'editor' not in user.roles:
return False, "Hanya pemilik atau editor yang bisa mengedit"
# Policy: tidak ada yang bisa menghapus dokumen yang sudah dipublish
# kecuali admin
if action == 'delete' and document.status == 'published':
if 'admin' not in user.roles:
return False, "Dokumen yang sudah dipublish tidak bisa dihapus"
# Policy: akses di luar jam kerja hanya untuk dokumen non-sensitif
if context.get('hour') and (context['hour'] < 8 or context['hour'] > 18):
if document.classification in ('confidential', 'restricted'):
return False, "Dokumen sensitif tidak bisa diakses di luar jam kerja"
return True, None
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:
doc:report ── owner ──→ user:ali
doc:report ── editor ──→ user:budi
doc:report ── viewer ──→ group:marketing
group:marketing ── member ──→ user:citra
Pertanyaan: bisakah user:citra melihat doc:report?
Traversal graph:
user:citra ──member of──→ group:marketing
group:marketing ──viewer of──→ 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)
def can_access(user, resource):
if user.is_banned:
return False
if resource.is_private and user.id != resource.owner_id:
return False
return True # default: izinkan
# Masalah: jika ada kondisi baru yang belum di-handle (resource baru,
# role baru, status baru), default-nya adalah MENGIZINKAN.
# Bug baru → celah keamanan baru
# BENAR: deny by default
def can_access(user, resource):
# Hanya izinkan jika ADA kondisi yang eksplisit mengizinkan
if resource.is_public:
return True
if user.id == resource.owner_id:
return True
if 'admin' in user.roles:
return True
if resource.shared_with and user.id in resource.shared_with:
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
@app.route('/orders/<int:order_id>')
@login_required
def get_order(order_id):
order = Order.query.get_or_404(order_id)
return jsonify(order.to_dict())
# 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
@app.route('/orders/<int:order_id>', methods=['PUT'])
@require_permission(Permission.ARTICLES_EDIT_OWN)
def update_order(order_id):
order = Order.query.get_or_404(order_id)
# Permission EDIT_OWN ada, tapi tidak dicek apakah ini MILIKNYA!
order.update(request.json)
return jsonify(order.to_dict())
# BENAR: validasi ownership secara eksplisit
@app.route('/orders/<int:order_id>')
@login_required
def get_order(order_id):
order = Order.query.get_or_404(order_id)
# Cek ownership SEBELUM mengembalikan data
if order.user_id != current_user.id:
# Admin bisa lihat semua — cek apakah user adalah admin
if not has_permission(current_user, Permission.ORDERS_READ_ANY):
abort(403) # Forbidden — ini bukan punyamu
return jsonify(order.to_dict())
# Pattern yang lebih bersih: scoped query
@app.route('/orders/<int:order_id>')
@login_required
def get_order(order_id):
# Query sudah dibatasi oleh user_id — tidak mungkin dapat order orang lain
if has_permission(current_user, Permission.ORDERS_READ_ANY):
order = Order.query.get_or_404(order_id)
else:
order = Order.query.filter_by(
id=order_id,
user_id=current_user.id
).first_or_404()
return jsonify(order.to_dict())
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
class APIKey(db.Model):
id = db.Column(db.Integer, primary_key=True)
key_hash = db.Column(db.String(64), unique=True, nullable=False)
name = db.Column(db.String(100), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
scopes = db.Column(db.JSON, nullable=False) # ["read:orders", "write:orders"]
expires_at = db.Column(db.DateTime, nullable=True)
last_used_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def validate_api_key_scope(api_key: APIKey, required_scope: str) -> bool:
"""Cek apakah API key memiliki scope yang diperlukan."""
if api_key.expires_at and api_key.expires_at < datetime.utcnow():
return False
# Exact match atau wildcard
allowed_scopes = api_key.scopes or []
return (required_scope in allowed_scopes or
'*' in allowed_scopes or
required_scope.split(':')[0] + ':*' in allowed_scopes)
def require_api_scope(scope: str):
"""Decorator untuk endpoint yang butuh scope tertentu."""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
api_key = get_current_api_key()
if not api_key:
return jsonify({'error': 'API key required'}), 401
if not validate_api_key_scope(api_key, scope):
return jsonify({'error': f"Missing required scope: {scope}"}), 403
return f(*args, **kwargs)
return decorated
return decorator
@app.route('/api/orders')
@require_api_scope('read:orders')
def list_orders():
# Hanya API key dengan scope "read:orders" atau "read:*" yang bisa akses
pass
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
class ResourceAccessPolicy:
"""Base class untuk policy akses per resource."""
def can_read(self, user, resource) -> bool:
raise NotImplementedError
def can_edit(self, user, resource) -> bool:
raise NotImplementedError
def can_delete(self, user, resource) -> bool:
raise NotImplementedError
class ArticleAccessPolicy(ResourceAccessPolicy):
def can_read(self, user, article) -> bool:
if article.status == 'published':
return True # artikel published bisa dibaca siapapun
return (article.author_id == user.id or
has_permission(user, Permission.ARTICLES_EDIT_ANY))
def can_edit(self, user, article) -> bool:
if has_permission(user, Permission.ARTICLES_EDIT_ANY):
return True
if has_permission(user, Permission.ARTICLES_EDIT_OWN):
return article.author_id == user.id
return False
def can_delete(self, user, article) -> bool:
if has_permission(user, Permission.ARTICLES_DELETE_ANY):
return True
if has_permission(user, Permission.ARTICLES_DELETE_OWN):
return article.author_id == user.id
return False
def can_publish(self, user, article) -> bool:
return has_permission(user, Permission.ARTICLES_PUBLISH)
# Policy registry
_policies: dict[type, ResourceAccessPolicy] = {
Article: ArticleAccessPolicy(),
}
def authorize(user, resource, action: str) -> bool:
"""Cek apakah user boleh melakukan action pada resource."""
policy = _policies.get(type(resource))
if not policy:
return False # Deny by default jika tidak ada policy
method = getattr(policy, f"can_{action}", None)
if not method:
return False # Action tidak dikenal → tolak
return method(user, resource)
# Penggunaan di endpoint
@app.route('/articles/<int:article_id>', methods=['PUT'])
@login_required
def update_article(article_id):
article = Article.query.get_or_404(article_id)
if not authorize(current_user, article, 'edit'):
abort(403)
article.update(request.json)
db.session.commit()
return jsonify(article.to_dict())
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
@app.route('/users/<int:user_id>', methods=['PUT'])
@login_required
def update_user(user_id):
if user_id != current_user.id:
abort(403)
user = User.query.get_or_404(user_id)
user.update(request.json) # ← user bisa kirim {"roles": ["admin"]}!
db.session.commit()
return jsonify(user.to_dict())
# BENAR: field yang bisa diubah oleh user dibatasi secara eksplisit
@app.route('/users/<int:user_id>', methods=['PUT'])
@login_required
def update_user(user_id):
if user_id != current_user.id:
abort(403)
user = User.query.get_or_404(user_id)
# Whitelist field yang boleh diubah user biasa
allowed_fields = {'name', 'bio', 'avatar_url', 'preferences'}
update_data = {k: v for k, v in request.json.items()
if k in allowed_fields}
# Field sensitif hanya bisa diubah oleh admin
if has_permission(current_user, Permission.USERS_MANAGE):
sensitive_fields = {'roles', 'is_active', 'email_verified'}
update_data.update({k: v for k, v in request.json.items()
if k in sensitive_fields})
user.update(update_data)
db.session.commit()
return jsonify(user.to_dict())
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 logging
from datetime import datetime
security_logger = logging.getLogger('security.authorization')
def log_authorization_decision(
user_id: int,
action: str,
resource_type: str,
resource_id: str,
granted: bool,
reason: str = None
):
"""Log setiap keputusan authorization."""
security_logger.info(
"Authorization decision",
extra={
'event_type': 'authorization',
'user_id': user_id,
'action': action,
'resource_type': resource_type,
'resource_id': str(resource_id),
'granted': granted,
'reason': reason,
'timestamp': datetime.utcnow().isoformat(),
'request_id': g.get('request_id'),
'ip': request.remote_addr,
}
)
# Contoh penggunaan di policy
class ArticleAccessPolicy(ResourceAccessPolicy):
def can_delete(self, user, article) -> bool:
if has_permission(user, Permission.ARTICLES_DELETE_ANY):
log_authorization_decision(
user.id, 'delete', 'article', article.id, True,
'has articles:delete_any permission'
)
return True
if (has_permission(user, Permission.ARTICLES_DELETE_OWN)
and article.author_id == user.id):
log_authorization_decision(
user.id, 'delete', 'article', article.id, True,
'owner with articles:delete_own permission'
)
return True
log_authorization_decision(
user.id, 'delete', 'article', article.id, False,
'insufficient permission'
)
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
// React component
function AdminPanel() {
if (user.role === 'admin') {
return <AdminDashboard />;
}
return null;
}
// 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
@app.route('/invoices/<int:invoice_id>/pdf')
@login_required
def download_invoice(invoice_id):
invoice = Invoice.query.get_or_404(invoice_id)
return send_file(invoice.pdf_path)
# User bisa mencoba /invoices/1, /invoices/2, /invoices/3 untuk download semua invoice
# ✓ Solusi: validasi ownership ATAU gunakan signed URL
@app.route('/invoices/<int:invoice_id>/pdf')
@login_required
def download_invoice(invoice_id):
invoice = Invoice.query.filter_by(
id=invoice_id,
user_id=current_user.id # hanya invoice milik user ini
).first_or_404()
return send_file(invoice.pdf_path)
────────────────────────────────────────────────────────────────────────────
# ✗ 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
@app.route('/articles/<int:article_id>')
@login_required
def get_article(article_id):
article = Article.query.get_or_404(article_id)
# Artikel yang sudah dihapus (soft delete) masih bisa diakses!
return jsonify(article.to_dict())
# ✓ Solusi: filter soft deleted records
article = Article.query.filter_by(
id=article_id,
deleted_at=None # hanya artikel yang belum dihapus
).first_or_404()
────────────────────────────────────────────────────────────────────────────
# ✗ 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.