Web Security 101 0x02 | IDOR Insecure Direct Object Reference'ı Bütünüyle Anlamak MDISEC
IDOR nedir ve nasıl çalışır? Bu makalede Insecure Direct Object Reference açıklarını tanıyacak ve savunma tekniklerini öğreneceksiniz.
📝 Önsöz & Yasal Uyarı
Yasal Uyarı: Bu yazı, yalnızca eğitim amaçlıdır. Burada öğrenilen bilgilerin kötüye kullanılması yasalara aykırıdır. Lütfen bu bilgileri yalnızca güvenlik eğitimi ve savunma amaçlı kullanın. Kötü niyetli kullanımlar yasal sonuçlar doğurabilir.
-
Bu yazı, güvenliğimizi sağlamak için yazılmıştır. Mehmet İnce’nin (mdisec)
Web Security 101 0x02 | IDOR Insecure Direct Object Reference'ı Bütünüyle Anlamak
adlı eğitiminden (twitch | youtube) öğrenilen bilgilerin derlenmiş toparlanmış blog haline getirilmiş halidir. -
Daha iyi bir deneyim için videoyu izleyerek ve kendi notlarınızı çıkararak (özellikle a4 kağıdı ve mavi tükenmez kalem kullanarak) öğrenmeniz şiddetle önerilir (ben tarafından).
-
Aklınıza takılan yerler olunca ya da tekrar etmek istediğinizde bu sayfayı yer imlerinize ekleyip oradan bir tıkla bakmanız tavsiye olunur (yine ben tarafından). Aşağıda video var, iyi Seyirler, iyi okumalar, iyi öğrenmeler…
🧭 Bölüm 0: Giriş - Neden IDOR Önemlidir?
🌐 Web Uygulamalarının Temel Yapısı
Modern web uygulamaları, kullanıcılardan sürekli veri alan ve bu verileri işleyen karmaşık sistemlerdir. Bu sistemlerin güvenliğini değerlendirirken en kritik noktalardan biri kullanıcı girdileridir. Bir web uygulamasının güvenliğini test ederken, bu girdilerin nasıl işlendiği ve kontrol edildiği büyük önem taşır.
🔍 Basit vs Karmaşık Web Uygulamaları
Basit bir web uygulaması düşünelim:
- Sadece sunucunun saatini gösteriyor
- Kullanıcıdan veri almıyor
- Tek işlevi var: Saati göstermek
Böyle bir uygulamada güvenlik açığı bulmak zordur çünkü:
- Kullanıcı etkileşimi minimumdur
- Veritabanı işlemleri yoktur
- Kullanıcıdan veri alınmaz
Ancak modern web uygulamaları çok daha karmaşıktır:
- Kullanıcı kayıtları ve profilleri
- Adres ve ödeme bilgileri
- Sipariş işlemleri
- Kullanıcı verilerinin işlenmesi
Bu karmaşık yapı, güvenlik açıklarına daha fazla zemin hazırlar. Özellikle kullanıcı verilerinin işlendiği noktalarda yetkilendirme ve erişim kontrollerinin doğru yapılandırılması kritik önem taşır.
🛍️ E-ticaret Sitesi Örneği
Bir e-ticaret sitesinde alışveriş yaparken birden fazla adres kaydedebildiğimizi düşünelim. Bu adresler veritabanında saklanır ve sipariş tamamlama ekranında bir açılır menüde gösterilir. Sistem bu adresleri getirmek için /api/addresses?user_id=123
gibi bir API çağrısı yapar. Eğer sistemde yetki kontrolü eksikse, user_id
parametresini değiştirerek (örneğin 456 yaparak) başka kullanıcıların adreslerine erişebiliriz. Bu, Second Order IDOR örneğidir; çünkü ilk aşamada adresleri listeleme, ikinci aşamada ise bu adreslerden birini seçip siparişi tamamlama işlemi yapılır ve her iki aşamada da yetki kontrolü eksiktir.
🎯 IDOR’un Önemi
IDOR zafiyetleri özellikle önemlidir çünkü:
- Tespit edilmesi zordur
- Otomatik güvenlik taramalarında genellikle yakalanamaz
- İş mantığı (business logic) seviyesinde ortaya çıkar
- Veri sızıntılarına ve yetkisiz erişimlere yol açabilir
- Mikroservis mimarilerinde daha sık görülür
🔍 Bölüm 1: IDOR Nedir?
Bir web uygulaması çalışırken kullanıcıdan aldığı bilgiler ile veritabanındaki verilere erişim sağlayıp bu veriyi okuyarak kullanıcıya gösterme, güncelleme, silme ya da değiştirme gibi işlemler yapmaktadır. Ancak uygulamanın bu işlemleri yapabilmesi için bazı kurallar bulunmaktadır.
Örneğin bu kurallardan biri şöyle olabilir: Başka bir kullanıcının adresini görememeliyiz. Adreslerim kısmına geldiğimizde sadece kendi adreslerimizi görebilmeliyiz ve başkasının adresini görememeliyiz. Eğer bu kuralı atlatıp başkasının adresini görebilirseniz, bu durum IDOR kategorisine giren bir zafiyet örneği olur.
🔍 Pratik Bir Örnek
İki adet kullanıcı oluşturalım ve bu kullanıcıların adreslerini ekleyelim.
MDI-1 Kullanıcısı için adresini silmek istediğimizde şöyle bir request ile karşılaşırız:
1
2
3
4
5
6
7
8
9
10
11
12
GET /address/delete/15 HTTP/1.1
Host: 18.132.45.78
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW46ZWFzeWVuYWN... ← (Base64 ile encode edilmiş "kullanıcıadı:şifre")
Connection: close
Referer: http://18.132.45.78/address
Cookie: XSRF-TOKEN=eyJpdiI6InIxU...
session=eyJpdiI6InUz...
Upgrade-Insecure-Requests: 1
Yukarıdaki HTTP GET
isteği incelendiğinde, address
isimli bir controller’ın bulunduğu, bu controller içinde delete
isimli bir fonksiyonun yer aldığı ve 15
değerinin bu fonksiyona bir parametre olarak iletildiği görülüyor. Bu değer büyük olasılıkla veritabanındaki bir adres kaydının id
bilgisini temsil ediyor. Yani bu istek, ID değeri 15
olan adres kaydını silmek amacıyla yapılmıştır.
📊 Veritabanı Yapısı
SQL Veritabanı yapısını şu şekilde düşünebiliriz (adresler tablosu):
1
2
3
4
5
6
7
CREATE TABLE addresses (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(255),
adres_bilgisi TEXT,
sehir_id INT
);
Örnek veriler:
id | user_id | title | adres_bilgisi | sehir_id |
---|---|---|---|---|
12 | 2 | Ev | MDI-2 Adresi | 34 |
15 | 1 | İş | MDI-1 Adresi | 6 |
🔍 IDOR Testi: Başka Kullanıcının Adresini Silme Denemesi
Şimdi IDOR zafiyetini test etmek için başka bir kullanıcının adresini silmeyi deneyelim. Bunun için request’teki ID’yi 12 olarak değiştiriyoruz:
1
2
3
4
5
6
7
8
9
10
11
GET /address/delete/12 HTTP/1.1
Host: 18.132.45.78
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW46ZWFzeWVuYWN...
Connection: close
Referer: http://18.132.45.78/address
Cookie: XSRF-TOKEN=eyJpdiI6I...; session=eyJpdiI6ImpGUU...
Upgrade-Insecure-Requests: 1
Bu isteğe gelen yanıt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HTTP/1.1 302 Found
Date: Mon, 20 Apr 2020 17:36:47 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Authorization
Cache-Control: no-cache, private
Location: http://18.132.45.78/address
Set-Cookie: XSRF-TOKEN=eyJpdiI6ImIpQCUzva3o4YWFDCVwVdEhhVKlycGVBPT01LCJ2YnX1ZSI6IJFnTN92YlVO6XJoNUMwaUdsZXJVVEJ; expires=Mon, 20-Apr-2020 19:36:47 GMT; Max-Age=7200; path=/
Set-Cookie: session=eyJpdiI6IlgzQQBLU1hrvHAAQOV1YbGUlSVpLVUE9PSIsInZhbHVlIjoi2lBYNmdlbTRCcn1mSWKQZzJcCQXAAZ; expires=Mon, 20-Apr-2020 19:36:47 GMT; Max-Age=7200; path=/; HttpOnly
Content-Length: 352
Connection: close
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="1;url=http://18.132.45.78/address" />
<title>Redirecting to http://18.132.45.78/address</title>
</head>
<body>
Redirecting to <a href="http://18.132.45.78/address">http://18.132.45.78/address</a>.
</body>
</html>
Burada gördüğünüz üzere 302 Found kodu ile cevap verildi. Uygulama arayüzüne geri geldiğimizde ise bizi şu şekilde bir mesaj karşılıyor:
Uygulamada yer alan ifadede ise Authorization failure
mesajı bulunuyor. Bu mesajı görüyor olmanız, veriyi silmediğiniz anlamına gelmez. Bu nedenle verinin silinip silinmediğini tekrar kontrol etmeliyiz. Hesaptaki adresleri kontrol ettiğimizde silme işleminin gerçekleşmediğini görüyoruz.
🔍 Var Olmayan ID Testi
Veritabanında bulunmayan bir ID değeri girersek ne olur?
1
2
3
4
5
6
7
8
9
10
11
GET /address/delete/1892378912319247189248 HTTP/1.1
Host: 18.132.45.78
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW462WPzeWNvDmVlYXN5Z293aXseW9lbGV0bWVnbw==
Connection: close
Referer: http://18.132.45.78/address
Cookie: XSRF-TOKEN=eyJpdiI6IlB0OHVzempmVktDdTMwcnB5TGJOWEE9PSIsInZhbHVlIjoiRWX...
Upgrade-Insecure-Requests: 1
Bu isteğe gelen yanıt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
HTTP/1.1 404 Not Found
Date: Mon, 20 Apr 2020 17:38:54 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Authorization
Cache-Control: no-cache, private
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Not Found</title>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; }
.container { max-width: 500px; margin: 100px auto; text-align: center; }
h1 { font-size: 2rem; color: #ef4444; }
p { color: #6b7280; margin: 1rem 0; }
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<h2>Not Found</h2>
<p>The requested resource could not be found.</p>
<p>Address ID: 189237891231924718924</p>
<a href="/address">← Back to Addresses</a>
</div>
</body>
</html>
💻 Uygulama Kodu Analizi
Uygulamanın davranışlarına göre oluşturduğumuz tahmini kod yapısı:
1
2
3
4
5
6
7
8
9
10
11
class AddressController extends Controller {
public function delete($address_id) {
// Adresin varlığını kontrol et
if (!AddressModel->checkAddress($address_id)) {
return redirect('/', 404);
}
// Adresi sil
AddressModel->deleteAddress($address_id);
}
}
📝 Test Sonuçları ve Değerlendirme
Bu testler sonucunda, uygulamanın IDOR zafiyetine karşı korumalı olduğunu görüyoruz. Çünkü:
- Başka kullanıcının adresini silmeye çalıştığımızda yetki hatası alıyoruz
- Var olmayan bir ID ile istek yaptığımızda 404 hatası alıyoruz
- Sistem, adresin varlığını ve kullanıcının yetkisini kontrol ediyor
Yani IDOR, bir kullanıcının, ID’leri değiştirerek, yetkisi olmayan veri ya da kaynaklara erişebilmesine neden olan bir güvenlik açığıdır.
🎯 Bölüm 2: IDOR vs Missing Function Level Access Control
Bu iki zafiyet türü sıklıkla karıştırılır. Aralarındaki temel farkları inceleyelim:
🔑 IDOR
- Veriye yetkisiz erişim ile ilgilidir
- Örnek: Başka kullanıcının adresini görüntüleme/silme
🔒 Missing Function Level Access Control
- Fonksiyon seviyesinde yetki kontrolü eksikliğidir
- Örnek: Normal kullanıcının admin fonksiyonlarına erişimi
Şöyle bir request üzerinden ilerleyelim, delete yerine update yazalım:
1
2
3
4
5
6
7
8
9
10
11
GET /address/update/12 HTTP/1.1
Host: 18.132.45.78
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW462WPzeWNvDmVlYXN5Z293aXseW9lbGV0bWVnbw==
Connection: close
Referer: http://18.132.45.78/address
Cookie: XSRF-TOKEN=eyJpdiI6IlB0OHVzempmVktDdTMwcnB5TGJOWEE9PSIsInZhbHVlIjoiRWX...
Upgrade-Insecure-Requests: 1
Bu isteğe gelen yanıt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 404 Not Found
Date: Mon, 20 Apr 2020 17:38:54 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Authorization
Cache-Control: no-cache, private
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Not Found</title>
</head>
</html>
404 verdiğini görüyoruz. Bu şu anlama gelebilir:
update
adında bir metot yoktur12
id’li bir değer yoktur.
Bunu anlamak için fonksiyon adını anlamsız bir karakter dizisine çevirelim. Update yerine mesela fr0stb1rd
yazalım:
1
2
3
4
5
6
7
8
9
10
11
GET /address/fr0stb1rd/12 HTTP/1.1
Host: 18.132.45.78
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW462WPzeWNvDmVlYXN5Z293aXseW9lbGV0bWVnbw==
Connection: close
Referer: http://18.132.45.78/address
Cookie: XSRF-TOKEN=eyJpdiI6IlB0OHVzempmVktDdTMwcnB5TGJOWEE9PSIsInZhbHVlIjoiRWX...
Upgrade-Insecure-Requests: 1
Bu isteğe gelen yanıt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 404 Not Found
Date: Mon, 20 Apr 2020 17:38:54 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Authorization
Cache-Control: no-cache, private
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Not Found</title>
</head>
</html>
Buradan anlıyoruz ki sorun fonksiyon adında değil. Intruder aracılığıyla ‘delete’ ifadesi yerine başka ifadeler yazarak sistemin ne cevap verdiğini test edebiliriz.
İstekte sağ tıkalyıp intruder’e iletelim, delete
kısmını seçerek add diyelim ve $
simgeleri arasına aldığıızdan emin olalım. Attack type olarak sniper seçelim.
Options’a gelelim, Payload options kısmında Add from list
seçeneğinden Form field names
deneyerek ilerleyebiliriz.
Görebileceğiniz gibi sık kullanılan kelimeler geldi. Şuna benzer bir liste olacak:
1
access, admin, administration, allow, auth, debug, deny, id, lock, locked, login, success, test, uid, unlock, unlocked, user, valid, secret, visible, hidden, hide, show, source, backdoor, root, content, update, edit
İşte yaptığımız işlemler neticesinde aldığımız sonuçlar:
Bu sonuçları incelediğimizde ‘edit’ ifadesi için alınan değerin uzunluğu diğerlerinden farklı. Bu yüzden sistemde kontrol edip bu ifadenin bize hangi sonuçları getirdiğini görmemiz gerekir.
Burada veri olmasına rağmen Intruder kısmında neden 404 verildiğini düşünmemiz gerekir. Burada ilk deneme yapılırken ‘delete’ fonksiyonuna GET isteği attığımız için 15 id’si siliniyor. Edit fonksiyonuna çağrı yaptığımızda bu yüzden 404 veriyor. Çünkü delete ile edit yapmaya çalışıyoruz. Dolayısıyla burada ilk deneme için ‘delete’ ifadesi yerine karşılığı olmayan bir ifade yazarak verinin silinmesini engellemeliyiz.
Yeni bir adres oluşturalım, idsi 16 olacak. delete tuşuna basıp intercept edelim, isteği kesip intruder’a gönderelim, delete
kısmını seçip add diyerek invalidmethodname
yapılmasını sağlayalım, wordlist’i tekrar seçelim ve saldırıyı tekrar başlatalım:
Şimdi request istediğimiz gibi geldi. edit
field name’i için de 200 döndüğünü görebiliyoruz:
İncelediğimiz web uygulamasında bize edit
ile ilgili bir fonksiyon işlemi sunulmamasına rağmen bu fonksiyona erişebildik (/address/edit/17
). Bu durum Missing Function Level Access Control zafiyeti olarak kabul eedilir.
IDOR zafiyeti ise başkasına ait verileri görebildiğimiz durumlarda geçerlidir. Örneğin id
değeri olarak 17 yerine 5 yazdığımızda başkasına ait verileri görebiliyorsak IDOR zafiyeti var demektir.
Özetle, bize sunulmamış edit
fonksiyonuna erişmek Missing Function Level Access Control, başkasının verisini görmek ise IDOR olarak geçer.
Burada edit
fonksiyonuna ulaşabilmemiz büyük ihtimalle şöyle bir backend kodu yazılmış olmasından kaynaklanıyordur:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AddressController extends Controller {
public function edit($address_id){
$address = AddressModel->find($address_id);
dd($address);
}
public function delete($address_id){
if(!AddressModel->checkaddress($address_id)){
redirect('/', 404)
}
AddressModel->deleteAddress($address_id);
}
}
Gördüğünüz gibi edit
fonksiyonu için herhangi bir yetkilendirme yapılmamış. Bu şekilde kod yazmayın. Çok ayıp.
🌐 Bölüm 3: Second Order IDOR
Başka bir örneğe bakalım. Sipariş vermeye çalışalım ve 2 ürün sepete ekleyip sipariş detayı sayfasına gidelim. Checkout kısmında adres bilgilerini görebiliyoruz. Demek ki burası adresin kullanıldığı bir başka yer. Herhangi bir ürünü sepete ekleyip satın alma aşamasında adres listesi geliyor.
Siparişi verirken isteği intercept edip burp suite’den bakalım:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /cart/checkout HTTP/1.1
Host: 18.132.45.78
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 83
Origin: http://18.132.45.78
Authorization: Basic YWRtaW46ZWF6eW5vbVlYXN5Z293aWxsonW1bGV0bWVnbw==
Connection: close
Referer: http://18.132.45.78/cart/checkout
Cookie: XSRF-TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NisImVhcCldcalZaZCtQK1hNWVpZdlZKNjBJOTgplVK9hMFh4a2Z1UZh
Upgrade-Insecure-Requests: 1
_token=la5sRXFdoBDBIarOdeJ3J4l6ito1zbDHRRUdEJ59fO&quantity=5&$50=1&coupon=&address=17
Burada _token
ifadesinde id
değerini değiştirirsek başka bir kullanıcının adres bilgisine erişebilir miyiz?
Örneğin ...&address=17
yerine ...&address=19
yazarsak, 17 yerine 19 id’li adresi görebilir miyiz? Yani burada sipariş özelliğini kullanarak başka bir kullanıcının adres bilgisine erişebilir miyiz?
Vee evet! Görebileceğiniz gibi başkasının adresini görmeyi başardık. Tabii verdiğimiz siparişler de oraya gitmiş oldu :D
Bu işlem iki aşamalı olduğu için adı Second Order Insecure Direct Object Reference
şeklindedir. Çünkü adresi seçtiğimiz endpoint ile değiştirdiğimiz id değerini gördüğümüz kısım farklıdır. Olay tek bir request-response döngüsü içinde yaşanmamaktadır.
Özetle, önce /cart/checkout
endpointine yaptığımız bu istekle adresi değiştirdik:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /cart/checkout HTTP/1.1
Host: 18.132.45.78
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 83
Origin: http://18.132.45.78
Authorization: Basic YWRtaW46ZWF6eW5vbVlYXN5Z293aWxsonW1bGV0bWVnbw==
Connection: close
Referer: http://18.132.45.78/cart/checkout
Cookie: XSRF-TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NisImVhcCldcalZaZCtQK1hNWVpZdlZKNjBJOTgplVK9hMFh4a2Z1UZh
Upgrade-Insecure-Requests: 1
_token=la5sRXFdoBDBIarOdeJ3J4l6ito1zbDHRRUdEJ59fO&quantity=5&$50=1&coupon=&address=17
Sonra siparişin detayına gittiğimizde yani /user/15
endpointinde bu yanıtı alıyoruz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HTTP/1.1 200 OK
Date: Mon, 20 Apr 2020 18:03:11 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Authorization,Accept-Encoding
Cache-Control: no-cache, private
Set-Cookie: XSRF-TOKEN=...
Connection: close
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
</head>
</html>
İşbu işlem iki aşamalı olduğu için adı Second Order Insecure Direct Object Reference
oluyor.
🛡️ Bölüm 4: IDOR Neden Web Dünyasındaki En Zor Zafiyettir?
Güvenlik zafiyetleri genel olarak iki ana kategoriye ayrılır: teknik zafiyetler ve iş mantığı (business logic) zafiyetleri. Burada odaklandığımız konu iş mantığı zafiyetleri. Bu tür zafiyetlerde, klasik saldırı kodları ya da kötü amaçlı payload’lar kullanılmaz. Örneğin SQL Injection, XSS veya SSRF gibi teknik açıklar, genellikle belirli kötü niyetli kod parçacıkları (payload) ile ortaya çıkar. Ancak IDOR gibi iş mantığı zafiyetlerinde böyle bir payload bulunmaz.
IDOR’da saldırgan, uygulamanın işleyiş mantığını kullanarak sadece bir parametreyi - mesela bir kullanıcı ya da nesne ID’sini - değiştirmekle yetkilendirilmemiş verilere erişim sağlayabilir. Örneğin, sistemde kullanıcı ID’si 17 iken, bu değeri 15 yaparak başka bir kullanıcının bilgilerine ulaşabilir. Bu durum, kodun kendisi içinde açık bir hata gibi gözükmez ve statik analiz ya da otomatik kaynak kodu tarama araçları tarafından tespit edilmesi son derece zordur. Çünkü burada sorun, kodun güvenlik açıklarından çok, iş mantığının yanlış tasarlanmasından kaynaklanır.
Özetle, iş mantığı zafiyetleri, teknik açıklara kıyasla çok daha zor tespit edilir ve engellenir; zira bunlar doğrudan kötü niyetli koddan değil, uygulamanın çalışma biçiminden kaynaklanan güvenlik problemleridir.
🛡️ Bölüm 5: Banka IDOR Zafiyeti: Çapraz Satış Saldırısı
MDISEC’in Türkiye’de 2012 yılında bir bankanın canlı ortamında bulduğu gerçek bir açığı anlatalım. Gece 3’te tüm bankayı ayağa kaldırmıştır.
🏦 Senaryo Özeti
Bir bankada çapraz satış (kıymetli metal dönüştürme) işleminde IDOR zafiyeti bulunmaktadır. Saldırgan, başkasının hesabından para çekebiliyor.
👥 Aktörler
- Ahmet: Saldırgan (kendi hesabı var)
- Mehmet: Kurban (hedef hesap)
🔄 Standart Çapraz Satış Süreci
1
2
3
4
5
6
1. Miktar girişi (örn: 1000 TL)
2. Kaynak hesap seçimi (Gümüş hesabı)
3. Hedef hesap seçimi (Altın hesabı)
4. "Onaylıyor musunuz?" ekranı
5. "Evet" onayı → OTP gönderimi
6. OTP doğrulama → İşlem tamamlanır
🔴 Saldırı Senaryosu
Faz 1: İlk Saldırı Denemesi (Session Sonlanır)
Adım 1: Ahmet Normal İşlem Başlatır
1
2
3
4
5
6
POST /api/exchange/confirm
{
"amount": 1000,
"source_account": "AHMET_GUMUS_12345",
"target_account": "AHMET_ALTIN_67890"
}
Adım 2: Request’i Değiştirme (IDOR Saldırısı)
1
2
3
4
5
6
POST /api/exchange/confirm
{
"amount": 1000,
"source_account": "MEHMET_GUMUS_54321", ← Mehmet'in hesabı!
"target_account": "AHMET_ALTIN_67890" ← Ahmet'e gelsin
}
Sonuç: ❌ Session sonlanır, saldırı başarısız
Faz 2: Timing Saldırısı (Başarılı)
Adım 1: Tekrar Normal İşlem
Ahmet normal işlemi tekrar başlatır ve OTP aşamasına kadar gelir.
Adım 2: OTP Öncesi Manipulation
OTP geldiği anda, hemen önceki request’i tekrar gönder:
1
2
3
4
5
6
POST /api/exchange/confirm
{
"amount": 1000,
"source_account": "MEHMET_GUMUS_54321", ← Mehmet'in hesabı
"target_account": "AHMET_ALTIN_67890" ← Ahmet'e transfer
}
Sonuç: ✅ HTTP 200 - İşlem kabul edilir!
Adım 3: OTP Doğrulama
1
2
3
4
5
POST /api/otp/verify
{
"otp_code": "123456",
"transaction_id": "TXN_789"
}
Sonuç: ✅ OTP doğrulanır, işlem tamamlanır!
🔍 Zafiyet Analizi
🐛 Güvenlik Açığının Nedeni
Problem | Açıklama |
---|---|
Zayıf Session Management | Oturum kontrolü yeterince sıkı değil |
Race Condition | OTP sürecinde timing zafiyeti |
Yetersiz Authorization | Hesap sahipliği kontrolü eksik |
State Management Hatası | İşlem durumu doğru takip edilmiyor |
🎯 Saldırı Diyagramı
⚠️ Risk Değerlendirmesi
- Mali Zarar: Kullanıcıların parası çalınabilir
- Güven Kaybı: Banka itibarı zarar görür
- Yasal Sorumluluk: Regülasyon ihlalleri
- Sistemik Risk: Tüm müşteriler etkilenebilir
🌐 Bölüm 6: Mikroservis Mimarilerinde IDOR
Bir e-ticaret platformunda kullanıcı bilgileri merkezi bir yerde tutulur ve bu bilgilere erişim gerektiren birden fazla farklı uygulama olabilir. Bu durumda, kullanıcı bilgilerini sağlayan ayrı bir servis oluşturulması gerekir. Bu servis, kullanıcı verilerini güvenli ve tutarlı bir şekilde diğer uygulamalara sunar.
Ancak, bu yapının doğru şekilde tasarlanması çok önemlidir. Özellikle kullanıcı yetkilendirme mekanizmaları, kimlerin hangi verilere erişebileceğini net biçimde belirlemeli ve denetlemelidir. Yetkilendirme doğru yapılmazsa, örneğin bir kullanıcının başka bir kullanıcının bilgilerine izinsiz erişebilmesine yol açan IDOR gibi güvenlik açıkları ortaya çıkabilir.
Bu yüzden, kullanıcı bilgilerini dönen servisin mimarisi katmanlı, güvenli ve erişim kontrolleri sıkı şekilde uygulanmış olmalıdır. Böylece her uygulama sadece izin verilen kullanıcı verilerine ulaşabilir ve sistem genelinde veri güvenliği sağlanmış olur.
Bir örnek üzerinden anlatalım.
🔐 Bölüm 7: Banka Adres Bilgisi Mikroservisi Senaryosu
🧩 Mimarideki Yapı:
- Banka, müşteri adres bilgilerini yöneten bir “Adres Mikroservisi” kullanıyor.
- Mikroservisin bir endpoint’i var:
GET /adresler/{musteri_id}
Bu çağrı, ilgili müşteri kimliği ile ilişkili adresleri döndürüyor.
🚨 Problemli Durum (IDOR Açığı)
👤 Örnek Kullanıcılar:
- Ahmet: müşteri_id =
1234
- Mehmet: müşteri_id =
5678
🔧 Mikroservisin Backend Kodları (örnek olarak Node.js/Express):
1
2
3
4
5
6
7
8
9
app.get('/adresler/:musteri_id', (req, res) => {
const musteriId = req.params.musteri_id;
// !!! Dikkat: Herhangi bir yetki kontrolü yapılmıyor
db.query('SELECT * FROM adresler WHERE musteri_id = ?', [musteriId], (err, results) => {
if (err) return res.status(500).send('Hata');
res.json(results);
});
});
💥 IDOR Güvenlik Açığı Nasıl Ortaya Çıkar?
Ahmet sisteme giriş yapmış ve kendi adresini görüntülemek istiyor. Ancak manuel olarak isteği şu şekilde değiştiriyor:
1
GET /adresler/5678 → yani Mehmet'in adres bilgisi
Sunucu bu isteği herhangi bir kontrol yapmadan çalıştırıyor ve Mehmet’in adres bilgilerini Ahmet’e gösteriyor.
🔐 Bu Kontrol Nerede Yapılmalı?
- Doğru yer: Servis katmanında (mikroserviste) yetki kontrolü mutlaka yapılmalı.
- Mikroservis, “bu müşteri_id bilgisi gerçekten bu kullanıcının mı?” sorusunu doğrulamadan veri döndürmemeli.
✅ Güvenli Yaklaşım
1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/adresler/:musteri_id', (req, res) => {
const musteriId = req.params.musteri_id;
const oturumAcanKullaniciId = req.user.musteri_id; // JWT'den ya da session'dan geliyor
// Yetki kontrolü
if (musteriId !== oturumAcanKullaniciId) {
return res.status(403).send('Erişim reddedildi');
}
db.query('SELECT * FROM adresler WHERE musteri_id = ?', [musteriId], (err, results) => {
if (err) return res.status(500).send('Hata');
res.json(results);
});
});
Başka Bir Mikrosistem Mimarisi Örneği
📌 Sonuç
- IDOR, kimlik doğrulaması yapılmış olsa bile yetki kontrolü yapılmazsa kritik verilere erişime neden olur.
- Bu kontrol, UI veya API Gateway katmanına bırakılmamalı, mikroservisin kendisinde uygulanmalıdır.
- Mikroservis bağımsız çalışabilmelidir, bu nedenle gelen kullanıcının gerçekten o kaynağa erişim hakkı var mı diye kendi içinde kontrol etmelidir.
🔍 Bölüm 8: Aynı JSON Anahtarının İki Kez Kullanılması Senaryosu
Uygulamalar arası veri iletimi genellikle JSON formatında yapılır. Ancak bazı durumlarda istemciden gelen JSON verisinde aynı anahtar (örneğin user_id
) birden fazla kez yer alabilir:
1
2
3
4
5
{
"operation": "get_user_info",
"user_id": "5",
"user_id": "6"
}
Bu yapı teknik olarak geçersiz JSON‘dur çünkü her anahtar yalnızca bir kez tanımlanmalıdır. Ancak birçok programlama dili bu durumu görmezden gelir ve sadece bir değerini dikkate alır hangisinin dikkate alınacağı ise dile göre değişir.
Bu farklılık, özellikle mikroservis mimarisine sahip yapılarda yetkilendirme (authorization) ile veri işleme işlemlerinin farklı bileşenlerde çalıştığı durumlarda, IDOR zafiyetine neden olabilir.
🧨 Zafiyet Nasıl Oluşur?
- Kullanıcı, yukarıdaki gibi bir JSON gönderir.
- Yetki kontrolü yapılan servis (örneğin Python tabanlı),
user_id
olarak sonuncu değeri (6
) alır ve erişim izni verir. - Ancak bilgiyi çeken mikroservis (örneğin PHP),
user_id
olarak ilk değeri (5
) alır ve farklı bir kullanıcıya ait bilgiyi döner. - Sonuç olarak, yetki verilen kullanıcı başka bir kullanıcının verisine erişmiş olur.
Bu senaryo, Mehmet İnce’nin paylaştığı gerçek bir vakada yaşanmıştır.
⚖️ Farklı Programlama Dillerinin Davranışı
Programlama Dili | Varsayılan JSON Parslama Davranışı | Açıklama |
---|---|---|
Python | Son anahtar geçerli kabul edilir ("6" ) |
json.loads() son değeri alır |
JavaScript | Son anahtar geçerli kabul edilir ("6" ) |
Nesne literalinde sonuncu değeri alır |
PHP | Son anahtar geçerli kabul edilir ("6" ) |
json_decode() son değeri alır |
Java | Son anahtar geçerli kabul edilir ("6" ) |
org.json , Jackson son değeri kullanır |
Go (Golang) | Derleme hatası (map literal içinde tekrarlanamaz) | Go’da map tanımı tekrara izin vermez |
Rust | Genelde hata vermez, sonuncu değeri alır (serde) | serde_json sonuncuyu alır |
C# (.NET) | Son anahtar geçerli kabul edilir ("6" ) |
System.Text.Json ve Newtonsoft.Json aynı davranır |
Ruby | Son anahtar geçerli kabul edilir ("6" ) |
JSON.parse sonuncuyu alır |
🎯 Diyagram Gösterimi
🧩 Bölüm 9: BurpSuite AuthMatrix Eklentisi
AuthMatrix, Burp Suite eklentilerinden biridir. Farklı kullanıcı rollerine karşı erişim kontrolünü otomatik veya yarı otomatik test eder. HTTP isteklerinin (GET, POST vb.) farklı kullanıcı rollerinde nasıl sonuçlandığını karşılaştırır. Yetkisiz erişim (IDOR, yatay/dikey yetki yükseltme) problemlerini bulmamıza yardımcı olur.
Şimdi birlikte uygulayalım. (Admin, Moderator, Normal, Anon kullanıcılar için)
Hazırlık: Kullanıcı Rolleri ve Oturumları Oluşturalım
-
Web uygulamanızda en az 4 farklı kullanıcı hesabı olsun:
admin
moderator
normal user
anonymous
(giriş yapmamış kullanıcı)
-
Bu kullanıcılarla giriş yapalım ve Burp Suite Proxy üzerinden oturum çerezlerini veya token’ları yakalayalım.
AuthMatrix Eklentisini Açalım
- Burp Suite > Extender > Extensions > AuthMatrix’i başlatalım.
- AuthMatrix sekmesi açılacak.
Kullanıcı Rolleri İçin Oturum Bilgilerini Girip Hazırlayalım
- AuthMatrix’de Users sekmesine geçelim.
- Her kullanıcı için yeni bir kullanıcı oluşturalım.
- Burp Suite’ten yakaladığımız oturum bilgilerini (cookie, header) ilgili kullanıcıya ekleyelim.
-
Örnekler:
- admin:
sessionid=abcd1234admin
- moderator:
sessionid=xyz567moderator
- normal:
sessionid=123normal
- anon: Cookie veya token yok, giriş yapmamış.
- admin:
Test Edilecek HTTP İsteklerini Ekleyelim
- AuthMatrix’de Requests sekmesine geçelim.
- Burp Proxy veya Repeater’dan test etmek istediğimiz istekleri sürükleyip bırakalım veya manuel ekleyelim.
- Örnek istekler:
/admin/dashboard
,/post/edit/123
, vb.
Matris Testini Çalıştıralım
- AuthMatrix’de tüm kullanıcılar için her isteği test edelim.
- “Run Tests” butonuna basalım.
- Sonuçları renklerle göreceğiz (yeşil, kırmızı); böylece farklı kullanıcı rollerindeki erişim durumlarını hızlıca inceleyebiliriz.
🧩 Bölüm 10: Autochrome
Sistemi test ederken birden fazla kullanıcı ile işlem yapmak gerekebileceğinden, bu süreci kolaylaştıracak etkili araçlardan biri de Autochrome‘dur. Bu araç sayesinde, farklı kullanıcıları tanımlayarak her biri için ayrı tarayıcı pencereleri açabilir ve işlemleri birbirinden bağımsız şekilde yürütebilirsiniz. Böylece oturumlar (cookie, token vb.) karışmadan kontrollü bir test ortamı oluşturabilirsiniz.
Autochrome, özellikle Burp Suite ile entegre şekilde çalışarak, çoklu kullanıcı oturumlarını daha pratik bir şekilde yönetmenize olanak tanır. Kullanıcı oturumlarının ayrıştırılması ve test senaryolarının net şekilde izlenebilmesi açısından büyük kolaylık sağlar.
Ayrıca, AuthMatrix aracıyla birlikte kullanıldığında etkinliği daha da artar. Oturum kontrolü, yetkilendirme testleri ve kullanıcılar arası geçişlerin takibi gibi işlemleri sistematik bir şekilde gerçekleştirmenizi sağlar.
Projeye aşağıdaki GitHub bağlantısından ulaşabilirsiniz: 🔗 https://github.com/nccgroup/autochrome
🔐 Ne İşe Yarar?
- Farklı kullanıcı rolleriyle giriş yapılmış tarayıcı pencereleri açmanı sağlar (ör: admin, user, mod).
- Bu pencerelerde giriş yapınca çerezleri otomatik olarak yakalar ve AuthMatrix’e gönderir.
- Böylece tek tek cookie kopyalama zahmeti ortadan kalkar.
🛠️ Nasıl Kurulur?
- Öncelikle Node.js kurulu olmalı. Terminali açalım ve şunu çalıştıralım:
1
npm install -g autochrome
- Kurulum tamamlandıktan sonra Autochrome’u başlatalım:
1
autochrome
Komut çalışınca ekranda bazı URL’ler göreceğiz.
🚀 Kullanımı Adım Adım Yapalım
Autochrome’u Başlatalım
Terminalde autochrome
yazalım. Aşağıdaki gibi çıktı alalım:
1
2
[+] AuthMatrix integration URL: http://localhost:8080/user1
[+] AuthMatrix integration URL: http://localhost:8080/user2
Tarayıcıları Açalım
Bu bağlantılara sırayla tıklayalım:
http://localhost:8080/user1
→ Admin hesabıyla giriş yapalım.http://localhost:8080/user2
→ Normal kullanıcı hesabıyla giriş yapalım.
Her sekmede farklı kullanıcı oturumu açmış oluruz.
AuthMatrix’te Otomatik Çerez Girişi
Burp Suite > AuthMatrix > Users sekmesine gidelim. Kullanıcı eklerken “Import from Autochrome” seçeneğini seçelim. Autochrome aktifse kullanıcıların cookie bilgisi otomatik olarak alınır.
🧠 Neden Autochrome Kullanmalıyız?
- Çoklu kullanıcı oturumlarını manuel yönetmek zaman alır, hata yaparız.
- Cookie kopyala/yapıştır hataları tamamen ortadan kalkar.
- AuthMatrix testlerini çok daha hızlı ve hatasız yaparız.
- Oturumlar birbirine karışmaz, her kullanıcı izole olur.
🧪 Örnek Senaryo
Rol | Tarayıcı (Autochrome) | AuthMatrix Kullanımı |
---|---|---|
Admin | localhost:8080/admin sekmesinde |
Cookie otomatik alınır |
Normal | localhost:8080/user sekmesinde |
Cookie otomatik alınır |
Anonim | Tarayıcıda giriş yapılmaz | Cookie girilmez (boş bırakılır) |
🧩 Bölüm 11: Firefox Multi-Account Containers
Firefox Multi-Account Containers, web güvenlik testlerinde özellikle IDOR ve yetkilendirme testlerinde kullanabileceğimiz çok güçlü bir araçtır. Bu eklenti sayesinde, aynı tarayıcıda farklı kullanıcı hesaplarıyla çalışabilir ve her bir hesabın oturumunu birbirinden izole edebiliriz.
🔐 Ne İşe Yarar?
- Her container (konteyner) kendi çerezlerini, yerel depolama alanını ve oturum bilgilerini saklar
- Farklı kullanıcı hesaplarını aynı anda açık tutabilirsiniz
- Her container’ı farklı bir renkle ayırt edebilirsiniz
- Container’lar arası geçiş yaparken oturumlar karışmaz
🛠️ Kurulum
- Firefox tarayıcısını açın
- Firefox Multi-Account Containers eklentisini yükleyin
- Firefox’u yeniden başlatın
🚀 Kullanımı
Container Oluşturma
- Adres çubuğunun yanındaki container simgesine tıklayın
- “New Container” seçeneğini seçin
- Container’a bir isim verin (örn: “Admin”, “Normal User”, “Test User”)
- Bir renk seçin
IDOR Testi İçin Kullanım
Örnek senaryo:
- “Admin” container’ında admin hesabıyla giriş yapın
- “Normal User” container’ında normal kullanıcı hesabıyla giriş yapın
- Her iki container’da da aynı endpoint’i test edin
- Farklı kullanıcı rollerinde nasıl davrandığını gözlemleyin
💡 İpuçları
- Her container’ı farklı bir renkle ayırt edin
- Container isimlerini anlamlı şekilde belirleyin
- Test senaryolarınızı container’lara göre organize edin
- Container’ları düzenli olarak temizleyin
🔄 AuthMatrix ile Entegrasyon
Firefox Multi-Account Containers, Burp Suite’in AuthMatrix eklentisi ile birlikte kullanıldığında çok güçlü bir test ortamı oluşturur:
- Her container’da farklı bir kullanıcı hesabı açın
- Burp Suite Proxy’yi aktif edin
- AuthMatrix’te her container için ayrı bir kullanıcı tanımlayın
- Test senaryolarınızı çalıştırın
🔄 FoxyProxy ile Entegrasyon
FoxyProxy, Firefox için geliştirilmiş güçlü bir proxy yönetim eklentisidir. Container’lar ile birlikte kullanıldığında, her kullanıcı rolü için ayrı proxy ayarları yapabilirsiniz.
🛠️ FoxyProxy Kurulumu
- Firefox tarayıcısını açın
- FoxyProxy eklentisini yükleyin
- Firefox’u yeniden başlatın
🎯 FoxyProxy’nin Özellikleri
- Birden fazla proxy sunucusunu yönetebilme
- Proxy’ler arasında hızlı geçiş yapabilme
- URL pattern’lerine göre otomatik proxy seçimi
- Container’lara özel proxy ayarları
💡 Üçlü Entegrasyon: Container + AuthMatrix + FoxyProxy
Bu üç aracı birlikte kullanarak çok güçlü bir test ortamı oluşturabilirsiniz:
- Container Hazırlığı
- Admin container’ı oluşturun
- Normal user container’ı oluşturun
- Test user container’ı oluşturun
- FoxyProxy Yapılandırması
1 2 3 4 5 6 7 8 9 10 11 12
{ "admin-proxy": { "pattern": "*target-domain.com*", "proxy": "127.0.0.1:8080", "container": "Admin" }, "user-proxy": { "pattern": "*target-domain.com*", "proxy": "127.0.0.1:8080", "container": "Normal User" } }
- AuthMatrix Yapılandırması
- Her container için ayrı kullanıcı profili oluşturun
- Test edilecek endpoint’leri ekleyin
- Test senaryolarını tanımlayın
- Test Senaryosu Örneği
Container | Kullanıcı Rolü | Test Edilecek Endpoint | Beklenen Sonuç |
---|---|---|---|
Admin | Admin | /api/users/123 | 200 OK |
Normal | Normal User | /api/users/123 | 403 Forbidden |
Test | Test User | /api/users/123 | 401 Unauthorized |
⚠️ Dikkat Edilmesi Gerekenler
- Container’lar arası geçiş yaparken oturumların karışmadığından emin olun
- Her container’ın kendi çerezlerini ve oturum bilgilerini sakladığını unutmayın
- Test sonuçlarını container’lara göre düzenli olarak kaydedin
- Container’ları düzenli olarak temizleyerek test ortamınızı temiz tutun
- Proxy ayarlarının doğru yapılandırıldığından emin olun
- AuthMatrix testlerini düzenli olarak çalıştırın ve sonuçları kaydedin
🎯 Örnek IDOR Test Senaryosu
- Hazırlık
- Her container’da ilgili kullanıcı hesabıyla giriş yapın
- FoxyProxy ayarlarını kontrol edin
- AuthMatrix’te test senaryolarını hazırlayın
- Test Adımları
- Admin container’ında endpoint’i test edin
- Normal user container’ında aynı endpoint’i test edin
- Test user container’ında aynı endpoint’i test edin
- AuthMatrix sonuçlarını analiz edin
- Sonuç Analizi
- Her kullanıcı rolü için alınan yanıtları karşılaştırın
- Yetkilendirme kontrollerini değerlendirin
- IDOR zafiyeti olup olmadığını belirleyin
Bu şekilde, Firefox Multi-Account Containers, AuthMatrix ve FoxyProxy’yi birlikte kullanarak:
- Her kullanıcı rolü için ayrı proxy trafiği
- Container’lar arası temiz geçiş
- Düzenli ve organize test ortamı
- Kolay izlenebilir test sonuçları
- Kapsamlı yetkilendirme testleri
elde edebilirsiniz.
📌 Özet & Sonuç
IDOR zafiyetleri, web uygulamalarında en sık karşılaşılan ve en tehlikeli güvenlik açıklarından biridir. Bu açıklar:
- Tespit edilmesi zor açıklardır
- Business Logic zafiyetleridir
- Mikroservis mimarilerinde daha sık görülür
- Yetkilendirme kontrollerinin eksikliğinden kaynaklanır
Bu tür zafiyetlere karşı en iyi savunma, güçlü yetkilendirme kontrolleri ve düzenli güvenlik testleridir.
Ayşe Bilge Gündüz’ün makalesini de okumayı unutmayın. Güzel ama ingiliççe.
Başka bir yazıda görüşmek üzere, esen kalın.