Web uygulamalarında en sık karşılaşılan ama en çok gözden kaçırılan güvenlik açıklarından biri IDOR (Insecure Direct Object Reference) zafiyetidir. Bu yazıda, hiçbir ön bilgiye sahip olmayan birinin bile anlayabileceği şekilde bir IDOR labını adım adım çözeceğiz.
Bu labda amacımız, bir kullanıcının kendisine ait olmayan faturalara (invoice) yetkisiz şekilde erişip erişemeyeceğini test etmek.
IDOR Nedir?
IDOR, bir uygulamanın:
- Kullanıcının erişmek istediği nesnenin (dosya, fatura, profil vb.)
- gerçekten o kullanıcıya ait olup olmadığını kontrol etmemesi durumudur.
Yani uygulama şunu varsayar:
"Kullanıcı bu ID'yi gönderiyorsa, demek ki yetkilidir."
Bu varsayım yanlıştır ve ciddi veri sızıntılarına yol açar.
Lab Ortamı ve Giriş Noktası
http://localhost:1337/lab/idor/invoices/Bu sayfada:
- "Faturalar" başlıklı bir ekran
- Ortada tek bir buton bulunuyor (örneğin "View Invoice")
Bu sayfa saldırı noktası değildir. Sadece bizi bir sonraki adıma yönlendirir.
Butona Bastığımızda Ne Oluyor?
Butona bastığımızda tarayıcı bizi otomatik olarak şu adrese yönlendiriyor:
http://localhost:1337/lab/idor/invoices/index.php?invoice_id=1Burada ilk kritik noktayı görüyoruz:
🔴 invoice_id adlı bir parametre URL üzerinden gönderiliyor.
Bu şu anlama geliyor:
- Hangi faturanın görüntüleneceği
- tamamen URL'deki invoice_id değerine bağlı
Kaynak Kodu İnceleyelim
Sunucu tarafındaki ilgili kod parçası şu şekilde:
if( isset($_GET['invoice_id']) ){
$query = $db -> prepare("SELECT * FROM idor_invoices WHERE id=:id");
$query -> execute(array(
'id' => $_GET['invoice_id']
));
$row = $query -> fetch();
header("Content-type: application/pdf");
header("Content-Disposition: inline; filename=invoice.pdf");
@readfile($row['file_url']);
}Bu kod ne yapıyor?
URL'den gelen invoice_id değerini alıyor
Veritabanında bu ID'ye ait faturayı sorguluyor
Faturaya ait PDF dosyasını kullanıcıya gösteriyor
🔴 Eksik olan şey: Kullanıcının bu faturayı görmeye yetkili olup olmadığı hiç kontrol edilmiyor.
IDOR Açığı Tam Olarak Burada
Uygulama şu soruyu sormuyor:
- "Bu fatura bu kullanıcıya mı ait?"
Sadece şuna bakıyor:
- "Böyle bir fatura ID'si var mı?"
Bu, IDOR'un textbook (klasik) örneğidir.
Saldırı Nasıl Yapılır?
Yapmamız gereken tek şey:
- URL'deki invoice_id değerini değiştirmek
İlk istek:
index.php?invoice_id=1
Sonraki denemeler:
index.php?invoice_id=2
index.php?invoice_id=3
index.php?invoice_id=4Her bir değeri manuel olarak değiştiriyoruz.
Sonuç (Zafiyetin Kanıtı)
Her farklı invoice_id değerinde:
- Farklı bir PDF dosyası açılıyor
- Başka kullanıcılara ait faturalar görüntülenebiliyor
Bu da şu anlama geliyor:
Yetkisiz bir kullanıcı, sistemdeki tüm faturalara sadece ID değiştirerek erişebiliyor.
Lab bu noktada başarıyla çözülmüş olur.
2. Ticket Sales
Lab Ortamı ve Amaç
Lab'a girdiğimizde karşımıza basit bir bilet satın alma arayüzü çıkıyor:
- Bir biletin fiyatı gösteriliyor
- Hesabımızdaki para miktarı gösteriliyor
- Kaç adet bilet almak istediğimizi soran bir form bulunuyor
Amaç:
Kullanıcı olarak normalde mümkün olmaması gereken bir işlemi yapıp yapamayacağımızı test etmek.
Yani:
- Daha az para ödemek
- Hiç para ödememek
- Sistemin mantığını bozmak
İlk Bakışta Her Şey Normal Gibi
Arayüzde şu bilgileri görüyoruz:
- Ticket price: 10 $
- Money in account: örneğin 1000 $
Formda sadece:
- "Kaç adet bilet almak istiyorsunuz?" sorusu var
Bu noktada çoğu kullanıcı şunu varsayar:
"Fiyatı sunucu hesaplıyor, kullanıcı değiştiremez."
Ama bu varsayım, güvenli değildir.
Kaynak Kodu İnceleyelim
İlgili PHP kodunda şu kritik satırı görüyoruz:
$total = (int)$_POST['ticket_money'] * (int)$_POST['amount'];Burada çok önemli bir detay var:
amount→ kullanıcının girdiği değer (normal)ticket_money→ kullanıcıdan gelen değer
Yani bilet fiyatı:
- Veritabanından tekrar alınmıyor
- Sunucu tarafından sabitlenmiyor
- Kullanıcının gönderdiği POST verisine güveniliyor
Bu, ciddi bir güvenlik hatasıdır.
"Hidden Input" Yanılgısı
HTML formunda bilet fiyatı şu şekilde gönderiliyor:
<input type="hidden" name="ticket_money" value="10">Bazı geliştiriciler şunu düşünür:
"Hidden input, kullanıcı değiştiremez."
Bu yanlıştır.
Hidden input:
- Sadece ekranda görünmez
- Ama tarayıcı, Burp Suite veya herhangi bir proxy ile kolayca değiştirilebilir
Sunucu, gizli bile olsa istemciden gelen hiçbir veriye güvenmemelidir.
Saldırıya Geçiyoruz
1. Formu normal dolduruyoruz
Örneğin:
- Amount:
1
Ve satın alma butonuna basıyoruz.
2. POST isteğini yakalıyoruz
Burp Suite üzerinden yakalanan istek şu şekildedir:
POST /lab/idor/ticket-sales/ HTTP/1.1
amount=1&ticket_money=10Bu noktada görüyoruz ki:
- Fiyat bilgisi (
ticket_money) - Doğrudan istemci tarafından gönderiliyor
3. Payload uyguluyoruz
Şimdi sadece request body'yi değiştiriyoruz.
Orijinal:
amount=1&ticket_money=10Manipüle edilmiş hali:
amount=10&ticket_money=0Ve isteği sunucuya gönderiyoruz.
Sonuç: Açık Kanıtlandı
Sunucunun döndürdüğü cevapta şunu görüyoruz:
- Number of tickets you bought: 10
- Money you pay: 0 $
Yani:
- Normalde 100 $ ödememiz gerekirken
- Hiç para ödemeden
- 10 adet bilet satın aldık
Sunucu Tarafında Ne Oldu?
Sunucu şu hesabı yaptı:
total = ticket_money * amount
total = 0 * 10 = 0Ve ardından şu kontrolü geçti:
if ($account['money'] >= $total)Bu kontrol:
- 0 olduğu için
- Her zaman doğru kabul edildi
Yani sistem:
"Bu işlem güvenli" diye düşündü
Ama güvenli değildi.
3.Changing Password
Uygulamada bir "Changing Password" sayfası var. Sayfada bize şu bilgi gösteriliyor:
- Giriş yapan kullanıcı: Christopher
- Şifre değiştirme formu mevcut
Normal beklenti şu:
"Ben sadece kendi şifremi değiştirebilmeliyim."
Kaynak Kodu İnceleyerek Başlayalım
İlk bakacağımız
"Bu uygulama şifreyi kime göre değiştiriyor?"
Formu incelediğimizde şunu görebiliriz:
<input type="hidden" name="user_id" value="1">Bu nokta kritik.
Çünkü:
user_idkullanıcıdan gizli ama kontrolsüz şekilde geliyor- Sunucu tarafı bu değeri doğrulamıyor
Backend Ne Yapıyor?
PHP koduna baktığımızda şunlar oluyor:
Sayfa başında kullanıcı sabit:
$user_id = 1;Ama form gönderildiğinde:
$_POST['user_id']kullanılıyor.
Yani:
- Sayfa bana "Christopher" diyor
- Ama backend POST ile gelen user_id'ye inanıyor
Bu bir yetkilendirme hatası.
Burp Suite ile Trafiği Yakalama
Adım 1: Burp Proxy Açılır
- Tarayıcı Burp Proxy'ye bağlanır
- Şifre değiştirme formu gönderilir
Burp'te yakalanan istek:
POST /lab/idor/changing-password/ HTTP/1.1
password=newpassword123&user_id=1Kritik Test: user_id Değiştirme
Burada kendime şu soruyu soruyorum:
"Ya bu
user_idbana ait değilse?"
İsteği şu şekilde değiştirelim:
password=hacked123&user_id=2Sonuç
Sunucudan gelen cevap:
"Pierre's password has been changed."
Yani:
- Ben Christopher olarak girişliyim
- Ama Pierre'in şifresini değiştirdim

