HG PHP GüvenliğiEl Kitabı
Bölümler 04 / 38

İçindekiler/Temeller ve Metodoloji

Bölüm 04

Girdi Doğrulama (Input Validation)

10 dk okuma1.906 kelimePHP 8.1+
Ön koşul

Bölüm 1 (güven sınırı, güvenilmeyen kaynaklar).

Bu bölümün kazanımı

Girdi doğrulamayı bir "güvenlik sihri" değil, güven sınırında uygulanan sistematik bir sözleşme olarak kavrayacak; doğrulamanın ne olduğunu ve daha önemlisi ne olmadığını (bir kodlama/kaçış yerine geçmediğini) ayırt edeceksiniz.

1. Giriş

Girdi doğrulama, uygulama güvenliğinin en yanlış anlaşılan konusudur. İki uç yanılgı yaygındır. Birincisi, "her şeyi doğrularsam güvendeyim" — oysa doğrulama tek başına enjeksiyonu çözmez. İkincisi, "framework hallediyordur" — oysa framework yalnızca söylediğiniz kadarını doğrular.

Doğru bakış şudur: Girdi doğrulama, güven sınırında (Bölüm 1) uygulanan bir sözleşmedir. "Bu alan yalnızca şu biçimdeki veriyi kabul eder" der. Bu sözleşme, saldırı yüzeyini daraltır, mantık hatalarını önler ve derinlemesine savunmanın (defense in depth) ilk katmanını oluşturur. Ama son katmanı değildir: doğrulanmış veri bile bir SQL sorgusuna gömülürken parametreleştirilmeli, bir HTML'e yazılırken kodlanmalıdır. Doğrulama ile bağlamsal kodlama (Bölüm 5), birbirinin yerine geçmeyen iki ayrı savunmadır.


2. Temel Teori: Beyaz Liste vs. Kara Liste

Doğrulamanın iki felsefesi vardır ve aralarındaki fark, güvenliğin belkemiğidir.

Kara liste (blacklist / denylist): "Şu kötü şeyleri reddet." Örneğin <script> dizesini yasakla. Bu yaklaşım temelden kusurludur, çünkü tüm kötü varyasyonları önceden bilmeniz gerekir — ki imkânsızdır. Saldırgan <ScRiPt>, <img onerror=...>, kodlanmış varyantlar veya henüz bilinmeyen bir vektörle listenizi atlatır. Kara liste, "bilmediğinizi bilmediğiniz" saldırılara karşı savunmasızdır.

Beyaz liste (whitelist / allowlist): "Yalnızca şu iyi şeyleri kabul et, gerisini reddet." Örneğin "bu alan yalnızca 1–20 arası rakam içerebilir." Bu yaklaşım varsayılan olarak güvenlidir (Bölüm 1, secure by default): bilmediğiniz saldırılar dahil, tanımınıza uymayan her şey reddedilir.

 KARA LİSTE (kırılgan):          BEYAZ LİSTE (dayanıklı):
   "Kötüleri say ve reddet"        "İyiyi tanımla, gerisini reddet"

   reddet: <script>                kabul: ^[a-z0-9_]{3,20}$
   reddet: <iframe>                → tanıma uymayan HER ŞEY reddedilir
   reddet: onerror
   ...                             Saldırgan yeni bir vektör bulsa da
   Saldırgan yeni vektör           beyaz liste onu zaten dışarıda bırakır.
   bulursa → geçer.

Kural: Girdi doğrulama daima beyaz liste ile yapılır. Kara liste yalnızca beyaz listenin mümkün olmadığı, ikincil bir katman olarak düşünülebilir — asla tek savunma olarak değil.


3. Sözdizimsel ve Anlamsal Doğrulama

Doğrulama iki katmanda çalışır:

Sözdizimsel (syntactic) doğrulama: Verinin biçimi doğru mu? Bir e-posta e-posta biçiminde mi, bir tarih geçerli bir tarih mi, bir sayı gerçekten sayı mı? Bu, tip ve format kontrolüdür.

