38980-vm/app-9w9pd00g5j41/ANONYMOUS_TRIP_SECURITY_FIX.md
2026-03-04 18:25:09 +00:00

7.9 KiB
Raw Permalink Blame History

Anonim Geziler Güvenlik Açığı Düzeltmesi

🔴 Sorun

Güvenlik Açığı

Önceki RLS politikaları, user_id IS NULL olan tüm anonim gezilere herkesin erişmesine izin veriyordu:

-- ❌ ESKİ VE GÜVENSİZ
USING (is_public = true OR auth.uid() = user_id OR user_id IS NULL);

Bu politika şu sorunlara yol açıyordu:

  1. Herkes başkasının anonim gezisini görebilir, düzenleyebilir ve silebilirdi
  2. Anonim geziler sahipsiz kalıyordu - kullanıcı giriş yaptıktan sonra gezisi kayboluyordu
  3. Veri tabanı kirliliği - eski anonim geziler hiç temizlenmiyordu

Etki

  • Gizlilik ihlali: Kullanıcıların anonim gezileri herkese açık
  • Veri kaybı: Login sonrası kullanıcı kendi gezisini bulamıyor
  • Performans: Sahipsiz geziler birikerek veritabanını şişiriyor

Çözüm

1. Token Tabanlı Erişim Sistemi

Her anonim gezi için benzersiz bir token oluşturulur ve localStorage'da saklanır:

// Gezi oluşturulurken
const anonymousToken = crypto.randomUUID();
localStorage.setItem(`trip_token_${tripId}`, anonymousToken);

2. Güvenli RLS Politikaları

Yeni politikalar sadece token sahibinin erişimine izin verir:

-- ✅ YENİ VE GÜVENLİ
CREATE POLICY "Seyahatleri görüntüleme"
  ON trips FOR SELECT
  USING (
    is_public = true 
    OR user_id = auth.uid()
  );

Anonim geziler için özel RPC fonksiyonları:

  • update_anonymous_trip(trip_id, token, updates) - Token ile güncelleme
  • delete_anonymous_trip(trip_id, token) - Token ile silme
  • get_anonymous_trip(trip_id, token) - Token ile okuma

3. Otomatik Ownership Transfer

Kullanıcı giriş yaptığında, anonim gezisi otomatik olarak hesabına bağlanır:

// AuthContext.tsx - onAuthStateChange
if (event === 'SIGNED_IN') {
  const currentTripId = localStorage.getItem('currentTripId');
  if (currentTripId) {
    await tripsApi.claimAnonymousTrip(currentTripId);
  }
}

4. Otomatik Temizlik

7 günden eski anonim geziler otomatik olarak silinir:

CREATE FUNCTION cleanup_old_anonymous_trips()
RETURNS INTEGER
AS $$
  DELETE FROM trips
  WHERE user_id IS NULL
  AND created_at < NOW() - INTERVAL '7 days'
$$;

📋 Değişiklikler

Database Migrations

00053_add_anonymous_trip_token_system.sql

  • trips tablosuna anonymous_token sütunu eklendi
  • Unique index oluşturuldu
  • Güvenli RLS politikaları eklendi
  • RPC fonksiyonları oluşturuldu:
    • claim_anonymous_trip(trip_id, token) - Ownership transfer
    • update_anonymous_trip(trip_id, token, updates) - Token ile güncelleme
    • delete_anonymous_trip(trip_id, token) - Token ile silme
    • get_anonymous_trip(trip_id, token) - Token ile okuma
    • cleanup_old_anonymous_trips() - Eski gezileri temizle

00054_fix_anonymous_trip_rls_policies.sql

  • RLS politikalarını optimize etti
  • Application layer'da token kontrolü için yapı oluşturdu

Frontend Değişiklikleri

src/db/api.ts

tripsApi.create()

// Anonim kullanıcı için token oluştur
if (!user) {
  anonymousToken = crypto.randomUUID();
  tripData = { ...trip, user_id: null, anonymous_token: anonymousToken };
  localStorage.setItem(`trip_token_${data.id}`, anonymousToken);
}

tripsApi.update()

// Anonim kullanıcı için RPC kullan
if (!user) {
  const token = localStorage.getItem(`trip_token_${tripId}`);
  return await supabase.rpc('update_anonymous_trip', {
    trip_id_param: tripId,
    token_param: token,
    updates: updates
  });
}

tripsApi.delete()

// Anonim kullanıcı için RPC kullan
if (!user) {
  const token = localStorage.getItem(`trip_token_${tripId}`);
  await supabase.rpc('delete_anonymous_trip', {
    trip_id_param: tripId,
    token_param: token
  });
  localStorage.removeItem(`trip_token_${tripId}`);
}