Bu noktada görüyoruz ki Bu net bir IDOR zafiyetidir.

Bu Neden Oldu?
Çünkü backend şunları YAPMADI:
- user_id'nin session'daki kullanıcıyla eşleşip eşleşmediğini kontrol etmedi
- "Bu isteği yapan kişi bu kullanıcıya yetkili mi?" sorusunu sormadı
Backend sadece şuna baktı:
"Bir user_id geldi mi? Geldi. O zaman güncelle."
4.Money Transfer
Senaryo: Para Transferi Sayfası
Uygulamada bir Money Transfer (Para Transferi) arayüzü bulunuyor.
Sayfaya girdiğimde:
- Aktif kullanıcı olarak girişliyim (
user_id = 1) - Hesap adım ve bakiyem gösteriliyor
- Para transferi yapabileceğim bir form var
Normal beklenti:
"Ben sadece kendi hesabımdan para gönderebilmeliyim."
Bu beklentinin backend tarafında gerçekten uygulanıp uygulanmadığını test etmek için kaynak kodu inceleyelim.
Kaynak Kod İncelemesi: Yetkilendirme Açısından Kritik Nokta
<input type="hidden" name="sender_id" value="1">Bu noktada
"Gönderen hesabı backend mi belirliyor, yoksa kullanıcıdan mı alıyor?"
Cevap backend kodunda açıkça görülüyor:
$sender_id = $_POST['sender_id'];Bu kritik bir hata.
Çünkü:
sender_idistemciden geliyor- Session'daki kullanıcıyla karşılaştırılmıyor
- Yetkilendirme kontrolü yapılmıyor
Yani backend şunu diyor:
"Kim gönderiyor? Bana POST'ta ne geldiyse odur."
Backend İş Akışını Anlamak
Para transferi sırasında backend şu adımları izliyor:
sender_id → POST isteğinden alınıyor
Bu ID'ye ait bakiye kontrol ediliyor
Para yeterliyse:
- Alıcının bakiyesine ekleniyor
- Gönderenin bakiyesinden düşülüyor
Ama en kritik kontrol yok:
"Bu sender_id gerçekten giriş yapan kullanıcıya mı ait?"
Burp Suite ile Trafiği Yakalama
Formu normal şekilde doldurup gönderdiğimizde Burp'te şu POST isteğini yakaladık:
POST /lab/idor/money-transfer/ HTTP/1.1
transfer_amount=100
recipient_id=2
sender_id=1Bu, uygulamanın beklediği "normal" istek.
🔓 IDOR Sömürüsü: sender_id Manipülasyonu
Bu aşamada kritik bir varsayım ele alınır:
İşlemi başlatan kullanıcı bilgisi sunucu tarafından gerçekten doğrulanıyor mu, yoksa istemciden gelen değere mi güveniliyor?
Bu varsayımı test etmek için, yakalanan isteğin yapısı korunarak yalnızca işlemi gerçekleştiren kullanıcıyı temsil eden sender_id parametresi farklı bir kullanıcıya ait olacak şekilde değiştirilir.
Değişiklik sonrası istek tekrar gönderildiğinde, sunucunun bu manipülasyona rağmen işlemi başarıyla tamamlaması, yetkilendirme kontrolünün eksik olduğunu açıkça ortaya koyar.
transfer_amount=100
recipient_id=2
-sender_id=1
+sender_id=3Bu isteğin anlamı:
- 3 numaralı kullanıcının parasını
- 2 numaralı kullanıcıya gönder
Ben 1 numaralı kullanıcı olmama rağmen.