Anlamsal (semantic) doğrulama: Verinin anlamı iş kuralına uygun mu? Tarih gelecekte olamayacak bir doğum tarihi mi? Sipariş adedi stoktan fazla mı? İki alan tutarlı mı? Bu, bağlama bağlı iş mantığı kontrolüdür.

Sözdizimsel doğrulamayı geçen veri hâlâ anlamsal olarak geçersiz olabilir. Örneğin 2099-01-01 geçerli bir tarih biçimidir (sözdizimsel geçer) ama bir doğum tarihi olarak geçersizdir (anlamsal başarısız). Business logic açıkları (Bölüm 18) çoğunlukla eksik anlamsal doğrulamadan doğar.


4. Güvensiz Kod

Gerçekçi bir kayıt (registration) uç noktası:

<?php
// ⚠️ GÜVENSİZ — girdi doğrulama eksik/yanlış
declare(strict_types=1);

$username = $_POST['username'];
$email    = $_POST['email'];
$age      = $_POST['age'];
$role     = $_POST['role'] ?? 'user';
$website  = $_POST['website'] ?? '';

// "Doğrulama" — kara liste denemesi
if (strpos($username, '<script>') !== false) {
    die('Geçersiz kullanıcı adı');
}

$user = User::create([
    'username' => $username,
    'email'    => $email,
    'age'      => $age,
    'role'     => $role,       // kullanıcı kendi rolünü belirliyor!
    'website'  => $website,
]);

5. Açığın Analizi

Kodu inceleyelim:

$username — Yalnızca <script> alt-dizesi aranıyor: klasik kara liste hatası. <ScRiPt>, <img src=x onerror=...> ve sayısız varyant geçer. Ayrıca uzunluk, izinli karakter kümesi ve boşluk kontrolü yok.

$email — Hiç doğrulanmıyor. Geçersiz format, header injection (\r\n ile CRLF) ve aşırı uzunluk mümkün.

$age — Tip ve aralık kontrolü yok. Bir dize, negatif sayı veya 9999 gelebilir; anlamsal doğrulama tamamen eksik.

$role — En kritik hata: kullanıcı kendi rolünü belirliyor. role=admin göndererek yetki yükseltme (STRIDE'da "Elevation of Privilege"). Bu aynı zamanda bir mass assignment açığıdır: kullanıcı girdisi doğrudan bir modelin özniteliklerine akıyor.

$website — Doğrulanmıyor; ileride bir bağlamda (profil sayfasında link olarak) kullanıldığında javascript: şeması ile XSS'e (Bölüm 7) veya SSRF'e (Bölüm 21) kaynak olabilir.

Kök sorun: doğrulama kara liste temelli, eksik ve sözleşmesiz. Hiçbir alanın "ne olması gerektiği" tanımlanmamış.


6. Hacker Bakış Açısı

Saldırgan, doğrulama zayıflıklarını nasıl arar?

Beklenmeyen tipleri dener. Bir sayı beklenen alana dize, bir dize beklenen alana dizi (age[]=1) gönderir. PHP'nin gevşek tip davranışı, doğrulanmamış tip dönüşümlerinde beklenmedik sonuçlar üretebilir.

Sınırları zorlar. Çok uzun girdiler (DoS, buffer sorunları), sınır değerler (0, -1, MAX_INT), boş değerler (null, boş dize) gönderir. Anlamsal doğrulama eksikse bunlar iş mantığını kırar.

Gizli/beklenmedik alanları enjekte eder. Formda görünmeyen ama modelde var olan alanları (role, is_admin, credit) isteğe ekler — mass assignment sınaması. Bu, "arayüzde yok" diye korunmayan alanları hedefler.

Kodlama katmanlarıyla oynar. Kara liste varsa, URL kodlama, çift kodlama, Unicode normalizasyon ve farklı karakter kümeleriyle listeyi atlatmayı dener.

Savunmacı dersi: Saldırgan daima sizin tanımlamadığınız durumu arar. Beyaz liste, "tanımlanmamış her şey reddedilir" diyerek bu arayışı en baştan boşa çıkarır.


7. Güvenli Kod

Aynı uç noktanın, beyaz liste ve tip zorlamalı güvenli sürümü:

<?php
// ✅ GÜVENLİ — beyaz liste temelli, sözleşmeli doğrulama
declare(strict_types=1);

/** Sözdizimsel + anlamsal doğrulama, beyaz liste ile */
function validateRegistration(array $in): array
{
    $errors = [];

    // username: kesin karakter kümesi ve uzunluk (beyaz liste)
    $username = trim($in['username'] ?? '');
    if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
        $errors['username'] = '3-20 karakter; harf, rakam, alt çizgi.';
    }

    // email: PHP'nin yerleşik filtresi (sözdizimsel) + uzunluk
    $email = trim($in['email'] ?? '');
    if (strlen($email) > 254 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = 'Geçerli bir e-posta girin.';
    }

    // age: tip zorlama (sözdizimsel) + aralık (anlamsal)
    $age = filter_var($in['age'] ?? null, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 13, 'max_range' => 120],
    ]);
    if ($age === false) {
        $errors['age'] = '13-120 arası bir yaş girin.';
    }

    // website: opsiyonel; yalnızca http/https şeması (beyaz liste)
    $website = trim($in['website'] ?? '');
    if ($website !== '') {
        $url = filter_var($website, FILTER_VALIDATE_URL);
        $scheme = $url ? strtolower(parse_url($url, PHP_URL_SCHEME) ?? '') : '';
        if (!$url || !in_array($scheme, ['http', 'https'], true)) {
            $errors['website'] = 'Yalnızca http/https adresi.';
        }
    }

    return ['errors' => $errors, 'data' => compact('username', 'email', 'age', 'website')];
}

