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=1

Burada 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
None

Sonraki denemeler:

index.php?invoice_id=2
index.php?invoice_id=3
index.php?invoice_id=4

Her 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_moneykullanı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=10

Bu 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=10

Manipüle edilmiş hali:

amount=10&ticket_money=0

Ve 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 = 0

Ve 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_id kullanı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=1

Kritik Test: user_id Değiştirme

Burada kendime şu soruyu soruyorum:

"Ya bu user_id bana ait değilse?"

İsteği şu şekilde değiştirelim:

password=hacked123&user_id=2

Sonuç

Sunucudan gelen cevap:

"Pierre's password has been changed."

Yani:

  • Ben Christopher olarak girişliyim
  • Ama Pierre'in şifresini değiştirdim
None

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

None

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_id istemciden 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=1

Bu, 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=3

Bu 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.

None

Sonuç: Yetkisiz Para Transferi

İsteği gönderdiğimde sunucu:

  • Hiçbir hata vermedi
  • İşlemi başarılı kabul etti
  • message=success ile 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.

None

Bu Neden Oldu?

Çünkü backend:

  • Kimlik doğrulama (login) yapmış olsa bile
  • Yetkilendirme kontrolü yapmadı
  • sender_id parametresine 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 addressID bu 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=1

Buraya 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=2

Ve isteği gönderelim.

Buradaki amaç basit:

Bana ait olmayan bir ID ile işlem yapılabiliyor mu?

None

Sunucu Ne Diyor?

Sunucudan gelen cevap:

HTTP/1.1 302 Found
Location: index.php?msg=success

Bu 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=OWNED

Burada amaç:

  • puserid ile 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.

None

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=1

Bu 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.

None

Bu uygulamada kullanıcı, cookie ve POST body üzerinden userid / puserid parametrelerini 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

None

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:

  • bm90IGhlcmU adlı 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:1337

Burada:

  • bm90IGhlcmU parametresi
  • 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.

None

🔹 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
None

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.