Sonuç: Yetkisiz Para Transferi
İsteği gönderdiğimde sunucu:
- Hiçbir hata vermedi
- İşlemi başarılı kabul etti
message=successile yönlendirme yaptı
Sayfaya döndüğümde:
- 3 numaralı kullanıcının parası azalmıştı
- 2 numaralı kullanıcının parası artmıştı
Bu noktada net olarak şunu söyleyebiliriz:
Başka bir kullanıcının hesabından yetkisiz para transferi yapılabildi.

Bu Neden Oldu?
Çünkü backend:
- Kimlik doğrulama (login) yapmış olsa bile
- Yetkilendirme kontrolü yapmadı
sender_idparametresine körü körüne güvendi
5. Address Entry
Bu labda bir adres güncelleme ve sipariş onaylama akışı var. İlk bakışta her şey sıradan görünüyor. Ancak asıl soru şu:
Uygulama, gönderilen ID'nin gerçekten o kullanıcıya ait olup olmadığını kontrol ediyor mu?
Akışı İnceleyelim
Adres girme ekranında kullanıcıdan bir adres isteniyor. Formu biraz incelediğimizde şu alan dikkat çekiyor:
<input type="hidden" name="addressID" value="1">Bu alan:
- Kullanıcıya gösterilmiyor
- Ama POST isteğiyle backend'e gönderiliyor
Burada durup düşünelim:
Bu addressID gerçekten session'daki kullanıcıyla mı eşleştiriliyor, yoksa olduğu gibi mi kabul ediliyor?
Bunu anlamanın tek yolu denemek.
Normal Senaryo Nasıl Olmalı?
Sağlıklı bir uygulamada backend şu kontrolü yapmalı:
- Session'daki kullanıcıyı al
- Gönderilen
addressIDbu kullanıcıya mı ait kontrol et - Değilse işlemi reddet
Şimdi bakalım bu kontrol gerçekten var mı.
İsteği Yakalayalım
Önce arayüzden geçerli bir adres girelim. Ardından Update Address butonuna basalım.
Bu sırada Burp Suite açık olsun ve POST isteğini yakalayalım. İstek gövdesinde şu parametreyi görmemiz gerekiyor:
addressID=1Buraya kadar her şey normal.
Asıl Test: ID'yi Değiştirelim
Şimdi kritik noktaya geldik.
Burp Repeater'da:
- Hiçbir cookie'ye dokunmayalım
- Session'ı bozmayalım
- Başka parametre eklemeyelim
Sadece şunu yapalım:
addressID=1 → addressID=2Ve isteği gönderelim.
Buradaki amaç basit:
Bana ait olmayan bir ID ile işlem yapılabiliyor mu?

