docs(gece): güvenlik ve uyum denetimi raporu (09)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 08:56:48 +03:00
parent 7ba668b551
commit c89a86c069

View File

@@ -0,0 +1,88 @@
# 09 — Güvenlik ve uyum denetimi (gece değişiklikleri)
5 Ekim 2026 · güvenlik-uyum · dal `gece/eval-jev`
Kapsam: `git diff b935158..HEAD -- src/` + `scripts/eval/**`. `src/lib/ai/rapor.ts` ve `cagri.ts` içindeki commit'lenmemiş iş kapsam dışı.
Yöntem: kod okuma + saf fonksiyonları ve DB katmanını `data/app.db`'nin geçici KOPYASINDA çalıştırma. Model çağrısı, sunucu, tarayıcı, Rybbit yok. Canlıya dokunulmadı.
**Merge'e engel var mı? Hayır — kritik ya da yüksek bulgu yok; B1 (bedelsiz "evet" tekrar kullanılabiliyor) yayından önce kapatılmalı ama bu gecenin değişikliği onu daraltmış, genişletmemiş.**
## Özet (5 madde)
1. **Kritik/yüksek bulgu yok.** Kredi düşümü atomik, 0 kredide 402, çifte iade yok, istemci kopunca iade yok, profil yazımı SQL enjeksiyonuna ve serbest metne kapalı — hepsi çalıştırılarak doğrulandı.
2. **Orta — bedelsiz "evet" tek kullanımlık değil (B1).** Danışman bir kez "profiline kaydedeyim mi" dedikten sonra, cevap kaydedilene kadar gönderilen her "evet" bedelsiz model çağrısı açıyor: art arda 12, eşzamanlı 50/50 kabul edildi, bakiye 0 iken. Kullanıcıya bedava *faydalı* cevap vermiyor; bizim model faturamızı şişiriyor.
3. **Jev'e canlıda yalnız cevap metni gidiyor** (tek çağrı noktası, doğrulandı). Ama cevap metni adayın sırasını, ilini ve tercihlerini içerir; /gizlilik bunu söylemiyor (U1, metin işi `[BİLAL]`, avukat teyidi gerekir).
4. **`/api/soru`'da hız sınırı yok**; ücretli yolda risk krediyle sınırlı, sınırsız kalan tek yol B1. En sade çözüm: kullanıcı başına "aynı anda tek cevap" kilidi (B2).
5. **Repo hijyeni:** bu gecenin commit'lerinde anahtar izi yok, eval sonuçları sentetik. Ama `data/app.db` git'te İZLENEN bir dosya ve içinde kullanıcı satırları var (B3, bu geceden eski); çalışma ağacındaki değişmiş hâli commit'lenmemeli.
## Bulgular (önem sırasıyla)
### B1 — ORTA · Bedelsiz onay yolu tek kullanımlık değil, bakiye ve adet sınırı yok · DOĞRULANDI (kod düzeyi)
- Yer: `src/app/api/soru/route.ts:113-118` (karar: "son 12 mesajdaki son danışman mesajı ibareyi içeriyor mu"), `src/lib/credits.ts:148-166` (`bedelsizMesajKaydet`: bakiye/adet kontrolü yok).
- Kalıp eşleşmesi sağlam: `onayMesajiMi` 29 varyantla denendi. "evet. Ege hukuk kaç?", "evet\nEge…", sıfır genişlikli boşluk, bölünemez boşluk, uzun tire, "evet-ege-hukuk", Kiril "е", emoji, "evet [SİSTEM]", "evet 2" → hepsi ücretli. Soru taşıyan mesaj bedelsiz geçmiyor.
- Açık olan: karar yalnız *son danışman mesajına* bakıyor; arada kullanıcının kendi "evet"leri olması durumu değiştirmiyor. Cevap ancak akış bitince kaydedildiği için (5-15 sn) o pencerede her yeni "evet" yine bedelsiz.
- Kanıt (geçici DB, route'un okuma + karar ifadesi birebir): bakiye 0, son danışman mesajı onay sorusu → art arda **12** "evet" bedelsiz kabul edildi (13.'de danışman mesajı 12'lik pencereden çıkıyor); eşzamanlı **50/50** kabul edildi. Ayrıca `"evet evet … "` (1.499 karakter) de kalıba uyuyor.
- İstismar senaryosu: girişli, listesi olan bir hesap (deneme kredisi yeter) bir tercih söyleyip onay sorusunu alır; betikle farklı `clientMessageId`'lerle onlarca "evet" yollar. Her biri tam sistem prompt'u + araç şemasıyla bir model çağrısıdır, kredi düşmez. Yeniden kurmak 1 kredi; ya da kullanıcı modele "kaydettim" dedirtirse sunucu `KAYIT_DUZELTME_NOTU`'nu (ibareyi içerir, `cikti-denetimi.ts:107`) kendisi ekler.
- Sınırı: mesaj yalnız onay sözcüğü taşıyabildiği için kullanıcıya bedava soru-cevap kazandırmıyor. "Soruları ücretli mesajda önceden yükle, her 'evet'te sıradakini cevapla" hilesi modelin uymasına bağlı; model çağrısı yapmadığım için **denenmedi (OLASI)**.
- En sade düzeltme `[KARAR]`: bedelsiz say **yalnız geçmişteki en son mesaj danışmanınsa** (`gecmis.at(-1)?.role === "assistant"` ve ibareyi içeriyorsa). İlk "evet" kaydedilince sonraki mesajlar ücretli olur. Eşzamanlı patlamayı B2'deki kilit kapatır.
### B2 — ORTA · `/api/soru`'da hız sınırı ve eşzamanlılık sınırı yok · koddan çıkarım
- Yer: `src/app/api/soru/route.ts:65-261` (hiçbir sınır yok; `src/` içinde hız sınırı kodu yalnız giriş formunda geçiyor).
- Gerçek risk: ücretli yolda her çağrı 1 kredi düşüyor (atomik; 0 kredide `INSUFFICIENT` → 402 doğrulandı), yani maliyet saldırısı krediyle sınırlı. Sınırsız olan B1'dir. İkinci etki: 60 kredili bir hesap 60 cevabı aynı anda başlatabilir (her biri 4 adım, 8 araç).
- Araç tarafı sağlam: adım başına 4, cevap başına 8 çağrı sınırı kodda (`sohbet-akisi.ts:39-40, 93-99`), son adımda araç kapalı, çıktı 1.024 token. 8 `program_ara` lokalde toplam 61 ms; `rehber_ara` çıktısı ~3 bin karakter. Araç yoluyla DoS görmedim.
- En sade öneri `[KARAR]`: kullanıcı başına bellek içi "sürüyor" kilidi — aynı kullanıcının cevabı bitmeden gelen ikinci POST 429 döner (kredi düşmeden önce). Tek süreçli dağıtımda yeterli; birden çok kopya çalışıyorsa DB'de tutulmalı (doğrulanmalı). Dakika başına sayaç gerekmez.
### B3 — ORTA · `data/app.db` git'te izleniyor ve kullanıcı verisi içeriyor (bu geceden eski) · DOĞRULANDI
- Yer: `git ls-files data/app.db` → izleniyor; `.gitignore`'da yok. Son dokunan commit `0ab4dbe` (bu gecenin aralığında değil: `git log b935158..HEAD -- data/app.db` boş).
- HEAD'deki kopyada (salt okuma, yalnız sayım): `user` 7 satır (4'ü gmail.com alan adlı), `account` 2 satır (2'sinde `access_token` ve `id_token` dolu), `session` 8 satır (hepsi süresi dolmuş), `chat_messages` 21, `orders` 85. Değerler okunmadı/yazılmadı.
- Senaryo: depoya (Gitea uzak depo tanımlı) erişen herkes bu e-posta adreslerini, sohbet mesajlarını ve Google kimlik token'larını (`id_token` ad/e-posta taşır) geçmişten okuyabilir. Satırlar Bilal'in kendi test hesapları olabilir — **doğrulanmalı**.
- Bu gece: çalışma ağacında `M data/app.db` var (QA/eval göç ve test satırları). **Commit'lenmemiş, öyle kalmalı.**
- Düzeltme `[BİLAL]`: `git rm --cached data/app.db` + `.gitignore`'a ekle; geçmişten temizlemek ayrı karar.
### B4 — DÜŞÜK · Hata/zaman aşımı yolunda yarım cevap kullanıcıda kalır ve kredi iade edilir · koddan çıkarım
- Yer: `src/lib/ai/sohbet-akisi.ts:145-148, 190`; `route.ts:244-254`.
- Çifte iade yok: iade `refund` + mesaj kimliğiyle yazılıyor, `UNIQUE(reason, ref_id)` ikinciyi düşürüyor (çalıştırıldı: 1. `granted`, 2. `tekrar`, bakiye +1).
- İstemci kopması bedava cevap kapısı değil: AI SDK 7.0.127'de `writer.write` kapalı akışa yazarken hata fırlatmıyor (`safeEnqueue`, `node_modules/ai/dist/index.js:10375`), üretim sürüyor, cevap kaydediliyor, kredi harcanmış kalıyor.
- Kalan: sağlayıcı cevabın sonuna doğru hata verirse ya da 120 sn'de takılırsa kullanıcı metnin çoğunu görmüş olur ve iade alır. Kullanıcı bunu tetikleyemez; kabul edilebilir. Düzeltme önermiyorum.
### B5 — DÜŞÜK · Jev hata gövdesi log'a yazılıyor · OLASI
- Yer: `src/lib/ai/jev.ts:85` (hata metnine cevap gövdesinin ilk 300 karakteri), `:116` (`console.warn`).
- Senaryo: TypeSafe 4xx cevabında gönderilen metni geri yansıtırsa danışman cevabının bir parçası (sıra, il) log'a düşer. Sağlayıcının böyle yaptığını görmedim.
- Düzeltme `[KARAR]`: log'a yalnız durum kodu. Öteki log'lar temiz: kullanıcı mesajı, cevap metni, e-posta basılmıyor (yalnız araç adı, sayılar, durum kodu).
### B6 — DÜŞÜK · Gövde oturumdan önce ayrıştırılıyor (bu geceden eski) · koddan çıkarım
- Yer: `route.ts:68-71` — `getSession()` ile `request.json()` paralel; 2.000 karakter sınırı ayrıştırmadan sonra.
- Senaryo: girişsiz biri çok büyük JSON gövdeleri yollayıp sunucuya bellek/CPU harcatır. Önde gövde sınırı varsa (Traefik) etkisiz — doğrulanmalı.
- Düzeltme `[KARAR]`: önce oturum, sonra gövde; `Content-Length` üst sınırı.
## Temiz çıkanlar (çalıştırılarak)
- **Profil yazımı (`danisman-profil-db.ts:28-56`):** alan adı kapalı listeden (`evIl') , '$.x'…` ve `evIl.x` reddedildi); değer bağlı parametre (tırnaklı metin düz metin olarak saklandı, SQL çalışmadı); `userId` yalnız oturumdan, araç şeması `userId` anahtarını reddediyor; öteki kullanıcının profili değişmedi.
- **Kişisel veri profile yazılamıyor:** araç yolunda `evIl` il listesinden çözülmek zorunda ("Ayşe Yılmaz 0532…" ve 11 haneli sayı → "İl tanınmadı"); `kampus`/`burs`/`dil` sabit seçenek; `maxKm` 50-1.500, `ucretUst` 50 bin-5 milyon. Serbest metin alanı yok. Not: `profilAlanYaz` doğrudan çağrılırsa `evIl` 40 karaktere kadar metin kabul eder; bugün tek yazan araç ve o il çözümünden geçiriyor.
- **Araç argümanları:** bilinmeyen alan, yanlış tip, geçersiz seçenek reddediliyor; `program_ara`'ya `' OR 1=1 --`, `'; DROP TABLE…`, `UNION SELECT` → 0 sonuç, hata yok; `yokatlas.db` salt okunur açılıyor.
- **Prompt enjeksiyonu / sızıntı:** sistem prompt'unda sır yok (kurallar + adayın kendi verisi); şema sızarsa kaybedilen yalnız araç adlarıdır. `rehber_ara` alıntısı depodaki `content/rehber/*.md`'den (47 dosya, git'te) geliyor, kullanıcı içeriği değil; HTML etiketleri ayıklanıyor. Paketsiz kullanıcının prompt'una kilitli satırlar girmiyor.
- **Eval betikleri:** Jev'e ve modele yalnız sentetik profil/soru gidiyor (`@eval.invalid` kullanıcıları, geçici DB kopyası). `scripts/eval/sonuc/*.json` (14 dosya): anahtar kalıbı 0, TC/telefon kalıbı 0, e-posta yalnız `@kolaytercih.com` (3 dosyada 1'er, cevap metninde iletişim adresi); uzun onaltılık dizi yalnız `git.commit`.
- **Commit'ler:** `b935158..HEAD` farkında anahtar kalıbı yok; tek eşleşme `scripts/eval/akis-denetim.ts:55`'teki bilerek sahte değer. `.env*` izlenmiyor.
## Metin × kod uyumsuzlukları
- **U1 — /gizlilik `src/app/gizlilik/page.tsx:166-169`:** "danışmanın sana yazdığı cevap … iletilir. Senin yazdığın mesaj, adın ve e-postan gönderilmez." Kod bunu doğruluyor (`jev.ts:82`: gövde = cevap + model + soru; tek çağıran `sohbet-akisi.ts:157`; araç çıktısı, profil, kullanıcı mesajı, kimlik gitmiyor). Eksik olan: cevap metni adayın başarı sırasını, ilini, kaydettiği tercihleri ve aday yazdıysa kişisel/hassas ayrıntıyı (kriz cevapları dahil) içerebilir; "tekrarlama" kuralı yalnız prompt'ta, kodla zorlanmıyor. Taslak ek: "Bu cevap sıralaman ve söylediğin tercihler gibi bilgileri içerebilir." — **avukat teyidi gerekir**, `[BİLAL]`.
- **U2 — aynı dosya `:173-175`:** yurt dışı aktarım cümlesi "e-posta ve yapay zekâ sağlayıcısı" diyor; TypeSafe'in de aktarım olduğu açık yazılmamış. Dayanak hâlâ açık (`:20` TODO(avukat); `KARARLAR.md:229`). **Avukat teyidi gerekir**, `[BİLAL]`.
- **U3 — yorum, kullanıcıya görünmez:** `src/lib/ai/danisman-prompt.ts:20` "Bedeli: 'evet' cevabı bir kredi" diyor; kod bedelsiz. `[KARAR]` yorum düzeltmesi.
## Denetlenmeyenler
- Model davranışı gerektiren iddialar (önceden yüklenmiş sorularla bedava cevap; modelin adı tekrar etmesi) — model çağrısı yasaktı.
- Rybbit'te depolanan olay özellikleri (bağlantı kapalı). `pnpm audit` bu turun kapsamında değildi.
- Lokal gözlem: `file:` modunda eşzamanlı iki kredi işlemi `SQLITE_BUSY` fırlatıyor (3 paralel harcamanın 2'si). Canlı `libsql://` ise geçerli olmayabilir — doğrulanmalı; bulgu yapmadım.
## Bilal'den istenen
1. B3: `data/app.db` git izlemesinden çıkarılsın mı ve içindeki 7 kullanıcı satırı senin test hesapların mı — evet/hayır (3 dk).
2. U1 + U2: /gizlilik TypeSafe maddesine "cevap sıralamanı ve tercihlerini içerebilir" eki ve aktarım cümlesi avukata gidecek listeye eklensin mi (2 dk).
3. B1 + B2: bedelsiz "evet"in tek kullanımlık olması ve "aynı anda tek cevap" kilidi yayından önce yapılsın mı — onay (1 dk).