Giriş

Onlarca code review'da aynı hataları tekrar tekrar görüyoruz. Türk yazılım ekiplerinin TC kimlik numarası (TCKN) ve Vergi Kimlik Numarası (VKN) doğrulamasında düştüğü tuzaklar, büyük ölçüde aynı 10 kalıpta toplanıyor. Bu yazıda her birini üçlü format ile ele alacağız: problem → yeniden üretim → çözüm.

Kodlar çoğunlukla JavaScript ve Python üzerinden; ama prensipler dil agnostik. İster bir form için TC doğrulaması yazıyor olun ister bir e-fatura akışı için vergi no kontrolü, aynı tuzaklar karşınıza çıkar. Algoritma detayları için TC algoritması yazımıza ve VKN yazımıza bakabilirsiniz. Ayrıca test ihtiyaçlarınız için hazır bir vkn algoritması aracı ile sentetik numara üretebilirsiniz.

1. TC kimlik numarasını integer olarak tutmak

Problem: TC kimlik numarası 11 haneli sayısal bir dizedir ama "sayı" değildir. Kod, int/number olarak tutunca leading zero'lar kaybolur. Her ne kadar algoritma gereği ilk hane sıfır olmasa da, kullanıcı "02345678901" girerse ve bu değer form validation'ından önce cast edilirse, TCKN önce 10 haneye düşer, sonra validation fail eder ama hata mesajı yanıltıcı olur.

Yeniden üretim:

const raw = "02345678901";
const asNumber = Number(raw);  // 2345678901
console.log(asNumber.toString().length); // 10 — yanlış

Çözüm: TC kimlik numarasını her zaman string olarak taşıyın. DB kolonunu VARCHAR(11) yapın, ORM'inizde str/String olarak map edin. JSON'dan parse ederken "tckn" alanını açıkça string okuyun.

2. Regex için \d kullanmak

Problem: \d JavaScript'te yalnız ASCII 0-9'u yakalar ama Python re modülünde varsayılan olarak unicode rakamları da kapsar (Arap-Hint, Devanagari, Bengali vs.). Kullanıcı Arap rakamlarıyla ("١٢٣٤٥٦٧٨٩٠١") TC kimlik numarası girerse Python regex geçer, ama integer cast'i veya .isdigit() sonrası algoritma beklenmedik davranır.

Yeniden üretim:

import re
arabic = "١٢٣٤٥٦٧٨٩٠١"
print(re.match(r"^\d{11}$", arabic))  # Match — istemediğimiz sonuç

Çözüm: Her zaman [0-9] veya ^[1-9][0-9]{10}$ yazın. \d yerine açık karakter sınıfı kullanın. Giriş input'unu da unicode normalize edin (unicodedata.normalize('NFKC', s)) ama sonra regex'i [0-9] ile koruyun.

3. Trim ve whitespace eksikliği

Problem: Kullanıcı mobile keyboard'dan kopyala-yapıştır yaptığında başa/sona boşluk eklenebilir. "\u00a0" (non-breaking space) veya "\u200b" (zero-width space) özellikle pdf/web'den kopyalanmış metinlerde sinsi davet.

Yeniden üretim:

const input = " 12345678901 ";
console.log(input.length); // 13 — "uzunluk 11 olmalı" kontrolü fail

Çözüm: En başta normalize edin:

const clean = String(input ?? '').normalize('NFKC').trim().replace(/\s+/g, '');

Sadece trim etmek yetmez — ortalarda whitespace varsa (kullanıcı "123 456 789 01" yazarsa) onları da kaldırın. Ancak tire ve nokta gibi karakterlerin kaldırılması iş gereksinimine bağlı — sessizce düzeltmek mi yoksa hata göstermek mi daha iyi, UX kararı.

4. Negatif mod

Problem: TC kimlik algoritması (sumOdd * 7 - sumEven) mod 10 hesaplarken sonuç negatif olabilir (nadiren ama olur). C, Java, Go, JavaScript gibi dillerde % operatörü negatif sonuç verir. Python farklıdır.