Sunucu Ne Diyor?
Sunucudan gelen cevap:
HTTP/1.1 302 Found
Location: index.php?msg=successBu cevap çok net:
- İstek reddedilmedi
- Yetkilendirme hatası yok
- İşlem başarılı kabul edildi
Yani backend şu kontrolü yapmadı:
"Bu addressID bu kullanıcıya mı ait?"
Buradaki Mantığı Netleştirelim
Burada kim olduğumuz önemli değil. Önemli olan şu:
- Sistem, ID'yi kimin gönderdiğine değil
- ID'nin doğru olup olmadığına bile bakmadan
- Sadece var olmasına güveniyor
Bu tam olarak Insecure Direct Object Reference (IDOR) tanımıdır.
6. About
Uygulamada İlk İnceleme
Uygulamada bir About / Profile sayfası bulunuyor. Sayfa açıldığında belirli bir kullanıcıya ait bilgiler gösteriliyor.
İlgili PHP kodu incelendiğinde şu yapı görülmektedir:
$puserid = $_POST['puserid'];
$pjob = $_POST['pjob'];
$pabout = $_POST['pabout'];
$pemail = $_POST['pemail'];
$pphone = $_POST['pphone'];
$plocation = $_POST['plocation'];
$sql = "UPDATE users
SET job='$pjob',
about='$pabout',
email='$pemail',
phone='$pphone',
location='$plocation'
WHERE id='$puserid'";
mysqli_query($conn, $sql);🚨 Kritik Güvenlik Hatası Nerede?
Bu kodda olmayan şey, olanlardan daha önemli:
❌ Oturumdaki kullanıcı ID'si ile puserid karşılaştırılmıyor
❌ $_SESSION['userid'] gibi bir doğrulama yok
❌ "Bu profili güncellemeye yetkili misin?" sorusu hiç sorulmuyor
Sunucu şunu yapıyor:
"Formdan gelen ID neyse, o kullanıcıyı güncelle."
Bu gerçek, zararlı ve write-impact'li IDOR zafiyetidir
Trafiği İzleyelim (Burp Suite)
Burp Suite açıkken sayfa yenilendiğinde bir HTTP response görüyoruz:
Set-Cookie: userid=deleted;Bu bize şunu söylüyor:
- Kullanıcı kimliği cookie üzerinden yönetiliyor
- Ama bu cookie kalıcı değil
- Her response'ta sıfırlanıyor
Yani kullanıcı kimliği:
- Server tarafından güvenli şekilde tutulmuyor
- İstemciden (client) gelen veriye güveniliyor
Bu zaten başlı başına kırmızı bayrak.
Write IDOR Exploit (Yetkisiz Değiştirme)
Request'i Repeater'a Gönder
Burp'te POST request Repeater'a gönderilir.
Sadece BODY Değiştirilir
Aşağıdaki gibi bilinçli bir payload yazılır:
puserid=1
&pjob=HACKED
&pabout=IDOR_WRITE_SUCCESS
&pemail=hacked@idor.test
&pphone=111111111
&plocation=OWNEDBurada amaç:
puseridile başka bir kullanıcıyı hedeflemek- İçeriği fark edilecek şekilde değiştirmek
Request Gönderilir
Response şu şekilde gelir:
HTTP/1.1 302 Found
Location: ./⚠️ Bu hata değildir. Bu, işlemin başarıyla yapıldığını gösteren bir redirect'tir.

