Files
kolaytercih/docs/gece-vardiyasi/2026-10-05/09-guvenlik-uyum.md
bilalgursen c89a86c069 docs(gece): güvenlik ve uyum denetimi raporu (09)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 08:56:48 +03:00

89 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).