Yeniden üretim (Java):

int result = -3 % 10; // -3, sıfır-dokuz arası değil

Çözüm: ((x % 10) + 10) % 10 kalıbı. Ya da Java 8+ Math.floorMod(x, 10). Kotlin, Swift, Rust benzer. Bir kütüphane kullanıyorsanız test'te negatif mod vakasını açıkça kapsayın.

5. İlk hane kontrolünü unutmak

Problem: "00000000000" algoritma çıkışından 10. ve 11. hane kontrollerinden geçer (her iki toplam da 0). Ama bu geçerli bir TC kimlik numarası değildir. Çoğu nostaljik implementasyon bu kontrolü atlayıp "algoritma geçti, valid" der.

Yeniden üretim:

def naive_valid(t):
    d = [int(c) for c in t]
    s_odd = d[0]+d[2]+d[4]+d[6]+d[8]
    s_even = d[1]+d[3]+d[5]+d[7]
    return d[9] == (s_odd*7 - s_even) % 10 and d[10] == sum(d[:10]) % 10

print(naive_valid("00000000000"))  # True — yanlış

Çözüm: Regex seviyesinde ^[1-9][0-9]{10}$ kullanın veya ayrıca d[0] != 0 kontrolü ekleyin. İdealde her ikisi de.

6. VKN (vergi no) "ilk 3 hane il kodudur" yanılgısı

Problem: Bazı eski bloglar vergi numarasının ilk üç hanesinin il kodu olduğunu söyler. Bu yanlıştır. GİB bugünkü tahsisatta coğrafi kodlama kullanmıyor. Yine de bazı form validation'ları bu varsayımla ekstra kontrol yapıyor ve geçerli VKN'leri reddediyor.

Yeniden üretim: "VKN 999 ile başlayamaz, il kodu geçersiz" diyen bir form — gerçek VKN'ler 900 bloğunda da var.

Çözüm: VKN için sadece algoritma çalıştırın. İl kodu gibi eksik bilgilere dayalı ek kontrol eklemeyin. VKN yapısı hakkında detay için VKN yazımıza bakın.

7. Tek alan, iki format: yanlış branch

Problem: "TC Kimlik No veya Vergi No" tek alan formlarında kod uzunluğa göre branch yapmaz, her iki algoritmayı da dener ve birine geçerse kabul eder. Sorun: kullanıcı 10 haneli yazdığında "TC kimlik" olarak da denenip fail olur; hata mesajı "TC kimlik geçersiz" olur ve kullanıcı şaşırır.

Yeniden üretim:

// kötü
function isValidId(s) {
  return isValidTckn(s) || isValidVkn(s); // mesaj hangi alana göre?
}

Çözüm: Uzunluğa göre dallandırın ve hata mesajını buna göre verin.

function isValidId(s) {
  const t = s.trim();
  if (t.length === 11) return { ok: isValidTckn(t), type: 'TCKN' };
  if (t.length === 10) return { ok: isValidVkn(t), type: 'VKN' };
  return { ok: false, type: 'UNKNOWN' };
}

8. API'de TC kimlik numarasını query string'de taşımak

Problem: GET /api/user?tckn=12345678901 görünümünde bir endpoint, TC kimlik numarasını erişim log'larına, nginx access log'una ve CDN cache'lerine bırakır. Bu KVKK açısından tehlikeli, güvenlik açısından log leakage.

Yeniden üretim: Production nginx access log'una bakın — query string içindeki TCKN'ler aylarca orada kalır.

Çözüm: TC kimlik numarasını sadece POST body'de taşıyın. URL'de asla görmesin. Loglayacaksanız maskeleyin. Detayı için KVKK rehberimizde log konusuna baktık.

9. NVİ "false" dönüşünü "hata" gibi ele almak