Yeni Fonksiyonlar:

  • claimAnonymousTrip(tripId) - Anonim geziyi kullanıcıya transfer et
  • cleanupOldAnonymousTrips() - Eski anonim gezileri temizle
  • canAccessTrip(tripId) - Geziye erişim kontrolü

src/contexts/AuthContext.tsx

// Login sonrası otomatik ownership transfer
if (event === 'SIGNED_IN') {
  const currentTripId = localStorage.getItem('currentTripId');
  if (currentTripId) {
    await tripsApi.claimAnonymousTrip(currentTripId);
  }
}

src/types/trip-ui.ts

export interface TripDataRaw {
  // ...
  anonymous_token?: string; // Yeni alan
}

🔒 Güvenlik Özellikleri

1. Token Güvenliği

  • UUID v4 kullanılır (128-bit rastgele)
  • localStorage'da saklanır (sadece aynı origin erişebilir)
  • Tek kullanımlık: Transfer sonrası token silinir

2. RLS Koruması

  • Kayıtlı kullanıcılar sadece kendi gezilerini görebilir
  • Anonim geziler sadece token ile erişilebilir
  • Public geziler herkese açık

3. Veri Temizliği

  • 7 günden eski anonim geziler otomatik silinir
  • Orphan data birikmesi önlenir

4. Ownership Transfer

  • Login sonrası otomatik transfer
  • Token doğrulaması ile güvenli transfer
  • Transfer sonrası token temizlenir

🧪 Test Senaryoları

Senaryo 1: Anonim Kullanıcı

  1. Kullanıcı giriş yapmadan gezi oluşturur
  2. Token localStorage'a kaydedilir
  3. Kullanıcı geziyi düzenleyebilir
  4. Kullanıcı geziyi silebilir
  5. Başka bir tarayıcıdan geziye erişilemez

Senaryo 2: Login Sonrası Transfer

  1. Anonim kullanıcı gezi oluşturur
  2. Kullanıcı giriş yapar
  3. Gezi otomatik olarak kullanıcıya transfer edilir
  4. Token temizlenir
  5. Gezi "Seyahat Planlarım"da görünür

Senaryo 3: Güvenlik Testi

  1. Kullanıcı A anonim gezi oluşturur
  2. Kullanıcı B aynı trip_id ile erişmeye çalışır
  3. Erişim reddedilir (token yok)
  4. Kullanıcı B geziyi düzenleyemez
  5. Kullanıcı B geziyi silemez

Senaryo 4: Temizlik

  1. 7 günden eski anonim gezi oluşturulur
  2. cleanup_old_anonymous_trips() çağrılır
  3. Eski gezi silinir
  4. Yeni anonim geziler korunur

📊 Performans

Öncesi

  • Tüm anonim geziler herkese açık
  • N+1 query problemi
  • Gereksiz veri transferi

Sonrası

  • Token kontrolü ile hızlı erişim
  • RPC fonksiyonları ile optimize edilmiş sorgular
  • Otomatik temizlik ile veritabanı boyutu kontrolü

🚀 Kullanım

Anonim Gezi Oluşturma

const trip = await tripsApi.create({
  title: 'Kapadokya Gezim',
  destination: 'Kapadokya',
  // ...
});
// Token otomatik oluşturulur ve kaydedilir

Anonim Gezi Güncelleme

await tripsApi.update(tripId, {
  title: 'Yeni Başlık'
});
// Token otomatik kontrol edilir

Anonim Gezi Silme

await tripsApi.delete(tripId);
// Token otomatik kontrol edilir ve temizlenir

Manuel Ownership Transfer

const success = await tripsApi.claimAnonymousTrip(tripId);
if (success) {
  console.log('Gezi başarıyla transfer edildi');
}

Eski Gezileri Temizleme (Admin)

const deletedCount = await tripsApi.cleanupOldAnonymousTrips();
console.log(`${deletedCount} adet eski gezi temizlendi`);

⚠️ Önemli Notlar

  1. Token Kaybı: Kullanıcı localStorage'ı temizlerse token kaybolur ve geziye erişemez
  2. Tarayıcı Değişimi: Farklı tarayıcıda token olmadığı için geziye erişilemez
  3. 7 Gün Sınırı: Anonim geziler 7 gün sonra otomatik silinir
  4. Public Geziler: Public yapılan geziler token olmadan da görüntülenebilir (ama düzenlenemez)

🔄 Migration Sırası

  1. 00053_add_anonymous_trip_token_system.sql - Token sistemi ve RPC fonksiyonları
  2. 00054_fix_anonymous_trip_rls_policies.sql - RLS politikalarını optimize et

📝 Sonuç

Bu güvenlik düzeltmesi ile:

  • Anonim geziler artık güvenli
  • Kullanıcılar giriş sonrası gezilerini kaybetmiyor
  • Veritabanı temiz kalıyor
  • Performans optimize edildi
  • Kullanıcı deneyimi iyileştirildi