Burada amaç:
- Kendi kullanıcımız dışında bir ID'yi hedeflemek
- Değişikliğin net şekilde ayırt edilebilir olması
🔍 GET İsteği ile Değişikliğin Doğrulanması
Profil güncellendikten sonra yapılan değişikliklerin ekranda görünmemesinin nedeni, uygulamanın GET isteği sırasında kullanıcıyı cookie üzerinden belirlemesidir.
Aşağıdaki GET isteği gönderilmiştir:
GET /lab/idor/about/ HTTP/1.1
Host: localhost:1337
Cookie: userid=1Bu istekle birlikte tarayıcı, sunucuya "userid=1 kullanıcısının profilini göster" demektedir.
Sunucu bu isteğe cevap verirken userid cookie'sini silmekte ve sayfayı varsayılan kullanıcı bilgileriyle döndürmektedir. Bu nedenle, POST isteği ile yapılan değişiklikler arayüzde kalıcı olarak görünmemektedir.
Ancak bu durum, verilerin değişmediği anlamına gelmez. POST isteği sırasında puserid parametresi sunucu tarafında kontrol edilmeden işlendiği için, başka bir kullanıcının profili yetkisiz şekilde başarıyla güncellenmiştir.

Bu uygulamada kullanıcı, cookie ve POST body üzerinden
userid / puseridparametrelerini değiştirerek başka kullanıcıların profil bilgilerini yetkisiz şekilde okuyup değiştirebilmektedir. Bu durum Insecure Direct Object Reference (IDOR) zafiyetidir.
Tamam. Şimdi Medium'a birebir koyabileceğin, senin özellikle istediğin üslupta yazıyorum: "inceleyelim bakalım — ne oluyor — neden oluyor — Burp'te nasıl yaptık" diliyle. Ne yaptım / ettim yok. Okuyan gerçekten öğrenir.
7. Shopping Cart