Problem: NVİ TCKimlikNoDogrula servisi iki farklı "fail" döndürür: (a) HTTP 500 veya timeout (upstream hatası), (b) HTTP 200 ama <TCKimlikNoDogrulaResult>false</TCKimlikNoDogrulaResult> (eşleşme yok). Çoğu client bu ikisini karıştırır; retry logic eşleşmeme durumlarında da devreye girer ve rate limit yaratır.

Yeniden üretim:

// kötü
try {
  const res = await nvi.verify(payload);
  if (!res.ok) throw new Error("nvi failed"); // hangi fail?
} catch (e) {
  await retry(payload); // eşleşmese bile tekrar dener
}

Çözüm: İki sonuç sınıfı arasında net ayrım yapın:

// iyi
const res = await nvi.verify(payload);
if (res.reason === 'UPSTREAM_ERROR') await retry();
else if (res.reason === 'NOT_MATCHED') showUserError();
else proceed();

Bu konuyu API entegrasyonu yazımızda detaylandırdık.

10. Database'de TC kimlik numarasını index'siz aramak

Problem: Büyük tablolarda WHERE tckn = '12345678901' sorgusu, tckn kolonunda index yoksa full table scan yapar. 100M kayıtta bu 30 saniye sürebilir. Unique constraint kimin tarafından ne zaman konulmuş, dokümanda yer almayan bir detay olur.

Yeniden üretim:

EXPLAIN SELECT * FROM users WHERE tckn = '12345678901';
-- Seq Scan on users  (cost=0.00..2345678.90 rows=1 width=...)

Çözüm: İki öneri:

  1. CREATE UNIQUE INDEX idx_users_tckn ON users(tckn); — Unique da ekler.
  2. TC kimlik numarasını çıplak tutmak istemiyorsanız: tckn_hash kolonu (SHA-256 + salt) ve o kolona index. Arama WHERE tckn_hash = ? olur. Gerçek TCKN şifreli tutulur.

İkinci yaklaşım KVKK uyumu için tercih edilir. Detay: KVKK rehberi.

Bonus: Client-side validation'a güvenmek

Aslında 11. madde: "TC kimlik formunu sadece JavaScript'te validate ediyoruz, server'a gittiğinde zaten DB constraint yakalıyor" yaklaşımı. Saldırgan DevTools'ta JS'i bypass eder, server'a bozuk TCKN yazar. DB constraint de yoksa (ya da sadece unique constraint varsa) geçersiz kayıt DB'ye düşer.

Çözüm: Her katmanda validate edin. Client'ta UX için, API gateway/BFF'de rate limit için, backend'de business rule için, DB'de constraint için. "Defense in depth" prensibi.

Özet test matrisi

Validator'unuzun aşağıdaki tablo ile test edilmesi iyi bir başlangıçtır:

| Girdi | Beklenen | Hata sınıfı | |-------|----------|-------------| | geçerli TC kimlik no | true | - | | "00000000000" | false | invalid_first_digit | | "abc" | false | non_numeric | | "١٢٣٤٥٦٧٨٩٠١" | false | non_ascii_digit | | " 12345678901 " | true (trim sonrası) | - | | null / undefined | false | null_input | | 1234567890 (number) | false | type_mismatch | | 12 haneli dize | false | length_mismatch | | son hanesi +1 bozulmuş geçerli TC | false | checksum_fail |

VKN için benzer tabloyu uzunluk 10 ile yapın.

Sonuç

Bu 10 hata, çoğu ekibin production'da yaşadığı ama dokümanında yazmadığı dersler. Yeni bir projeye TC kimlik / VKN validator yazarken kontrol listesi olarak kullanın. Bir sonraki sprint'inizde mevcut kod tabanında bu kalıpları arayın: muhtemelen en az 3-4'üne rastlayacaksınız.

Hızlı kontrol için TC Doğrulayıcı ve VKN Doğrulayıcı. Kendi dilinizde hazır kütüphane için Kütüphaneler.