İzin listesiyle girdi doğrulama
- Sunucu tarafında izin listesi, şema ve kanonikleştirmeyle girdi doğrulamayı; tip karmaşası ve Unicode tuzaklarını örnek kodla öğren.
- İzin listesi yalnız bilinen iyi kalıpları kabul eder, engelleme listesi her zaman bir adım geriden gelir.
- Bu derste AltayKargo iade formunun üç alanını sunucu tarafında doğrulayacaksı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.
Bir form gönderildiğinde sunucunun ilk işi, gelen veriye güvenmemektir. İstemci tarafında yazılmış bir doğrulama kullanıcı deneyimini iyileştirir ama güvenlik sağlamaz, çünkü saldırgan tarayıcıyı atlayıp isteği doğrudan sunucuya yollar. Gerçek sınır sunucu tarafındadır.
Doğrulama iki şekilde kurulabilir: engelleme listesi bilinen kötü kalıpları reddeder, izin listesi yalnız bilinen iyi kalıpları kabul eder. Engelleme listesi kaybeden taraftadır, çünkü saldırganın deneyebileceği kötü varyasyon sayısı sonsuza yakındır, oysa iyi kalıp genelde sabittir. Eksik ya da hatalı girdi doğrulaması CWE-20 kimliğini taşır ve 2025 CWE Top 25 listesinde 18. sıradadır.
AltayKargo iade servisinde üç alan var: takip numarası, tutar, açıklama metni. Bu üç alan modülün geri kalanında da karşına çıkacak. Aynı veriyi önce doğrulayacak, sonra ekrana basarken kodlayacak, en son da sorguya güvenle taşıyacaksın.
Her alana ayrı bir izin listesi
Doğrulama tam olarak uygulama katmanına girerken çalışır, çünkü veri o noktadan sonra iş kuralına ve depoya akar. Her alan için ayrı bir izin listesi tanımlanır: tip, uzunluk sınırı, karakter seti, aralık. Takip numarası sabit bir kalıba uyar: AK harfleriyle başlayıp altı rakamla biter. Tutar, sıfır ile yüz bin arasında bir sayı olmalıdır.
Açıklama metni beş yüz karaktere kadar serbest metin kabul eder ama kontrol karakteri taşımamalıdır. Regex bu kontrollerden yalnız biridir. Tip ve aralık kontrolü ayrı satırlarda kalmalıdır, aksi halde tek bir karmaşık kalıp hem okunmaz hem yavaş çalışabilir. Kontrolleri ayrı satırlarda tutmak, hangi kuralın hangi alanı reddettiğini sonradan okumayı da kolaylaştırır.
app.post("/api/iadeler", (req, res) => {
const { takipNo, tutar, aciklama } = req.body;
db.iadeler.insert({ takipNo, tutar, aciklama });
res.json({ durum: "alindi" });
});const TAKIP_NO = /^AK\d{6}$/;
app.post("/api/iadeler", (req, res) => {
const { takipNo, tutar, aciklama } = req.body;
if (!TAKIP_NO.test(takipNo)) return res.status(400).json({ hata: "gecersiz_takip_no" });
if (typeof tutar !== "number" || tutar < 0 || tutar > 100000) return res.status(400).json({ hata: "gecersiz_tutar" });
if (typeof aciklama !== "string" || aciklama.length > 500 || /[\x00-\x1F]/.test(aciklama)) return res.status(400).json({ hata: "gecersiz_aciklama" });
db.iadeler.insert({ takipNo, tutar, aciklama });
res.json({ durum: "alindi" });
});İkinci sürümü satır satır oku. TAKIP_NO sabiti kalıbı bir kez tanımlar. ^ ve $ çapaları, kalıbın değerin tamamına uymasını zorunlu kılar. İlk if takip numarasının biçimini kilitler. İkinci if önce tipe, sonra aralığa bakar; tutar sayı değilse aralık kontrolüne hiç gelinmez.
Üçüncü if açıklamanın metin olduğunu, beş yüz karakteri aşmadığını ve kontrol karakteri taşımadığını denetler. Kontrol karakteri reddi, açıklamanın ekranı ya da günlük satırını bozmasını önler. Her kontrol kendi 400 yanıtını döner.
Sunucu girdiyi kendiliğinden düzeltmeye çalışmaz. Kalıba uymayan istek reddedilir, kullanıcı doğru biçimle yeniden dener. Bu tutum kullanıcıya net bir geri bildirim verir, sunucuyu da beklenmeyen bir veri tipinden korur.
TAKIP_NO gibi bir kalıp yazarken üç kural çoğu hatayı önler. Birincisi çapadır. Çapasız /AK\d{6}/ kalıbı xxAK123456<script> metninde de eşleşme bulur, çünkü değerin yalnız bir parçasına bakar. İkincisi nokta karakteridir.
Nokta satır sonu dışında her karakteri kabul eder, bu yüzden izin listesinde onun yerine açık bir karakter sınıfı koy. Üçüncüsü JavaScript'e özgü bir tuzaktır. g bayraklı bir regex nesnesi, test() çağrısından sonra lastIndex değerini saklar. Aynı nesneyi farklı değerlerde yeniden kullanırsan sonraki arama kaldığı yerden başlar.
Geçerli bir takip numarası, önceki çağrının izi yüzünden reddedilebilir. Engelleme listesinde ise kötü değer arada kaçar. Doğrulama kalıbında g bayrağını kullanma.
Etkinlik 1 / 7
Yukarıdaki doğrulanmış uç noktaya takipNo alanında 'AK12' gönderilirse ne olur?
Önce kodu oku, sonra çıktıyı seç.
const TAKIP_NO = /^AK\d{6}$/;
if (!TAKIP_NO.test(takipNo)) return res.status(400).json({ hata: "gecersiz_takip_no" });Sabit liste, sözdizimi ve anlam
İzin listesi yalnız regex ile kurulmaz. Kargo tipi gibi sabit seçenekli bir alan için en güvenli yöntem, değeri önceden tanımlı bir listeyle karşılaştırmaktır. Yalnız standart, hizli ya da sogutmali kabul edilir, başka her değer reddedilir. Sabit bir liste regex'ten daha okunur olur ve yanlış yazılma riski taşımaz.
Durum kodu, ülke kodu ve para birimi gibi alanlar da aynı yolla denetlenir. Listeyi kodda tek bir yerde tut. Sunucudaki doğrulama ile arayüzdeki seçim kutusu aynı listeden beslenirse ikisi hiç ayrışmaz. Yeni bir kargo tipi eklendiğinde de tek bir satırı değiştirirsin.
OWASP doğrulamayı iki katmana ayırır. Sözdizimsel doğrulama (syntactic validation) değerin biçimine bakar: AK ve altı rakam, sıfırla yüz bin arasında bir sayı. Anlamsal doğrulama (semantic validation) değerin iş bağlamında doğru olup olmadığını sorar. Takip numarası kalıba uyabilir ama başka bir müşterinin gönderisine ait olabilir.
Teslimden otuz gün geçmişse iade hakkı da bitmiştir. Bu soruları regex cevaplayamaz, veritabanına bakan iş kuralı cevaplar. Sıra da önemlidir. Önce ucuz olan sözdizimi kontrolü çalışır, böylece bozuk istek veritabanına hiç gitmez.
Anlamsal kontrol ondan sonra gelir. Anlamsal kontrolü atlarsan biçimi kusursuz ama sahibi yanlış bir isteği kabul edersin. Bu da çoğu zaman bir yetkilendirme açığına dönüşür.
// Sözdizimi geçti, şimdi iş kuralı.
const gonderi = await db.gonderiler.bul({ takipNo });
if (!gonderi || gonderi.musteriId !== req.oturum.musteriId)
return res.status(404).json({ hata: "gonderi_bulunamadi" });
const gecenGun = (new Date().getTime() - gonderi.teslimTarihi.getTime()) / 86400000;
if (gecenGun > 30)
return res.status(422).json({ hata: "iade_suresi_doldu" });İlk kontrol iki durumu tek yanıtta birleştirir. Gönderi hiç yoksa da başka bir müşteriye aitse de 404 döner. Ayrı yanıt verseydin saldırgan, takip numaralarını tek tek deneyerek hangilerinin var olduğunu öğrenirdi. Süre kontrolü tarihi sunucudaki kayıttan okur, istemcinin gönderdiği bir tarihe hiç bakmaz.
422 kodu isteğin biçimce doğru ama iş kuralınca geçersiz olduğunu anlatır. Bu ayrım günlükte de işe yarar. 400 artışı biçim denemesine, 422 artışı kural sınamasına işaret eder.
Etkinlik 2 / 7
Her doğrulama yaklaşımını izin listesi ya da engelleme listesi 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.
Etkinlik 3 / 7
Sunucunun yalnız önceden tanımlanmış iyi kalıpları kabul edip gerisini reddetmesine ____ denir.
Şema ile doğrulama ve kütle atama
Her alanı elle if satırlarıyla denetlemek küçük bir uç noktada iş görür. Alan sayısı arttıkça bir kontrolü unutmak kolaylaşır. Şema doğrulaması (schema validation) beklenen gövdeyi tek bir tanımda toplar: hangi alanlar var, tipleri ne, sınırları ne.
TypeScript tarafında Zod, Python tarafında Pydantic bu işin yaygın araçlarıdır. İkisinde de kritik ayar, tanımda olmayan alanı reddetmektir. Zod 4'te z.strictObject, Pydantic'te extra='forbid' bu işi görür.
Varsayılan davranış farklıdır: ikisi de bilinmeyen alanı sessizce atar. Sessizce atmak zararsız görünür ama istemciye hatasını söylemez, deneme de günlüğe hiç düşmez.
// TypeScript, Zod 4
import * as z from "zod";
const IadeIstegi = z.strictObject({
takipNo: z.string().regex(/^AK\d{6}$/),
tutar: z.number().min(0).max(100000),
aciklama: z.string().max(500).regex(/^[^\p{Cc}]*$/u),
});
app.post("/api/iadeler", (req, res) => {
const { success, data } = IadeIstegi.safeParse(req.body);
if (!success) return res.status(400).json({ hata: "gecersiz_istek" });
db.iadeler.insert(data);
res.status(201).json({ durum: "alindi" });
});
# Python, Pydantic
from pydantic import BaseModel, ConfigDict, Field
class IadeIstegi(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
takip_no: str = Field(pattern=r"^AK\d{6}$")
tutar: float = Field(ge=0, le=100_000, allow_inf_nan=False)
aciklama: str = Field(max_length=500, pattern=r"^[^\x00-\x1f\x7f-\x9f]*$")safeParse gövdeyi denetler ve iki sonuçtan birini döner. success false ise istek hiç işlenmeden 400 alır. success true ise veritabanına req.body değil, şemadan geçen data nesnesi yazılır. Fark önemlidir, çünkü data yalnız şemada tanımlı alanları taşır.
Pydantic tarafında strict=True, metin olarak gelen "100" değerini sayıya çevirmeyi keser. Bilinmeyen alanı reddetmenin güvenlik karşılığı kütle atamadır (mass assignment, CWE-915). Bazı çatılar istekteki bütün alanları doğrudan modele bağlar. Gövdeye onayli: true gibi bir alan ekleyen saldırgan, formda hiç olmayan bir alanı değiştirir.
Katı şema bu ek alanı daha kapıda reddeder. OWASP de bağlanacak alanları izin listesiyle sınırlamayı ya da ayrı bir veri aktarım nesnesi (DTO) kullanmayı önerir. Kod incelemesinde insert(req.body), update(req.body) ya da Object.assign(kayit, req.body) gibi satırları ara. Bu kalıplar hangi alanın yazılacağı kararını istemciye bırakır.
$ curl -s -X POST http://localhost:3000/api/iadeler \
-H "Content-Type: application/json" \
-d '{"takipNo":"AK12","tutar":250,"aciklama":"Kutu ezik geldi"}' \
-w '\nHTTP %{http_code}\n'
{"hata":"gecersiz_istek"}
HTTP 400
$ curl -s -X POST http://localhost:3000/api/iadeler \
-H "Content-Type: application/json" \
-d '{"takipNo":"AK482913","tutar":250,"aciklama":"Kutu ezik geldi","onayli":true}' \
-w '\nHTTP %{http_code}\n'
{"hata":"gecersiz_istek"}
HTTP 400
$ curl -s -X POST http://localhost:3000/api/iadeler \
-H "Content-Type: application/json" \
-d '{"takipNo":"AK482913","tutar":250,"aciklama":"Kutu ezik geldi"}' \
-w '\nHTTP %{http_code}\n'
{"durum":"alindi"}
HTTP 201Üç isteği kendi lab makinende sırayla gönder. İlki kısa takip numarası yüzünden kalıba takılır. İkincisi geçerli alanlar taşır ama şemada olmayan onayli alanı yüzünden reddedilir.
Katı şema olmasaydı bu alan sessizce atılırdı. req.body doğrudan kaydedilseydi veritabanına bile yazılırdı. Üçüncüsü üç alanı da doğru tipte taşır ve 201 Created alır.
-w seçeneği yanıt gövdesinin ardına durum kodunu basar, böylece ikisini tek çıktıda görürsün. Her yeni kural için iki test yaz: reddedilecek bir istek ve kabul edilecek bir istek. İkincisi unutulursa fazla sıkı bir kural gerçek müşteriyi kapıda bırakır.
Etkinlik 4 / 7
Katı şemalı uç nokta bu isteğe 400 dönüyor. Gövdeden çıkarman gereken alanın adını yaz.
İsteği tamamla ve gönder.
Tip karmaşası, Unicode ve kod çözme
JSON gövdesinde bir alan her tipte gelebilir. tutar için sayı beklerken "abc" metni, [1] dizisi, null ya da 1e400 gibi aşırı büyük bir değer gelebilir. JavaScript karşılaştırmaları bu tiplerle hata vermez, sessizce dönüştürür. "abc" < 0 ifadesi false döner, "abc" > 100000 de false döner.
Yalnız aralığa bakan bir kontrol bu değeri geçirir. null ve [1] de aynı yoldan sızar. JSON.parse ise 1e400 değerini Infinity olarak okur.
Aralık kontrolü bunu yakalar, ama yalnız tipe bakan başka bir kod yolu yakalamaz. Çözüm, aralıktan önce tipi denetlemektir: typeof tutar === "number" ve Number.isFinite(tutar). Zod'un z.number() şeması NaN ve Infinity değerlerini zaten reddeder.
Serbest metin alanı en zor doğrulanan alandır. Açıklamada Türkçe harf, rakam ve noktalama olabilir. Karakterleri tek tek listelemek bu yüzden işe yaramaz. OWASP bunun yerine Unicode karakter kategorisi kullanmayı önerir. \p{L} bütün alfabelerdeki harfleri, \p{N} rakamları kapsar.
JavaScript'te bu sınıflar yalnız u bayrağıyla çalışır. Kategoriden önce metni normalleştir, çünkü aynı görünen iki metin farklı baytlarla yazılabilir. Tam genişlikli A harfi ya da fi bağ harfi (ligature), NFKC normalizasyonuyla düz A ve fi olur. Görünmez karakterlere de dikkat et.
Sıfır genişlikli boşluk (U+200B) ve yön değiştiren karakterler (U+202E gibi) ekranda iz bırakmaz ama metnin okunuşunu değiştirir. İkisi de Cf kategorisindedir. İzin listende Cf yoksa kendiliğinden reddedilir.
const temiz = aciklama.normalize("NFKC");
const IZINLI = /^[\p{L}\p{N}\p{Zs}.,;:?'"()-]{1,500}$/u;
if (!IZINLI.test(temiz)) return res.status(400).json({ hata: "gecersiz_aciklama" });
db.iadeler.insert({ takipNo, tutar, aciklama: temiz });İlk satır metni NFKC ile tek bir biçime indirir. İkinci satır izin listesini kategorilerle kurar: harf, rakam, boşluk ve birkaç noktalama işareti. Uzunluk sınırı da kalıbın içindedir. u bayrağı olmadan \p{L} yazımı çalışmaz.
Son satır kritik bir kuralı uygular: denetlediğin değer ile kaydettiğin değer aynıdır. Ham metni denetleyip normalleşmiş metni kaydedersen, ya da tersini yaparsan, arada kimsenin denetlemediği bir fark kalır. Emoji kabul etmek istersen onu da kategoriyle ekle, tek tek karakter yazma.
Derinleş:Trojan Source: görünmez karakter kaynak koda girince
Yön değiştiren karakterler yalnız form alanında sorun çıkarmaz. 2021'de iki araştırmacı, aynı karakterlerin kaynak kodda da kullanılabildiğini gösterdi. Çalışma Trojan Source adıyla duyuruldu ve CVE-2021-42574 kimliğini aldı. Unicode, Arapça ve İbranice gibi sağdan sola yazılan dilleri karışık metinde göstermek için yön denetim karakterleri tanımlar.
U+202E sağdan sola geçersiz kılma (RLO) karakteri, ardından gelen metni ekranda ters sırada gösterir. Derleyici ise karakterleri mantıksal sırayla okur. Sonuç: kod incelemesinde yorum satırı gibi görünen bir parça aslında çalışır, çalışıyor gibi görünen bir koşul ise yorumun içinde kalır. Aynı çalışmanın ikinci kimliği CVE-2021-42694, birbirine benzeyen farklı alfabe harflerini (homoglyph) işler.
Duyurunun ardından derleyiciler ve kod barındırma arayüzleri bu karakterleri uyarıyla işaretlemeye başladı. Savunma aynı ilkeye dayanır: görünmesi gereken ile işlenen arasındaki farkı yok et. Kod deposunda da Cf kategorisindeki karakterleri tarayan bir ön işleme kancası (pre-commit hook) kurabilirsin.
Derinleş:Türkçe İ ve büyük-küçük harf dönüşümü
Türkçede i ile ı iki ayrı harftir, büyükleri de İ ile I olur. Unicode'un varsayılan dönüşüm kuralları ise dilden bağımsızdır. JavaScript'te "I".toLowerCase() sonucu i olur, ama "İ".toLowerCase() sonucu i harfi ile ona eklenen bir üst nokta (U+0307) çıkar. Yani "ADMİN".toLowerCase() === "admin" karşılaştırması false döner.
Dile duyarlı sürüm başka bir sonuç verir: "ADMIN".toLocaleLowerCase("tr") sonucu admın olur. Kullanıcı adı, e-posta ya da rol adı gibi değerleri büyük-küçük harf duyarsız karşılaştırırken bu fark iki yönde de hata doğurur. Aynı hesap için iki ayrı kayıt açılabilir ya da ayrı kalması gereken iki ad çakışabilir. Java'da parametresiz toLowerCase() çağrısı sunucunun yerel ayarını kullanır.
Türkçe yerel ayarlı bir sunucuda TITLE değeri tıtle olur ve bir anahtar kelime karşılaştırması sessizce bozulur. Güvenlik kararlarında yerel ayardan bağımsız dönüşüm kullan, Java'da bunun adı Locale.ROOT'tur. Karşılaştırmadan önce iki tarafa da aynı normalizasyonu uygula.
Doğrulamanın zamanı da önemlidir. Önce kod çözme (decoding) ve kanonikleştirme (canonicalization) yapılır, sonra doğrulama. Sıra ters olursa kodlanmış bir değer kontrolden geçer, arkadaki katman onu çözünce tehlikeli hale gelir.
Çift URL kodlaması bunun klasik örneğidir. ../ dizisi bir kez kodlanınca %2e%2e%2f olur. % işareti de kodlanırsa %252e%252e%252f çıkar. Kontrol değeri bir kez çözüp bakarsa %2e%2e%2f görür ve ../ aramasından temiz geçer. Değeri ikinci kez çözen bir kütüphane ise ../ elde eder.
Kural basittir. Girdiyi uygulamanın kullanacağı son biçime tam bir kez çöz, o biçimi doğrula, aynı biçimi kullan. Çözülmüş değerde hâlâ % kalıyorsa ve alan bunu beklemiyorsa isteği reddet.
Etkinlik 5 / 7
Aşağıdaki gevşek kontrol, gövdede tutar olarak "abc" metni gelince ne yapar?
Önce kodu oku, sonra çıktıyı seç.
const { tutar } = req.body;
if (tutar < 0 || tutar > 100000) return res.status(400).json({ hata: "gecersiz_tutar" });
db.iadeler.insert({ tutar });Etkinlik 6 / 7
Doğru mu, yanlış mı?
Bir giriş formunda kullanıcı adının yalnız harf, rakam ve alt çizgi içermesini zorunlu kılmak izin listesi yaklaşımıdır.
Reddedilen isteği izlemek
Doğrulama başarısız olduğunda sunucunun döndüğü hata da tutarlı olmalıdır. Hata yanıtı hangi alanın reddedildiğini söyleyebilir, çünkü gerçek kullanıcı neyi düzelteceğini bilmelidir. Ama kalıbın kendisini, iç kuralın ayrıntısını ya da yığın izini (stack trace) yanıta koyma. Her alan için farklı ayrıntı veren bir hata metni, saldırgana kontrolün nasıl kurulduğunu adım adım öğretir.
Ayrıntının yeri günlüktür. Yanıt kısa ve tek biçimli kalır, sunucudaki kayıt ise hangi kuralın neden tetiklendiğini tutar. Böylece kullanıcı yönlendirilir, saldırgan eğitilmez.
Reddedilen istek aynı zamanda bir sinyaldir. Gerçek kullanıcı arada bir 400 alır ve bir sonraki denemede düzeltir. Tek bir kaynaktan saniyeler içinde farklı alanlara art arda 400 geliyorsa biri kuralları sistemli biçimde yokluyordur. Her reddi bir sayaçla izle ve günlüğe yapılandırılmış bir satır yaz.
Satırda zaman, kaynak IP, uç nokta, reddedilen alan ve kural adı bulunsun. Ham değeri yazma ya da kısaltıp maskele. Açıklama alanında kişisel veri olabilir, üstelik satır sonu taşıyan bir değer günlüğe sahte satır ekleyebilir. Uyarı kuralını basit kur: aynı IP'den beş dakikada elliden fazla 400 gelirse inceleme kaydı açılsın.
{"zaman":"2026-09-14T10:02:40Z","ip":"203.0.113.77","uc":"POST /api/iadeler","sonuc":400,"alan":"takipNo","kural":"kalip"}
{"zaman":"2026-09-14T10:02:41Z","ip":"203.0.113.77","uc":"POST /api/iadeler","sonuc":400,"alan":"tutar","kural":"tip"}Etkinlik 7 / 7
İade uç noktasının günlüğünde kuralları sistemli biçimde yoklayan satırları işaretle.
Şüpheli satırlara tıkla. Birden fazla olabilir.
Okumayı istediğin sırayla sürdürebilirsin. Ders, bütün etkinlikleri cevapladığında tamamlanır.
- Etkinlik 1· Her alana ayrı bir izin listesi
- Etkinlik 2· Sabit liste, sözdizimi ve anlam
- Etkinlik 3· Sabit liste, sözdizimi ve anlam
- Etkinlik 4· Şema ile doğrulama ve kütle atama
- Etkinlik 5· Tip karmaşası, Unicode ve kod çözme
- Etkinlik 6· Tip karmaşası, Unicode ve kod çözme
- Etkinlik 7· Reddedilen isteği izlemek