$result = validateRegistration($_POST);
if ($result['errors']) {
    http_response_code(422);
    echo json_encode(['errors' => $result['errors']]);
    exit;
}

// role KULLANICIDAN ALINMAZ — sunucu belirler (mass assignment önlemi)
$data = $result['data'];
$data['role'] = 'user';

$user = User::create($data); // yalnızca doğrulanmış, izinli alanlar

Modern PHP araçları: filter_var (FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT, FILTER_VALIDATE_URL) sözdizimsel doğrulama için; preg_match ile beyaz liste desenleri; aralık seçenekleriyle anlamsal kontrol. Framework kullanıyorsanız Laravel'in FormRequest / Validator veya Symfony'nin Validator bileşeni aynı sözleşmeyi bildirimsel biçimde kurar.


8. Patch Analizi

Neden işe yarar? Her alan artık bir sözleşmeye bağlı: izinli karakter kümesi, tip, aralık ve şema. Tanıma uymayan her şey reddedilir (beyaz liste). role sunucu tarafından atanır; kullanıcı girdisi ayrıcalıklı alanlara ulaşamaz.

Alternatifler:

Yaklaşım Değerlendirme
Kara liste (strpos '<script>') Yetersiz; atlatılabilir.
filter_var + regex beyaz liste Sözdizimsel için sağlam, yerleşik.
Framework validator (Laravel/Symfony) Bildirimsel, tutarlı, önerilen.
Şema doğrulama (JSON Schema, API'lerde) Yapılandırılmış girdi için güçlü.

Mass assignment önlemi: Modelde "doldurulabilir (fillable)" alanları açıkça beyaz listelemek (Laravel $fillable, Symfony'de DTO'lar) veya girdiyi bir DTO'ya haritalamak, role gibi alanların asla dışarıdan yazılamamasını sağlar.

Performans: Doğrulama maliyeti ihmal edilebilir; birkaç regex ve filtre çağrısı. DoS'a karşı ise koruyucudur (uzunluk sınırları erken reddeder). Tek dikkat: ReDoS — kötü yazılmış (katastrofik geri izlemeli) regex'ler bir DoS vektörüdür; desenleri basit ve sınırlı tutun.


9. Kanonikleştirme (Canonicalization) Tuzağı

Doğrulamanın en sinsi hatası, veriyi yanlış temsilde doğrulamaktır. Bir girdi doğrulanmadan önce kanonik (tekil, normalize) biçimine getirilmelidir; aksi halde saldırgan alternatif kodlamalarla doğrulamayı atlatır.

Klasik örnek path traversal (Bölüm 20): ../ doğrulaması, ..%2f, ..%252f (çift kodlama) veya Unicode varyantlarıyla atlatılabilir. Doğru sıra: önce kanonikleştir (decode + normalize + realpath), sonra doğrula. Aynı ilke Unicode kullanıcı adları, e-posta ve dosya adlarında geçerlidir. "Doğrulamadan önce hangi temsilde olduğunu bil" kuralı, birçok atlatma sınıfını kapatır.


10. Gerçek Hayat Senaryosu

Kurgu — bir SaaS proje yönetim aracı. Kullanıcı davet formu, davet edilen kişinin rolünü (member) belirler. Backend, gelen role alanını doğrudan kullanır ve yalnızca "arayüz sadece member/viewer gösteriyor" diye güvenir. Bir kullanıcı, isteği elle düzenleyip role=owner gönderir. Doğrulama sözleşmesi olmadığından backend bunu kabul eder ve saldırgan, davet ettiği kendi ikinci hesabını "owner" yaparak tüm çalışma alanını ele geçirir. Tek eksik: rolün beyaz listeden geçmemesi ve ayrıcalıklı bir değerin dışarıdan atanabilmesi. Bu, doğrulama + yetki (Bölüm 14) katmanlarının birlikte gerektiğini gösterir.


11. Gerçek CVE Örneği

Girdi doğrulama sınıfının en bilinen gerçek dünya tezahürü mass assignmenttır. GitHub, 2012'de Rails uygulamalarını hedefleyen bir mass assignment gösterimiyle sarsıldı: bir araştırmacı, uygun beyaz liste olmadığında kullanıcı girdisinin beklenmeyen model alanlarına (örn. yetki alanlarına) yazılabildiğini kamuya kanıtladı. Aynı sınıf, PHP framework'lerinde $fillable/$guarded yanlış yapılandırıldığında geçerlidir (CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes). Ders: girdi doğrulama yalnızca "format" değil, "hangi alanların yazılabileceği"nin de beyaz listelenmesidir.

Genel bir öğreti olarak veriyorum; kendi projenizde ilgili CWE ve framework advisory'lerini doğrulayın.


12. Detection

Kod incelemede: Süpergloballerden ($_GET/$_POST/...) modele/sorguya/dosyaya giden yolları izleyin. Her yolda bir beyaz liste doğrulaması var mı? strpos/str_replace tabanlı kara liste "doğrulamaları" kırmızı bayraktır. Model::create($_POST) veya ->fill($request->all()) kalıpları mass assignment işaretidir.

SAST: Taint analizi, doğrulanmamış girdinin hassas alanlara akışını gösterir. Framework-farkında kurallar (Larastan, Psalm eklentileri) mass assignment'ı yakalayabilir.

DAST: Beklenmeyen alan enjeksiyonu, tip karıştırma ve sınır-değer testleriyle eksik doğrulamayı bulur.

Loglarda: 422/400 hata oranındaki ani artış, otomatik bir doğrulama-atlatma taramasının işareti olabilir; aynı IP'den sistematik alan varyasyonları SIEM'de korele edilir.


13. Prevention

  • Merkezî doğrulama katmanı: Doğrulamayı controller'a dağıtmak yerine tek yerde (FormRequest / DTO / middleware) toplayın.
  • Beyaz liste varsayılan: Tüm alanlar için izinli değer/format tanımlanır; tanımsız reddedilir.
  • Mass assignment koruması: Modeller $fillable ile açık beyaz listeye alınır; ayrıcalıklı alanlar (role, is_admin) asla dışarıdan yazılamaz.
  • Kanonikleştir-sonra-doğrula: Kodlanmış/normalize edilmemiş veri hiç doğrulanmaz.
  • Şema sözleşmeleri (API): JSON Schema / OpenAPI ile girdi sözleşmesini makine-doğrulanabilir yapın.

14. Mitigation

Canlı sistemde eksik doğrulamadan kaynaklı aktif istismar varsa: - Acil: etkilenen uç noktaya sıkı, sunucu tarafı doğrulama (özellikle ayrıcalıklı alanlar için tip/beyaz liste) enjekte edin; WAF'ta alan-tabanlı geçici kural. - Geçici: mass assignment ile değişmiş olabilecek kayıtları (yetki yükseltilmiş hesaplar) tespit edip düzeltin. - Kalıcı: merkezî doğrulama + $fillable beyaz listesi + testler.


15. Checklist

  • [ ] Her girdi alanı beyaz liste ile mi doğrulanıyor (kara liste değil)?
  • [ ] Sözdizimsel ve anlamsal doğrulama var mı?
  • [ ] Tipler açıkça zorlanıyor mu (FILTER_VALIDATE_INT, (int))?
  • [ ] Uzunluk/aralık sınırları var mı (DoS + mantık)?
  • [ ] role/is_admin gibi ayrıcalıklı alanlar dışarıdan yazılamıyor mu (mass assignment)?
  • [ ] URL/şema alanları yalnızca izinli şemalara (http/https) mı izin veriyor?
  • [ ] Veri, doğrulamadan önce kanonikleştiriliyor mu?
  • [ ] Regex'ler ReDoS'a karşı basit ve sınırlı mı?
  • [ ] Doğrulama bir ek katman olarak görülüyor, kodlama/parametreleştirmenin yerine değil mi?

16. Laboratuvar

Lab 4.1 — Kara listeyi kır. Bölümdeki güvensiz <script> kara listesini yerel olarak kurun ve onu atlatan en az üç farklı girdi bulun (büyük/küçük harf, farklı etiket, kodlama). Sonra beyaz liste ile yeniden yazıp aynı girdilerin reddedildiğini doğrulayın.

Lab 4.2 — Mass assignment. User::create($_POST) kalıbını izole bir ortamda kurun. role=admin enjekte edip yetkinin yükseldiğini gözlemleyin. $fillable beyaz listesiyle düzeltin.

Lab 4.3 — Kanonikleştirme. Bir dosya adı doğrulamasını ../ kara listesiyle yazın, ..%2f ile atlatın, sonra realpath + kanonikleştirme ile düzeltin.


17. Quiz

  1. Beyaz liste ve kara liste arasındaki temel fark nedir? Neden beyaz liste güvenli varsayılandır?
  2. Sözdizimsel ve anlamsal doğrulama farkı nedir? Her birine örnek verin.
  3. 2099-01-01 bir doğum tarihi olarak hangi doğrulamayı geçer, hangisinde kalır?
  4. Mass assignment açığı nasıl oluşur ve nasıl önlenir?
  5. Girdi doğrulama, SQL Injection'ı tek başına neden çözmez?
  6. Kanonikleştirme neden doğrulamadan önce yapılmalıdır?
  7. filter_var ile FILTER_VALIDATE_URL kullanmak neden tek başına javascript: XSS'ini engellemeye yetmez?
  8. ReDoS nedir ve doğrulama regex'lerinde nasıl önlenir?
  9. Kara liste temelli "doğrulama"yı kod incelemede nasıl tanırsınız?
  10. Doğrulama ile bağlamsal kodlama neden birbirinin yerine geçmez?

18. Kaynakça

  • OWASP, Input Validation Cheat Sheet.
  • OWASP, Mass Assignment Cheat Sheet.
  • MITRE, CWE-20: Improper Input Validation; CWE-915: Mass Assignment.
  • PHP Manual, Filter Functions (filter_var, filtre sabitleri).
  • OWASP ASVS, V5 (Validation, Sanitization and Encoding).
  • MITRE, CWE-1289: Improper Validation of Unsafe Equivalence in Input (kanonikleştirme).