Model çıktısını güvenilmez veri saymak
- Model çıktısını kullanıcı girdisi gibi güvenilmez veri sayar, HTML, SQL, kabuk ve dosya yolunda bağlamına uygun kodlama ve doğrulama uygularsın.
- Bu derste saklı XSS'i bulup kapatır, model önerisini sabit komutlara bağlar, uydurma paket adını ayıklar ve şüpheli çıktıyı kayıtta yakalarsın.
Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +50 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.
Altay Kargo'nun destek ekibi müşteri biletlerini bir dil modeline özetletiyor. Model bileti okur, birkaç cümlelik bir özet yazar ve özet temsilcinin ekranına düşer. İlk hafta her şey yolunda görünür. Sonra bir temsilci, özet kutusunda tuhaf bir davranış fark eder.
Sayfa, kimsenin istemediği bir işi kendi başına yapmaya çalışıyor. Sorun modelde değil, özeti sayfaya basan koddadır. Kod, modelin ürettiği metni güvenilir bir şablon parçası gibi işler. Oysa bu metin, bileti yazan müşterinin sözlerinden beslenir.
Kod, metnin hangi kısmını kimin yönlendirdiğini bilemez. Bu derste model çıktısını neden güvenilmeyen veri saydığını öğrenirsin. Ardından HTML, markdown, kabuk, SQL ve dosya yolu bağlamlarında çıktıyı nasıl kodlayıp doğrulayacağını adım adım görürsün. Son bölümde şüpheli çıktıyı kayıtta nasıl yakalayacağını işlersin.
Model çıktısı neden güvenilmez
Bir dil modeli, verdiğin istemi okur ve bir metin üretir. Üretim olasılıksaldır. Model her adımda, o ana kadarki bağlama göre olası bir sonraki parçayı seçer. Bağlam yalnız senin yazdığın sistem isteminden (system prompt) oluşmaz.
Kullanıcının mesajı, modele okutulan belge ve bir aracın döndürdüğü sonuç da aynı bağlama girer. Bu parçalardan birine yazılmış tek bir cümle, çıktının içeriğini değiştirebilir. Bu yüzden model çıktısı, bir kullanıcının form alanına yazdığı metin gibi güvenilmeyen girdi sayılır.
Kodun geri kalanı bu çıktıyı hiçbir zaman olduğu gibi işlemez. Bu farkı unutmak eski açıkları yeni bir kılıkla geri getirir: XSS, SQL enjeksiyonu, komut enjeksiyonu ve yol gezinmesi. Saldırgan bu kez açığa doğrudan değil, modelin üzerinden ulaşır.
OWASP'ın LLM uygulamaları için hazırladığı 2025 listesi bu sorunu LLM05 koduyla sayar: uygunsuz çıktı işleme (Improper Output Handling). Liste beş tipik örnek verir. Birincisinde model çıktısı bir kabuğa ya da eval ve exec gibi bir işleve girer ve uzaktan kod çalıştırmaya yol açar.
İkincisinde modelin ürettiği JavaScript ya da markdown tarayıcıda yorumlanır ve XSS doğar. Üçüncüsünde modelin yazdığı SQL sorgusu parametresiz çalışır. Dördüncüsünde model çıktısı bir dosya yolu kurar ve yol gezinmesi açılır.
Beşincisinde çıktı bir e-posta şablonuna kaçışsız girer ve oltalamaya zemin hazırlar. Belgedeki senaryolardan biri bir adım öteye gider: kod öneren bir model var olmayan bir paket adı uydurabilir. Altı durumun ortak noktası aynıdır. Çıktı, kullanıldığı bağlamın kuralı uygulanmadan bir sonraki sisteme geçer.
Kural basittir: model çıktısını nerede kullanırsan, o bağlamın güvenlik kuralını uygula. Çıktıyı bir web sayfasına basıyorsan HTML bağlamına göre kodla. Bir veritabanı sorgusuna katıyorsan parametreli sorgu kullan, metni birleştirme. Bir kabuk komutuna veriyorsan kabuğu hiç çağırma, çıktıyı sabit komutlardan birine bağla.
Çıktıyı koda çevirip eval ile çalıştırmak en tehlikeli kalıptır. Model ne üretirse üretsin, o kod uygulamanın bütün yetkisiyle çalışır. OWASP'ın önerisi de aynı yere varır.
Modeli sıradan bir kullanıcı gibi görür, sıfır güven yaklaşımıyla ondan gelen her yanıtı doğrularsın. Çıktının biçimi ne kadar düzenli görünürse görünsün kural değişmez. Düzenli görünen bir metin de içinde gömülü bir komut taşıyabilir.
Etkinlik 1 / 7
Bir destek asistanı, modelin ürettiği özeti doğrudan sayfaya innerHTML ile basıyor. En büyük risk hangisidir?
Saklı XSS'i bulmak
Altay Kargo'daki hatanın kaynağına dönelim. Bilet özetleyen işlev, modelin ürettiği metni bir HTML şablonuna yerleştiriyor. Özetin içeriği tamamen modelin kelimelerine bağlıdır ve geliştirici bu içeriği önceden bilemez. Bilet metnine HTML etiketi yazan bir müşteri, modelin bu etiketi özete taşımasını sağlayabilir.
Model etiketi bazen olduğu gibi aktarır, bazen biletteki bir talimata uyup kendisi üretir. İki durumda da etiket temsilcinin tarayıcısında yorumlanır. Saldırının hedefi bileti yazan kişi değil, bileti açan temsilcidir.
Temsilcinin oturumu daha geniş yetki taşıdığı için hasar da büyür. Bu tür açığa saklı XSS denir. Zararlı metin önce bir yerde saklanır, sonra başka birinin ekranında çalışır. Aşağıdaki kodu satır satır oku ve metnin hangi noktada HTML'e dönüştüğünü bul.
async function biletOzetle(bilet) {
const ozet = await model.yanit(bilet.metni);
res.send(`<div class="ozet">${ozet}</div>`);
}Etkinlik 2 / 7
Bu işlevde hangi satır saklı bir XSS açığına yol açar?
Çıktıyı kodlamak ve markdown'ı sınırlamak
import escapeHtml from "escape-html";
async function biletOzetle(bilet, res) {
const ozet = await model.yanit(bilet.metni);
res.type("html").send(`<div class="ozet">${escapeHtml(ozet)}</div>`);
}Düzeltme tek bir satırın anlamını değiştirir. 1. satır escape-html paketini içe aktarır. Bu küçük paket HTML'de anlam taşıyan beş karakteri karşılığıyla değiştirir: küçüktür ve büyüktür işaretleri, ve işareti, çift ve tek tırnak.
5. satır özeti şablona eklemeden önce bu işlevden geçirir. Model bir etiket üretse bile tarayıcı onu artık etiket olarak yorumlamaz, ekranda düz metin olarak gösterir. Aynı satır yanıtın türünü de açıkça belirtir.
Kodlama bağlama özgüdür. Burada metin bir div etiketinin gövdesine giriyor. Aynı metin bir öznitelik değerine, bir JavaScript dizgesine ya da bir adrese girseydi her biri ayrı kural isterdi.
OWASP'ın XSS önleme rehberi bu bağlamları tek tek listeler. Şablon motorun varsayılan olarak kaçış yapıyorsa, model çıktısında o kaçışı kapatan özel sözdizimini hiç kullanma.
Pek çok sohbet arayüzü model çıktısını düz metin değil markdown olarak gösterir. Başlık, liste ve bağlantı güzel görünür, ama markdown oluşturucu ham HTML'i de sayfaya geçirebilir. markdown-it kütüphanesi bu konuda güvenli bir varsayılanla gelir.
Kaynak metindeki ham HTML kapalıdır, javascript: ve vbscript: gibi tehlikeli bağlantı şemaları da reddedilir. Bu ayarı açma. Ham HTML gerçekten gerekiyorsa sonucu DOMPurify gibi bir temizleyiciden geçir.
Üçüncü katman İçerik Güvenliği Politikası'dır, kısaca CSP. Satır içi betiği yasaklayan bir CSP, ilk iki katman bir gün atlansa bile betiğin çalışmasını engeller. Katmanlar birbirinin yerini tutmaz. Her biri ötekinin kaçırdığı durumu yakalar.
const md = new MarkdownIt({ html: false, linkify: false });
kutu.innerHTML = DOMPurify.sanitize(md.render(ozet));
// Yanıt başlığı:
// Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' https://cdn.altaykargo.exampleMarkdown'ın sessiz bir riski daha var: görsel. Model çıktısında bir görsel sözdizimi varsa tarayıcı, kullanıcı hiçbir şeye tıklamadan o adrese istek atar. Adresin sorgu dizesi, modelin bağlamındaki bir bilgiyi taşıyabilir.
Dolaylı istem enjeksiyonuyla yönlendirilen bir model bu yolla oturumdaki bilgiyi dış bir sunucuya sızdırabilir. OWASP'ın istem enjeksiyonu önleme rehberi bu kalıbı açıkça anar. Savunma, görselin nereden yükleneceğini sınırlamaktır.
Yukarıdaki CSP'de img-src yönergesi görseli yalnız kendi alan adından ve onaylı içerik ağından yükletir. Oluşturucu tarafında dış görsel bağlantısı kaldırılır ya da tıklanması gereken düz bir bağlantıya çevrilir. Bu sızıntının saldırı yönünü AI Güvenliği yolu ayrıntılı işler.
Etkinlik 3 / 7
Doğru mu, yanlış mı?
Model çıktısındaki bir markdown görselinde, görsel adresine giden istek kullanıcının görsele tıklamasıyla başlar.
Kabuk, SQL, dosya yolu ve paket adı
Model çıktısını çalıştırmak, sayfaya basmaktan daha tehlikelidir. Altay Kargo'nun geliştirici asistanı, sohbetten gelen isteğe göre depo geçmişini özetliyor. İlk sürümde model bir kabuk komutu yazıyor, kod da bu komutu kabuk üzerinden çalıştırıyordu.
Model üzerindeki tek bir enjeksiyon doğrudan sunucuda komuta dönüşebilirdi. Güvenli tasarım üç adımda kurulur. Model komut yazmaz, önceden tanımlı bir komut kimliği seçer.
Kod bu kimliği sabit bir argüman dizisine eşler. Değişken parametre ayrıca doğrulanır ve kabuk hiç çağrılmaz. Beurer-Kellner ve arkadaşlarının 2025'te yayımladığı tasarım desenleri çalışması bu yaklaşıma Eylem Seçici (Action-Selector) adını verir. Model bir tercüman gibi çalışır ve doğal dili önceden onaylanmış eylemlerden birine çevirir.
import subprocess
IZINLI_KOMUTLAR = {
"son_commitler": ["git", "log", "--oneline", "-n"],
"degisen_dosyalar": ["git", "diff", "--stat", "HEAD~1"],
}
def komut_calistir(oneri: dict) -> str:
kimlik = oneri.get("komut")
if kimlik not in IZINLI_KOMUTLAR:
raise PermissionError(f"izinsiz komut: {kimlik}")
argumanlar = list(IZINLI_KOMUTLAR[kimlik])
if kimlik == "son_commitler":
sayi = int(oneri.get("sayi", 5))
if not 1 <= sayi <= 50:
raise ValueError("sayi 1 ile 50 arasinda olmali")
argumanlar.append(str(sayi))
sonuc = subprocess.run(argumanlar, capture_output=True, text=True, timeout=10, check=True)
return sonuc.stdout>>> komut_calistir({"komut": "son_commitler", "sayi": "3"})
'7c41e2a Iade esigini yapilandirmaya tasi\n3b9d0f1 Takip ucuna zaman asimi ekle\ne02a6c8 README guncelle\n'
>>> komut_calistir({"komut": "dosya_sil"})
PermissionError: izinsiz komut: dosya_sil
>>> komut_calistir({"komut": "son_commitler", "sayi": "3; ls"})
ValueError: invalid literal for int() with base 10: '3; ls'İzin listesi bir sözlüktür ve her kimlik sabit bir argüman dizisine bağlanır. 10. satır modelin seçtiği kimliği listede arar. Listede olmayan her şey, örneğin dosya_sil, PermissionError ile durur.
13-17. satırlar tek değişken parametreyi işler. int dönüşümü sayı olmayan her girdiyi reddeder.
Çıktıdaki son örnekte noktalı virgül taşıyan değer ValueError fırlatır ve hiçbir komut çalışmaz. Aralık denetimi de çok büyük bir sayıyla kaynak tüketmeyi önler. 18. satır komutu argüman dizisi olarak verir.
shell parametresi varsayılan değeri olan False'ta kalır, bu yüzden kabuk özel karakterleri anlam taşımaz. Argüman dizisi tek başına yetmez.
Model tire ile başlayan bir değer üretirse git onu bir seçenek sanabilir. Bu yüzden değişken parametreyi kendi türüne çevirerek doğrularsın. timeout takılan süreci keser, check=True da hata koduyla biten komutu sessizce geçirmez.
Aynı ilke SQL'de de geçerlidir. Modelden SQL yazmasını isteme, parametre iste. Model bir takip numarası ve bir tarih aralığı üretir, kod bunları sabit ve parametreli bir sorguya bağlar. Müşteri kimliği hiçbir zaman modelden gelmez, oturumdan alınır.
Doğal dilden sorgu üretmek bir ürün özelliğiyse sorguyu yalnız okuma yetkili bir veritabanı rolüyle, kısa bir zaman aşımıyla ve satır sınırıyla çalıştırırsın. Dosya yolu için ikinci modülde gördüğün kanonik yol denetimini kullan. Yolu gerçek hâline çevirir, izinli kök dizinin altında kaldığını doğrularsın. Model bir bağlantı önerirse şemasını izin listesinden geçirir, yalnız https kabul edersin.
Kod öneren bir model var olmayan bir paket adı da uydurabilir. Saldırganlar sık uydurulan adları önceden kaydedip içine zararlı kod koyar. Bu yüzden önerilen paketi kurmadan önce kayıt defterinde varlığını, yaşını ve bakımcısını denetlersin.
Etkinlik 4 / 7
Aşağıdaki kullanım kalıplarını güvenli ve güvensiz olarak ayır.
Önce bir seçeneğe, sonra aşağıdaki uygun kutuya bas. İstersen seçeneği kutuya sürükleyip bırak. Yerini değiştirmek için aynı işlemi tekrarla.
0/4 seçenek yerleşti.
LLM05 listesindeki beşinci örnek daha az konuşulur: e-posta şablonu. Altay Kargo, gecikme bildirimlerinin metnini modele yazdırıyor ve metni HTML e-posta şablonuna yerleştiriyor. Model çıktısı burada da kodlanmazsa e-postaya sahte bir bağlantı ya da biçimlendirilmiş bir ödeme talimatı eklenebilir.
Müşteri bu e-postayı şirketin kendi adresinden aldığı için ona güvenir. Savunma aynıdır: model yalnız metin gövdesini üretir, şablon bu metni kodlar ve bağlantılar modelden değil, sipariş kaydından gelir. Her bağlamın kendi kuralı olduğunu aşağıdaki eşleştirmeyle pekiştir.
Etkinlik 5 / 7
Model çıktısının girdiği bağlamı, o bağlamın savunmasıyla eşleştir.
Tespit ve sık hatalar
Savunma katmanları yerinde olsa da neyin denendiğini görmek istersin. Altay Kargo'nun ekibi her model yanıtı için kayda birkaç sayısal alan ekler: HTML etiketi sayısı, dış bağlantı sayısı, görsel sözdizimi sayısı ve kabuk özel karakteri sayısı. Yanıtın tamamı değil, bu sayılar kaydedilir.
Bir bilet özetinde HTML etiketi beklenmez, bu yüzden sıfırdan farklı her değer bir uyarı üretir. Uyarı, yanıtı tetikleyen bilet kimliğiyle birlikte gelir. Analist hangi müşteri metninin modeli hangi çıktıya yönelttiğini geriye doğru izler.
Bulgu sınıflandırılırken iki eşleme işe yarar. Sayfada çalışan betik CWE-79, yani XSS olarak kaydedilir. Kaynağı bir istemdeki gömülü talimatsa kök neden ayrıca CWE-1427 ile işaretlenir.
{"zaman":"2026-09-30T14:22:07Z","olay":"model_yanit","bilet":"B-88213","html_etiketi":2,"dis_baglanti":1,"gorsel":1,"kabuk_karakteri":0,"karar":"kodlandi_ve_uyarildi"}Kayıt satırını alan alan oku. zaman ve olay, satırın ne zaman ve hangi adımda yazıldığını söyler. bilet alanı yanıtı tetikleyen girdiyi gösterir ve araştırmanın başlangıç noktasıdır.
html_etiketi 2 değeri, özette iki etiket bulunduğunu söyler. dis_baglanti ve gorsel alanları birer değer taşır. Görsel sayısının sıfırdan büyük olması, az önce gördüğün sızdırma kalıbına işaret edebilir.
kabuk_karakteri 0'dır, çünkü bu yanıt bir komuta gitmiyor. karar alanı kodun ne yaptığını yazar: çıktı kodlanarak gösterildi ve analiste uyarı gitti. Tek bir satır saldırı kanıtı değildir. Aynı müşteriden art arda gelen benzer satırlar ise bilinçli bir denemeye işaret eder.
Derinleş:CWE kayıtları ve model çıktısı
MITRE'nin ortak zayıflık listesi CWE, 2024'te üretken yapay zekâya özgü iki kayıt ekledi. CWE-1426, üretken yapay zekâ çıktısının yetersiz doğrulanmasıdır ve 4.15 sürümüyle, Temmuz 2024'te listeye girdi. Kayıt, ürünün denetleyemediği bir modelin çıktısını güvenlik, içerik ya da gizlilik politikasına göre doğrulamamasını tanımlar. MITRE bu kaydı gerçek açıkları eşlemek için önermez.
Kaydın ileride büyük ölçüde değişebileceğini ve sık yanlış yorumlandığını belirtir. Bu yüzden pratikte sonuca bakılır: sayfada çalışan betik CWE-79, parametresiz sorgu CWE-89, kabuk komutu CWE-78, yol gezinmesi CWE-22 olarak kaydedilir. CWE-1427 ise Kasım 2024'te 4.16 sürümüyle geldi.
İstemi kuran ürünün, dışarıdan gelen veriyi geliştiricinin talimatından ayıramamasını anlatır ve eşlemede kullanılabilir. Bir olayda iki kayıt birlikte görünebilir: kök neden CWE-1427, çıktıdaki sonuç CWE-79.
Etkinlik 6 / 7
Doğru mu, yanlış mı?
İstemi kendin yazmış olsan da, model çıktısını kaçışlamadan innerHTML ile sayfaya basan kod saklı XSS'e açık kalır.
Etkinlik 7 / 7
Bir otomasyon betiği, modelin önerdiği bir kabuk komutunu sorgusuz çalıştırıyor. Bu tasarımdaki açığın kök nedeni nedir?
Okumayı istediğin sırayla sürdürebilirsin. Ders, bütün etkinlikleri cevapladığında tamamlanır.