Bölüm 01
PHP Güvenlik Temelleri
Temel PHP ve HTTP istek/yanıt akışı bilgisi.
Bu bölümü bitirdiğinizde, tekil açıkları ezberlemeden önce her açığın altında yatan ortak yapıyı — güven sınırlarının ihlalini — görebileceksiniz.
1. Giriş
Uygulama güvenliğinin tarihi, aslında güven kavramının kötüye kullanımının tarihidir. 1990'ların sonunda web, statik sayfalardan dinamik uygulamalara evrildiğinde, sunucu ilk kez tarayıcıdan gelen veriyi işlemeye başladı. PHP bu dönüşümün tam merkezindeydi: 1995'te Rasmus Lerdorf'un kişisel araç setinden doğan dil, düşük giriş bariyeri sayesinde webin dili hâline geldi.
Bu erişilebilirlik bir bedelle geldi. PHP, güvenliği sonradan düşünülen, "kolay olsun" felsefesiyle tasarlanmış birçok özellik taşıdı: dışarıdan gelen değişkenleri otomatik olarak global kapsama enjekte eden register_globals, tırnak işaretlerini sessizce kaçıran magic_quotes_gpc, tip dönüşümlerini sürücü gibi kullanan gevşek karşılaştırma. Bu özelliklerin her biri, o dönemde "kullanışlı" görüldü; her biri sonradan bir güvenlik felaketi kaynağı oldu.
Bugün bu mirasın çoğu dilden temizlendi (register_globals PHP 5.4'te, magic_quotes yine 5.4'te kaldırıldı). Ancak dilin kültürü — "çalışıyorsa yeterli" yaklaşımı — internetteki milyonlarca satır eski koda sinmiş durumda. PHP hâlâ web sunucularının çok büyük bir bölümünü çalıştırdığı için, PHP güvenliğini anlamak, webin kendisinin saldırı yüzeyini anlamaktır.
Bu bölüm tek bir açığı öğretmez. Bunun yerine, kitabın geri kalanının üzerine inşa edileceği dört temeli kurar: güven sınırları, PHP istek yaşam döngüsü, dilin güvenlik açısından tehlikeli davranışları ve tehdit modelleme zihniyeti.
2. Temel Kavram: Güven Sınırı (Trust Boundary)
Neredeyse her uygulama açığı tek bir cümleyle özetlenebilir: Güvenilmeyen veri, güvenilir bir bağlamda, doğrulanmadan kullanıldı.
Bir güven sınırı, verinin güvenilir olarak kabul edildiği alan ile edilmediği alan arasındaki hayali çizgidir. Veri bu çizgiyi her geçtiğinde ya doğrulanmalı (validation) ya da hedef bağlama uygun biçimde kodlanmalıdır (encoding). SQL Injection, XSS, Command Injection, SSRF — hepsi aynı hatanın farklı bağlamlardaki tezahürüdür: sınır ihlali.
GÜVENİLMEYEN ╳ güven sınırı ╳ GÜVENİLİR
┌──────────────┐ ┌──────────────────┐
│ Tarayıcı │ ── HTTP isteği ─────────╳─────────────▶ │ PHP Uygulaması │
│ Mobil app │ (GET/POST/Cookie/Header/Body/URL) │ │
│ 3. parti API│ │ BURADA veri │
│ Yüklenen │ │ "güvenilir" │
│ dosya │ │ SANILIR — hata! │
└──────────────┘ └──────────────────┘
Kritik nokta: Sınırın hangi tarafta olduğu kod tarafından değil, verinin kaynağı tarafından belirlenir. Bir geliştiricinin en sık yaptığı psikolojik hata, veriyi "kendi kodundan geldiği için" güvenilir sanmaktır. Oysa bir veritabanından okunan değer de, oraya daha önce bir kullanıcı yazmışsa, güvenilmezdir. Bu, ikinci dereceden (second-order) açıkların temelidir: veri bir yerde güvenle saklanır, sonra başka bir bağlamda güvenilir sanılarak kullanılır.
PHP'de güvenilmeyen veri kaynakları
Güven sınırının "dışarı" tarafında ne olduğunu bilmek zorunludur. PHP'de tipik güvenilmeyen kaynaklar:
| Kaynak | PHP karşılığı | Sık yapılan hata |
|---|---|---|
| Sorgu dizesi | $_GET |
Doğrudan SQL/HTML'e gömmek |
| Form gövdesi | $_POST |
Tip ve aralık doğrulamasını atlamak |
| Çerezler | $_COOKIE |
İstemcinin değiştirebileceğini unutmak |
| HTTP başlıkları | $_SERVER['HTTP_*'] |
Host, X-Forwarded-For, Referer'a güvenmek |
| Yüklenen dosyalar | $_FILES |
type ve dosya adına güvenmek |
| Ham gövde | php://input |
JSON/XML'i doğrulamadan ayrıştırmak |
| URL yolu | $_SERVER['REQUEST_URI'] |
Path traversal'ı gözden kaçırmak |
| Veritabanı (2. derece) | Sorgu sonucu | "İçeriden geldi" sanmak |
$_SERVER içindeki değerlerin bir kısmının istemci tarafından kontrol edilebildiğine dikkat edin. $_SERVER['HTTP_HOST'] başlığı, arkasında doğru yapılandırılmış bir sunucu yoksa, doğrudan istemcinin gönderdiği Host başlığından gelir ve host header injection açıklarına kapı aralar.
3. PHP İstek Yaşam Döngüsü ve "Shared-Nothing" Modeli
PHP'nin güvenlik davranışını anlamak için, dilin çalışma modelini anlamak gerekir. Java veya Node.js gibi uzun ömürlü süreç modellerinin aksine, klasik PHP shared-nothing (paylaşımsız) bir mimariyle çalışır: her HTTP isteği temiz bir sayfada başlar, kod çalışır, yanıt üretilir ve süreçle birlikte tüm bellek durumu yok edilir.
İstek gelir
↓
Web Sunucusu (Apache / Nginx)
↓
PHP-FPM ──► bir worker süreci isteği alır
↓
[ Bellek temiz ] → Bootstrap → Kod çalışır → Yanıt
↓
[ Bellek yok edilir ] ← süpergloballer, değişkenler, nesneler silinir
↓
Worker bir sonraki isteği bekler
Bu modelin iki önemli güvenlik sonucu vardır:
Olumlu tarafı: Bir isteğin belleği bir sonrakine sızmaz. Bir kullanıcının değişkenleri, başka bir kullanıcının isteğinde varsayılan olarak görünmez. Bu, birçok bellek-durumu sızıntısı sınıfını doğuştan engeller.
Dikkat edilecek tarafı: Kalıcılık gereken her şey (oturum, önbellek, kilit) süreç dışına taşınır — dosyaya, Redis'e, veritabanına. Bu dış depolar yeni güven sınırlarıdır. Örneğin varsayılan dosya-tabanlı oturum deposu (/tmp veya /var/lib/php/sessions), paylaşımlı sunucularda başka bir kiracının okuyabileceği bir yer olabilir. Ayrıca uzun ömürlü worker'larda (özellikle pcntl veya Swoole/RoadRunner gibi kalıcı çalıştırma ortamlarında) shared-nothing garantisi kaybolur; statik değişkenler ve container durumu istekler arası taşınır ve bilgi sızıntısına yol açabilir.
Modern asenkron çalıştırma ortamlarını (Swoole, RoadRunner, FrankenPHP) kullanıyorsanız, klasik PHP'nin "her istek temiz başlar" varsayımını doğrulamadan taşımayın. Bu, kitabın API bölümünde ayrıntılandırılır.
4. PHP'nin Güvenlik Açısından Tehlikeli Davranışları
PHP, geliştiriciyi rahatlatmak için tasarlanmış esnek davranışlar içerir. Bu esneklik, güvenlik bağlamında sıklıkla ayağa dolanır. En kritik üçünü ele alalım.
4.1. Tip Hokkabazlığı (Type Juggling) ve Gevşek Karşılaştırma
PHP, == operatörüyle iki değeri karşılaştırırken tiplerini otomatik dönüştürür. Bu, güvenlik açısından tehlikeli sonuçlar doğurur. En bilinen örnek magic hash sorunudur:
// ⚠️ GÜVENSİZ — parola hash'i doğrulaması
$hashFromDb = "0e462097431906509019562988736854"; // md5 sonucu
$userInput = md5($_POST['password']);
if ($hashFromDb == $userInput) { // GEVŞEK karşılaştırma
// giriş başarılı
}
"0e462097431906509019562988736854" gibi başında 0e bulunan ve gerisi tamamen rakamdan oluşan bir dize, PHP tarafından bilimsel gösterimli sayı (0 × 10^…) olarak yorumlanır ve sayısal değeri sıfırdır. Başka bir "0e…" hash'i de sıfıra eşit olacağından, iki farklı dize == altında eşit çıkar. Saldırgan, md5/sha1 çıktısı yine böyle bir "0e" biçimine düşen özel girdiler bularak parola bilmeden doğrulamayı geçebilir.
PHP 8, string ↔ number karşılaştırma kurallarını değiştirdi: artık 0 == "foo" false döner (PHP 7'de true idi). Ancak iki dize karşılaştırıldığında ve her ikisi de sayısal dize ("0e123") ise, sayısal karşılaştırma hâlâ geçerlidir; magic hash sorunu PHP 8'de de sürer. Çözüm sürüme değil, operatöre bağlıdır:
// ✅ GÜVENLİ — sıkı, tipe duyarlı karşılaştırma
if (hash_equals($hashFromDb, $userInput)) {
// ...
}
hash_equals(), hem tipe duyarlı (=== mantığı) hem de zamanlama saldırılarına karşı sabit süreli karşılaştırma yapar. Parola/token doğrulamada == ve hatta === yerine daima hash_equals() kullanılır. Genel kural: güvenlik kararı veren her karşılaştırma sıkı olmalıdır.
Aynı tuzak dizi fonksiyonlarında da gizlidir:
// ⚠️ GÜVENSİZ — gevşek in_array
$allowed = [0, 1, 2];
in_array("admin", $allowed); // true! ("admin" → 0'a dönüşür, eski PHP'de)
in_array("admin", $allowed, true); // ✅ false — üçüncü parametre sıkı kontrol
in_array, array_search, switch gibi yapılar varsayılan olarak gevşek karşılaştırır. Yetki listesi kontrolü gibi güvenlik kararlarında üçüncü parametreyi (strict = true) daima verin.
4.2. Tehlikeli Fonksiyonlar ve Dinamik Yürütme
PHP, dizeleri koda dönüştüren güçlü ama tehlikeli yapılar içerir. Bunların hepsi, güvenilmeyen veriyle beslendiğinde uzaktan kod çalıştırmaya (RCE) giden yollardır:
| Fonksiyon / Yapı | Riski |
|---|---|
eval() |
Dize'yi PHP kodu olarak çalıştırır |
system, exec, shell_exec, passthru, `` (backtick) |
Kabuk komutu çalıştırır |
preg_replace (eski /e bayrağı) |
Değişimi kod olarak çalıştırırdı (PHP 7'de kaldırıldı) |
include, require (dinamik yol) |
Uzak/yerel dosya dahil etme (LFI/RFI) |
unserialize() |
Nesne enjeksiyonu (bkz. Bölüm 11) |
call_user_func, $fn() (değişken fonksiyon) |
Keyfi fonksiyon çağrısı |
extract() |
Diziyi değişkenlere açar; değişken üzerine yazma |
Kural nettir: Bu yapıların hiçbirine güvenilmeyen veri, doğrulama ve beyaz liste olmadan girmemelidir. Kod incelemede bu fonksiyonları aramak, ilk taramanın en verimli adımıdır.
4.3. Sessiz Başarısızlık ve Tip Gevşekliği
PHP tarihsel olarak hataları sessizce yutmaya eğilimlidir. @ operatörü hataları bastırır; birçok fonksiyon başarısızlıkta istisna değil false döner. Güvenlik kararı veren bir fonksiyonun false dönüşünü kontrol etmemek, açık üretir:
// ⚠️ GÜVENSİZ — dönüş değeri kontrol edilmiyor
$data = openssl_decrypt($ciphertext, $cipher, $key, 0, $iv);
process($data); // openssl_decrypt false dönerse, false'ı işleriz
// ✅ GÜVENLİ
$data = openssl_decrypt($ciphertext, $cipher, $key, 0, $iv);
if ($data === false) {
throw new RuntimeException('Şifre çözme başarısız');
}
declare(strict_types=1); yönergesini her dosyanın başına ekleyin. Bu, PHP'nin fonksiyon argümanlarında sessiz tip dönüşümü yapmasını engeller ve tip kaynaklı mantık hatalarını erken yakalar.
5. Savunma Felsefesi: Derinlemesine Savunma ve Güvenli Varsayılanlar
Tekil açıkları öğrenmeden önce, onlara karşı duruşumuzu belirleyen üç ilkeyi içselleştirin.
Derinlemesine savunma (defense in depth): Hiçbir tek kontrol kusursuz değildir. Bir prepared statement SQL Injection'ı engeller, ama en az yetkili veritabanı kullanıcısı, WAF ve loglama, o kontrol bir gün başarısız olursa hasarı sınırlar. Katmanlar bağımsız olmalı ki biri düştüğünde diğerleri ayakta kalsın.
Güvenli varsayılanlar (secure by default): Sistem, yapılandırılmadığında güvenli olmalıdır — güvensiz değil. Bir yetkilendirme kontrolü "aksi belirtilmedikçe reddet" mantığıyla yazılırsa, yeni bir uç nokta eklendiğinde birinin izin vermeyi unutması yalnızca erişimi engeller, açığa yol açmaz. Tersi felakettir.
En az yetki (least privilege): Her bileşen, işini yapmak için gereken asgari yetkiye sahip olmalıdır. Web uygulamasının veritabanı kullanıcısı DROP TABLE yapabilmemeli; PHP-FPM süreci root çalışmamalı; uygulama dizini web sunucusu tarafından yazılabilir olmamalıdır.
Tek katman (kırılgan): Derinlemesine savunma (dayanıklı):
[ Uygulama kodu ] [ Input validation ]
↓ ↓
Veritabanı [ Prepared statement ]
↓
Kontrol düşerse → [ En az yetkili DB user ]
sistem düşer. ↓
[ WAF + Rate limit ]
↓
[ Loglama + Alarm ]
Biri düşse de sistem ayakta.
6. Tehdit Modelleme Zihniyetine Giriş (STRIDE)
Güvenli kod yazmanın ilk adımı, "ne ters gidebilir?" sorusunu sistematik olarak sormaktır. Microsoft'un STRIDE modeli, bu soruyu altı kategoriye böler ve Bölüm 2'de derinlemesine işlenecektir. Burada zihinsel çerçeveyi kuralım:
| Harf | Tehdit | PHP bağlamında örnek |
|---|---|---|
| Spoofing | Kimlik taklidi | Zayıf oturum token'ı, JWT imza doğrulamama |
| Tampering | Veri kurcalama | IDOR, gizli form alanı manipülasyonu |
| Repudiation | İnkâr | Yetersiz loglama, iz bırakmayan işlemler |
| Information Disclosure | Bilgi sızıntısı | Ayrıntılı hata mesajları, .git ifşası |
| Denial of Service | Hizmet dışı bırakma | ReDoS, sınırsız dosya yükleme |
| Elevation of Privilege | Yetki yükseltme | Bozuk erişim kontrolü, mass assignment |
Bir özellik geliştirirken her veri akışını bu altı mercekten geçirmek, açıkları koda girmeden yakalamanın en ucuz yoludur. Üretimde bulunan bir açığı düzeltmenin maliyeti, tasarımda yakalamanın maliyetinin kat kat üzerindedir.
7. Checklist — Her Projede Temel Hijyen
- [ ] Her PHP dosyasının başında
declare(strict_types=1);var mı? - [ ] Güvenlik kararı veren tüm karşılaştırmalar
===veyahash_equals()mı? - [ ]
in_array/array_searchçağrılarındastrict = trueverildi mi? - [ ]
eval,system,exec, dinamikincludegibi yapılar güvenilmeyen veriyle beslenmiyor mu? - [ ] Üretimde
display_errors = Offmı? (Bilgi sızıntısı) - [ ] Uygulama en az yetkili DB kullanıcısı ve OS kullanıcısıyla mı çalışıyor?
- [ ] Güvenilmeyen kaynaklar (
$_GET/$_POST/$_COOKIE/$_SERVER/$_FILES) açıkça tanımlanıp doğrulanıyor mu? - [ ] Kritik güvenlik fonksiyonlarının (
openssl_*,hash_*) dönüş değerleri kontrol ediliyor mu?
8. Laboratuvar
Lab 1.1 — Type juggling'i gözlemleyin. Aşağıdaki kodu bir PHP 8 ortamında çalıştırın ve çıktıyı açıklayın:
declare(strict_types=1);
var_dump("0e123" == "0e456"); // ne döner? neden?
var_dump("0e123" === "0e456"); // ne döner?
var_dump(hash_equals("0e123", "0e456"));
var_dump(in_array("1abc", [1, 2, 3]));
var_dump(in_array("1abc", [1, 2, 3], true));
Her satırın neden o değeri döndürdüğünü bir cümleyle yazın.
Lab 1.2 — Güven sınırı haritası. Kendi (veya örnek bir) projenizde tek bir uç noktayı seçin. Verinin girdiği tüm kaynakları ($_GET, başlıklar, dosya, DB) listeleyin ve her birinin güven sınırının hangi tarafında olduğunu işaretleyin. Sınırı geçen her veri için "doğrulanıyor mu / kodlanıyor mu?" sorusunu yanıtlayın.
Lab 1.3 — Tehlikeli fonksiyon avı. Bir kod tabanında grep -rn "eval\|system\|exec\|shell_exec\|passthru\|unserialize" . çalıştırın. Her sonucun güvenilmeyen veriye ulaşıp ulaşmadığını izleyin.
9. Quiz
- Güven sınırı nedir ve neden "veri kaynak tarafından güvenilir sayılır" düşüncesi tehlikelidir?
- İkinci dereceden (second-order) açık ne demektir? Bir örnek verin.
"0e123" == "0e456"PHP 8'de neden hâlâtruedöner?- PHP 8, PHP 7'ye göre string-number karşılaştırmasında neyi değiştirdi? Bu değişiklik magic hash sorununu çözer mi?
hash_equals()fonksiyonunun==='e göre iki üstünlüğü nedir?- Shared-nothing modelinin bir güvenlik avantajını ve bir dikkat noktasını yazın.
$_SERVER['HTTP_HOST']neden güvenilmeyen bir kaynaktır?- "Secure by default" ilkesini bir yetkilendirme kontrolü üzerinden açıklayın.
in_arrayneden varsayılan hâliyle bir yetki kontrolünde tehlikelidir?- STRIDE'daki "Elevation of Privilege" kategorisine PHP dünyasından iki örnek verin.
10. Kaynakça
- OWASP, Application Security Verification Standard (ASVS), v4.0.3.
- OWASP, Top 10:2021.
- MITRE, CWE-697: Incorrect Comparison; CWE-1025: Comparison Using Wrong Factors.
- PHP Manual, Comparison Operators ve Type Juggling bölümleri (php.net/manual).
- PHP Manual,
hash_equals,password_hashreferansları. - Microsoft, The STRIDE Threat Model.
- NIST SP 800-160 Vol. 1, Systems Security Engineering (defense in depth, least privilege ilkeleri).