CafePOS adalah sistem POS multi-tenant — satu codebase, banyak cafe, masing-masing punya subdomain sendiri. Semua data (order, menu, meja) dipisah berdasarkan tenant_id. Masalahnya: pemisahan itu awalnya cuma konvensi, bukan aturan yang dipaksakan di level kode.
Gejala awal
Saat audit rutin, saya mencoba mengganti nilai order_id di URL request order milik tenant lain — sekadar iseng memverifikasi asumsi. Ternyata beberapa endpoint lama masih mengembalikan data, karena query-nya cuma memfilter berdasarkan order_id, bukan kombinasi order_id + tenant_id. Itu IDOR (Insecure Direct Object Reference) tekstbuku.
Kenapa ini bisa lolos
Setiap developer (termasuk saya sendiri, beberapa bulan sebelumnya) menulis query secara manual di setiap view function. Sebagian ingat menambahkan filter tenant, sebagian lupa — terutama di endpoint yang ditambahkan buru-buru. Tidak ada satu titik pun yang memaksa aturan itu.
Perbaikannya: satu helper wajib
Solusinya bukan "hati-hati lebih lagi" — itu tidak scalable. Saya membuat satu helper query yang otomatis menyisipkan filter tenant, dan menjadikannya satu-satunya jalan resmi untuk mengambil data sensitif:
def get_order_or_404(order_id: int, tenant_id: int):
"""Satu-satunya cara resmi mengambil order.
Semua view WAJIB lewat sini — tidak ada raw query lain
yang boleh menyentuh tabel `orders` langsung."""
order = Order.query.filter_by(
id=order_id,
tenant_id=tenant_id, # <- baris yang dulu sering lupa ditulis
).first()
if order is None:
abort(404) # sengaja 404, bukan 403 — jangan bocorkan
# bahwa order itu "ada tapi punya orang lain"
return order
Detail yang sengaja saya pilih: kalau order ditemukan tapi milik tenant lain, responsnya tetap 404, bukan 403. Membedakan "tidak ada" dari "ada tapi bukan milikmu" sendiri sudah membocorkan informasi ke penyerang.
Menutup jalan belakang
Helper saja tidak cukup kalau masih ada view lama yang query langsung ke model. Jadi saya:
- Grep seluruh codebase untuk pola
Order.query.filter_by(id=tanpatenant_iddi baris yang sama - Refactor satu per satu ke helper di atas
- Menambahkan code review checklist pribadi: setiap endpoint baru yang menyentuh data tenant harus lewat helper, tidak ada pengecualian
Hasilnya
Bukan cuma menutup satu bug — tapi menghilangkan seluruh kelas bug itu. Endpoint baru yang saya tulis setelahnya otomatis aman, karena satu-satunya cara mengambil data memang sudah scoped by design, bukan tergantung ingatan saya di hari itu.