IDOR atau Insecure Direct Object Reference adalah salah satu kerentanan keamanan web yang paling umum namun seringkali diabaikan oleh developer. Kerentanan ini memungkinkan attacker untuk mengakses atau memodifikasi data milik user lain hanya dengan mengubah parameter dalam URL atau request.
Meskipun terdengar sederhana, IDOR dapat menyebabkan kebocoran data sensitif yang serius. Artikel ini akan membahas secara mendalam tentang IDOR, contoh-contoh kasusnya, dan bagaimana cara mencegahnya dengan efektif.
Apa Itu IDOR?
IDOR adalah kerentanan keamanan yang terjadi ketika aplikasi web memberikan akses langsung ke objek (seperti file, database record, atau user data) berdasarkan input dari user, tanpa melakukan validasi otorisasi yang memadai.
Kerentanan ini masuk dalam kategori A01:2021 – Broken Access Control dalam OWASP Top 10, yang merupakan risiko keamanan tertinggi untuk aplikasi web.
Karakteristik IDOR
- Menggunakan referensi langsung ke objek internal (ID, nama file, key) yang dapat diprediksi
- Tidak ada atau lemahnya validasi otorisasi untuk mengakses objek tersebut
- Attacker dapat dengan mudah mengubah parameter untuk mengakses data user lain
- Sering ditemukan pada URL, parameter form, cookie, atau header HTTP
Contoh Kasus IDOR dalam Real World
1. IDOR pada Profile User
Contoh paling klasik dari IDOR adalah akses ke profile user menggunakan ID yang dapat diprediksi.
https://example.com/profile?user_id=12345
User dengan ID 12345 dapat melihat profilenya sendiri.
Attacker mencoba mengubah parameter:
https://example.com/profile?user_id=12346
https://example.com/profile?user_id=12347
Jika tidak ada validasi, attacker dapat melihat profile user lain.
<?php
// ❌ KODE VULNERABLE - Tidak ada validasi otorisasi
function viewProfile() {
$userId = $_GET['user_id'];
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
// Langsung tampilkan data tanpa cek apakah user berhak akses
echo json_encode($user);
}
// ✅ KODE AMAN - Dengan validasi otorisasi
function viewProfileSecure() {
$requestedUserId = $_GET['user_id'];
$currentUserId = $_SESSION['user_id'];
// Validasi: user hanya bisa akses profilenya sendiri
if ($requestedUserId != $currentUserId) {
http_response_code(403);
echo json_encode(['error' => 'Unauthorized access']);
return;
}
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$requestedUserId]);
$user = $stmt->fetch();
echo json_encode($user);
}
?>
2. IDOR pada Dokumen atau File
Kerentanan IDOR sering terjadi pada sistem download dokumen atau file.
https://example.com/download?file_id=8901
Attacker dapat mencoba:
https://example.com/download?file_id=8900
https://example.com/download?file_id=8902
Akses ke dokumen pribadi user lain tanpa otorisasi.
<?php
// ❌ VULNERABLE - Tidak cek ownership
function downloadFile() {
$fileId = $_GET['file_id'];
$stmt = $db->prepare("SELECT * FROM files WHERE id = ?");
$stmt->execute([$fileId]);
$file = $stmt->fetch();
if ($file) {
header('Content-Type: application/pdf');
readfile($file['file_path']);
}
}
// ✅ AMAN - Cek ownership sebelum download
function downloadFileSecure() {
$fileId = $_GET['file_id'];
$currentUserId = $_SESSION['user_id'];
$stmt = $db->prepare(
"SELECT * FROM files WHERE id = ? AND user_id = ?"
);
$stmt->execute([$fileId, $currentUserId]);
$file = $stmt->fetch();
if (!$file) {
http_response_code(404);
echo "File not found or unauthorized";
return;
}
header('Content-Type: application/pdf');
readfile($file['file_path']);
}
?>
3. IDOR pada API Endpoint
API modern juga rentan terhadap IDOR, terutama pada endpoint yang mengelola resource user.
<?php
// ❌ VULNERABLE API
// DELETE /api/orders/5432
function deleteOrder() {
$orderId = $_GET['id'];
$stmt = $db->prepare("DELETE FROM orders WHERE id = ?");
$stmt->execute([$orderId]);
echo json_encode(['success' => true]);
}
// ✅ SECURE API
// DELETE /api/orders/5432
function deleteOrderSecure() {
$orderId = $_GET['id'];
$currentUserId = $_SESSION['user_id'];
// Cek ownership sebelum delete
$stmt = $db->prepare(
"SELECT user_id FROM orders WHERE id = ?"
);
$stmt->execute([$orderId]);
$order = $stmt->fetch();
if (!$order || $order['user_id'] != $currentUserId) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
return;
}
$stmt = $db->prepare("DELETE FROM orders WHERE id = ?");
$stmt->execute([$orderId]);
echo json_encode(['success' => true]);
}
?>
4. IDOR pada Parameter Tersembunyi
IDOR tidak selalu pada URL, bisa juga pada hidden field atau request body.
<?php
// Form dengan hidden field
?>
<form method="POST" action="/update-profile">
<input type="hidden" name="user_id" value="12345">
<input type="text" name="email">
<button type="submit">Update</button>
</form>
<?php
// ❌ VULNERABLE - Trust client input
function updateProfile() {
$userId = $_POST['user_id']; // Attacker bisa ubah ini!
$email = $_POST['email'];
$stmt = $db->prepare(
"UPDATE users SET email = ? WHERE id = ?"
);
$stmt->execute([$email, $userId]);
}
// ✅ SECURE - Gunakan session, bukan input user
function updateProfileSecure() {
$userId = $_SESSION['user_id']; // Dari session, bukan POST
$email = $_POST['email'];
$stmt = $db->prepare(
"UPDATE users SET email = ? WHERE id = ?"
);
$stmt->execute([$email, $userId]);
}
?>
Alur Serangan IDOR
Diagram berikut menunjukkan bagaimana attacker mengeksploitasi kerentanan IDOR:
sequenceDiagram
participant A as Attacker
participant W as Web Application
participant D as Database
A->>W: Login sebagai User A (ID: 100)
W->>A: Session User A
A->>W: Request: /profile?user_id=100
W->>D: SELECT * FROM users WHERE id=100
D->>W: Data User A
W->>A: Tampilkan Profile User A ✓
Note over A: Attacker mencoba ubah parameter
A->>W: Request: /profile?user_id=101
W->>D: SELECT * FROM users WHERE id=101
D->>W: Data User B
alt Aplikasi Vulnerable (IDOR)
W->>A: Tampilkan Profile User B ✗
Note over A: SUKSES! Data bocor
else Aplikasi Secure
W->>W: Validasi: Session(100) ≠ Request(101)
W->>A: HTTP 403 Forbidden
Note over A: GAGAL! Akses ditolak
end
Metode Pencegahan IDOR
1. Implementasi Access Control yang Ketat
Selalu validasi bahwa user yang sedang login memiliki hak untuk mengakses resource yang diminta.
<?php
class AccessControl {
public static function canAccessOrder($orderId, $userId) {
global $db;
$stmt = $db->prepare(
"SELECT COUNT(*) FROM orders WHERE id = ? AND user_id = ?"
);
$stmt->execute([$orderId, $userId]);
return $stmt->fetchColumn() > 0;
}
public static function canModifyDocument($docId, $userId) {
global $db;
$stmt = $db->prepare(
"SELECT user_id, is_public FROM documents WHERE id = ?"
);
$stmt->execute([$docId]);
$doc = $stmt->fetch();
if (!$doc) return false;
// Owner bisa modify, atau jika public dan user punya permission
return $doc['user_id'] == $userId ||
($doc['is_public'] && self::hasEditPermission($userId));
}
}
// Penggunaan
$orderId = $_GET['order_id'];
$currentUser = $_SESSION['user_id'];
if (!AccessControl::canAccessOrder($orderId, $currentUser)) {
http_response_code(403);
die('Forbidden');
}
// Lanjutkan proses...
?>
2. Gunakan Indirect Reference Maps
Alih-alih menggunakan ID database secara langsung, gunakan reference map atau UUID yang tidak dapat diprediksi.
<?php
// ❌ VULNERABLE - Predictable ID
// URL: /invoice?id=1001
// ✅ LEBIH AMAN - UUID
// URL: /invoice?id=a3f2c8b9-4e7d-4a1c-9f6e-8d2c1b5a7e9f
class SecureReference {
public static function generateUUID() {
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
public static function createInvoice($userId, $data) {
global $db;
$uuid = self::generateUUID();
$stmt = $db->prepare(
"INSERT INTO invoices (uuid, user_id, data) VALUES (?, ?, ?)"
);
$stmt->execute([$uuid, $userId, json_encode($data)]);
return $uuid;
}
public static function getInvoice($uuid, $userId) {
global $db;
$stmt = $db->prepare(
"SELECT * FROM invoices WHERE uuid = ? AND user_id = ?"
);
$stmt->execute([$uuid, $userId]);
return $stmt->fetch();
}
}
?>
3. Implementasi Role-Based Access Control (RBAC)
Untuk aplikasi yang kompleks, implementasikan sistem role dan permission yang terstruktur.
<?php
class RBAC {
private $userRoles = [];
private $rolePermissions = [
'admin' => ['view_all', 'edit_all', 'delete_all'],
'editor' => ['view_own', 'edit_own', 'view_published'],
'viewer' => ['view_published']
];
public function __construct($userId) {
$this->loadUserRoles($userId);
}
private function loadUserRoles($userId) {
global $db;
$stmt = $db->prepare(
"SELECT role FROM user_roles WHERE user_id = ?"
);
$stmt->execute([$userId]);
$this->userRoles = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
public function hasPermission($permission) {
foreach ($this->userRoles as $role) {
if (in_array($permission, $this->rolePermissions[$role] ?? [])) {
return true;
}
}
return false;
}
public function canAccessResource($resourceId, $resourceType) {
if ($this->hasPermission('view_all')) {
return true;
}
if ($this->hasPermission('view_own')) {
return $this->isOwner($resourceId, $resourceType);
}
return false;
}
private function isOwner($resourceId, $resourceType) {
global $db;
$stmt = $db->prepare(
"SELECT user_id FROM $resourceType WHERE id = ?"
);
$stmt->execute([$resourceId]);
$owner = $stmt->fetchColumn();
return $owner == $_SESSION['user_id'];
}
}
// Penggunaan
$rbac = new RBAC($_SESSION['user_id']);
if (!$rbac->canAccessResource($_GET['document_id'], 'documents')) {
http_response_code(403);
die('Access Denied');
}
?>
4. Validasi di Sisi Server
Jangan pernah mengandalkan validasi client-side atau data yang dikirim dari client.
<?php
// ❌ JANGAN LAKUKAN INI
function badPractice() {
// Percaya data dari client
$userId = $_POST['user_id'];
$role = $_COOKIE['user_role'];
}
// ✅ LAKUKAN INI
function goodPractice() {
// Ambil dari session yang dikelola server
$userId = $_SESSION['user_id'] ?? null;
if (!$userId) {
http_response_code(401);
die('Not authenticated');
}
// Load role dari database
global $db;
$stmt = $db->prepare(
"SELECT role FROM users WHERE id = ?"
);
$stmt->execute([$userId]);
$role = $stmt->fetchColumn();
return ['userId' => $userId, 'role' => $role];
}
?>
Testing untuk Menemukan IDOR
Manual Testing
Beberapa langkah untuk menguji kerentanan IDOR secara manual:
- Buat dua akun user yang berbeda
- Login sebagai User A, catat semua endpoint dan ID yang digunakan
- Login sebagai User B, coba akses resource milik User A dengan mengubah parameter
- Perhatikan response code: 200 (vulnerable), 403/404 (aman)
- Test pada semua HTTP method: GET, POST, PUT, DELETE
Automated Testing dengan Script
<?php
// Script sederhana untuk test IDOR
class IDORTester {
public function testEndpoint($url, $token, $startId, $endId) {
$vulnerable = [];
for ($id = $startId; $id <= $endId; $id++) {
$testUrl = str_replace('{ID}', $id, $url);
$ch = curl_init($testUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token"
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$vulnerable[] = [
'id' => $id,
'url' => $testUrl,
'response' => substr($response, 0, 100)
];
}
}
return $vulnerable;
}
}
// Penggunaan
$tester = new IDORTester();
$results = $tester->testEndpoint(
'https://api.example.com/orders/{ID}',
'user_token_here',
1000,
1100
);
if (!empty($results)) {
echo "VULNERABLE! Found " . count($results) . " accessible IDs\n";
print_r($results);
}
?>
Checklist Pencegahan IDOR
Gunakan checklist berikut untuk memastikan aplikasi Anda terlindungi dari IDOR:
graph TD
A[Terima Request dengan ID] --> B{User Terauthentikasi?}
B -->|Tidak| C[Return 401 Unauthorized]
B -->|Ya| D{ID Valid di Database?}
D -->|Tidak| E[Return 404 Not Found]
D -->|Ya| F{User = Owner Resource?}
F -->|Ya| G[Izinkan Akses]
F -->|Tidak| H{User Punya Role Special?}
H -->|Ya| I{Role Punya Permission?}
H -->|Tidak| J[Return 403 Forbidden]
I -->|Ya| G
I -->|Tidak| J
G --> K[Log Aktivitas]
K --> L[Return Resource]
☐ Validasi authentication setiap request
☐ Validasi authorization sebelum akses resource
☐ Gunakan user ID dari session, bukan dari input
☐ Implementasi access control di semua endpoint
☐ Gunakan UUID atau indirect reference jika memungkinkan
☐ Log semua akses ke resource sensitif
☐ Test dengan multiple user accounts
☐ Review code secara berkala untuk celah IDOR
☐ Implementasi rate limiting untuk mencegah enumeration
☐ Gunakan RBAC untuk aplikasi kompleks
Studi Kasus: IDOR di Platform Terkenal
Kasus 1: Instagram IDOR (2019)
Peneliti keamanan menemukan IDOR pada API Instagram yang memungkinkan akses ke data private account hanya dengan mengetahui user ID. Bug ini memungkinkan attacker melihat email dan nomor telepon user lain.
Kasus 2: IDOR pada E-commerce
Sebuah platform e-commerce mengalami kebocoran data pelanggan karena endpoint order history tidak memvalidasi ownership. Attacker dapat mengakses detail pesanan, alamat pengiriman, dan informasi pembayaran user lain hanya dengan mengiterasi order ID.
Kasus 3: Healthcare App IDOR
Aplikasi healthcare mengalami kebocoran rekam medis pasien karena IDOR pada endpoint download hasil lab. Ini merupakan pelanggaran serius terhadap privacy dan regulasi HIPAA.
Tools untuk Deteksi IDOR
- Burp Suite - Proxy tool dengan Autorize extension untuk automated IDOR testing
- OWASP ZAP - Open source security scanner dengan plugin untuk access control testing
- Postman - Untuk manual testing API dengan collections dan environment variables
- Custom Scripts - Python atau PHP scripts untuk automated enumeration testing
Kesimpulan
IDOR adalah kerentanan serius yang dapat menyebabkan kebocoran data massal, namun relatif mudah dicegah dengan implementasi access control yang benar. Kunci pencegahan IDOR meliputi:
- Selalu validasi otorisasi di server-side untuk setiap akses resource
- Jangan pernah mengandalkan data dari client untuk menentukan akses
- Gunakan session atau token yang tervalidasi untuk identifik