Kaynak Kod İncelemesi (En Kritik Aşama)
Labın ana sayfasındaki PHP kodunu incelediğimizde ilk olarak şu yapı dikkat çekmektedir:
🔴 Bakiye sıfırlama işlemi (yetki kontrolü yok)
if (isset($_GET['resetBalance'])) {
$id = $balance['id'];
$query = $conn->prepare("UPDATE temp SET balance=1000 WHERE id = ?");
$query->execute(array($id));
header("Location: index.php");
}Burada:
- İşlem GET isteği ile tetikleniyor
- Kullanıcının yetkisi kontrol edilmiyor
- CSRF veya rol kontrolü yok
Bu kısım güvenli olmayan bir tasarıma işaret etse de, labın ana IDOR noktası burada değil, sadece bir ön uyarı niteliğinde.
Asıl Şüpheli Kısım: Ürün Ekleme Mekanizması
Ürünlerin listelendiği bölümde aşağıdaki kod yer almaktadır:
<a href="add.php?bm90IGhlcmU=' . $product["id"] . '" class="btn">Burada kritik noktalar:
bm90IGhlcmUadlı GET parametresi- Parametre değeri doğrudan:
$product["id"]
Sunucu tarafında:
- Kullanıcının bu ürüne erişim yetkisi var mı?
- Bu ürün gerçekten sepete eklenebilir mi?
- Kullanıcı bu nesnenin sahibi mi?
❌ Hiçbiri kontrol edilmiyor
Bu noktada artık şunu net olarak söyleyebiliriz:
Uygulama, nesne erişim kontrolünü tamamen kullanıcıdan gelen ID'ye bırakmış durumda.
Bu, klasik bir IDOR zafiyeti göstergesidir.
IDOR Mantığını Netleştirelim
Bu senaryoda:
- Nesne: Ürün (product)
- Nesne referansı:
product["id"] - Erişim yöntemi: GET parametresi
- Koruma: Yok
Yani kullanıcı, yalnızca ID değerini değiştirerek:
- Normalde erişmemesi gereken ürünleri
- Sistemde gizli olması gereken nesneleri
- Yetkisiz işlemleri
gerçekleştirebilir.
Burp Suite ile Sömürü
🔹 Adım 1: Trafiği yakala
- Burp Proxy açıkken herhangi bir ürüne "Add to Cart" butonuna tıklanır.
- İstek Burp'te yakalanır.
🔹 Adım 2: GET isteğini incele
Yakalanan isteğin yapısı şu şekildedir:
GET /lab/idor/shopping/add.php?bm90IGhlcmU=1 HTTP/1.1
Host: localhost:1337Burada:
bm90IGhlcmUparametresi- Doğrudan ürün ID'sini temsil etmektedir
🔹 Adım 3: ID'yi değiştir
Burp Repeater'a gönderilir ve örneğin:
bm90IGhlcmU=1 → bm90IGhlcmU=5şeklinde değiştirilir.

🔹 Adım 4: İsteği gönder
- Sunucu isteği herhangi bir hata vermeden kabul eder
- Ürün sepete eklenir
- Yetki kontrolü yapılmadığı doğrulanır

Sepet ekranında, kullanıcının bakiyesi yetersiz olmasına rağmen ürünün başarıyla eklenmesi, ürün ekleme işleminin yalnızca istemciden gelen nesne ID'sine dayandığını ve sunucu tarafında yetkilendirme kontrolü yapılmadığını göstermektedir. Bu durum, IDOR zafiyetinin başarıyla sömürüldüğünü kanıtlamaktadır.