PHP güvenlik önlemleri: gerçek projelerde en sık gördüğüm 8 açık

Semih5 dk okuma

SQL injection, XSS, CSRF, parola saklama, oturum, dosya yükleme ve hata ayarları: PHP projelerindeki en sık açıklar ve kod örnekli önlemler.

Devraldığım PHP projelerinde güvenlik açıkları nadiren egzotiktir. Film sahnelerindeki gibi yeşil yazıların aktığı sofistike saldırılarla değil, on beş yıldır bilinen, adı konmuş, çözümü de belli olan hatalarla karşılaşıyorum. Kötü haber bu. İyi haber de bu: listeyi bilen biri açıkların çoğunu bir öğleden sonrada kapatabilir.

Aşağıdaki sekiz madde, bir projeyi devraldığımda ilk kontrol ettiğim şeyler. Kod örnekleri düz PHP ile yazıldı. Laravel, Symfony gibi bir çatı kullanıyorsan çoğunu çatı zaten yapıyor ama nasıl yaptığını bilmek, çatının dışına çıktığın anda seni korur.

Kısa cevap: sorguları hazırlanmış ifadelerle çalıştır, çıktıyı bağlama göre temizle, formlara CSRF belirteci ekle, parolaları password_hash ile sakla, oturum çerezlerinin güvenliğini artır, yüklenen dosyaya güvenme, kullanıcı girdisiyle dosya ya da nesne yükleme, üretimde hataları ekrana basma. Bir de PHP sürümünü ve bağımlılıkları güncel tut.

1. SQL injection: sorguyu metin birleştirerek kurma

Hâlâ bir numara. Sebebi de hep aynı:

// Yapma
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";

Kullanıcının yazdığı metin sorgunun parçası olursa, sorguyu da kullanıcı yazmış olur. Çözüm, veriyi sorgudan ayıran hazırlanmış ifadeler (prepared statements):

$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$_POST['email']]);

Dikkat edilecek bir nokta: tablo ve sütun adları parametre olarak bağlanamaz. ORDER BY ile kullanıcının seçtiği sütuna göre sıralama yapıyorsan, gelen değeri izin verilen bir listeyle karşılaştır:

$sort = in_array($_GET['sort'] ?? '', ['name', 'created_at'], true) ? $_GET['sort'] : 'created_at';

2. XSS: çıktıyı her zaman temizle (escape)

Kullanıcıdan gelen bir metni sayfaya olduğu gibi basarsan, o metin bir <script> etiketi olduğunda ziyaretçinin tarayıcısında kod çalışır. Oturum çerezi çalınır, sahte formlar gösterilir. Kural basit: veri girişte doğrulanır, çıkışta temizlenir.

function e(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

echo '<p>' . e($comment) . '</p>';

Şablon motorları bunu otomatik yapar: Blade’de {{ }}, Twig’de varsayılan davranış. Smarty’de ise otomatik temizleme varsayılan olarak kapalı; $smarty->setEscapeHtml(true) ile açabilir ya da değişkenlere |escape ekleyebilirsin. WHMCS ve WiseCP tema geliştirirken bu ayrıntıyı unutmamak gerekiyor.

Ek bir katman olarak Content-Security-Policy başlığı, sayfaya sızan bir betiğin çalışmasını zorlaştırır. Temizlemenin yerini tutmaz ama kaçırdığın tek bir yerde seni kurtarabilir.

3. CSRF: formun senden geldiğini kanıtla

Kullanıcı senin sitende oturum açıkken başka bir siteye girer. O site gizlice senin sitene “e-posta adresini değiştir” formu gönderir ve tarayıcı çerezi de beraberinde yollar. Önlem, her forma oturuma bağlı rastgele bir belirteç eklemek:

// Formu gösterirken
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
echo '<input type="hidden" name="csrf" value="' . e($_SESSION['csrf']) . '">';

// Formu işlerken
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(403);
    exit;
}

hash_equals, karşılaştırmanın süresinden bilgi sızmasını engeller. Çerezlerde SameSite=Lax kullanmak ek bir koruma sağlar.

4. Parolalar: md5 değil, password_hash

2026’da hâlâ md5($password) görüyorum. md5 ve sha1 hızlı olmak için tasarlandı, parola saklamak için tam olarak istemediğin özellik bu. PHP’nin kendi fonksiyonları salt eklemeyi ve algoritma seçimini senin yerine yapar:

$hash = password_hash($password, PASSWORD_DEFAULT);

if (password_verify($input, $hash)) {
    if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
        // Yeni hash'i kaydet
    }
}

PASSWORD_DEFAULT zamanla daha güçlü bir algoritmaya geçebilir. password_needs_rehash sayesinde eski hash’ler kullanıcılar giriş yaptıkça kendiliğinden yenilenir. Veritabanında hash sütununu en az 255 karakter tut.

5. Oturum: çerezin güvenliğini artır, kimliği yenile

session_set_cookie_params([
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();

// Başarılı girişten hemen sonra
session_regenerate_id(true);

httponly, JavaScript’in oturum çerezini okumasını engeller. session_regenerate_id ise saldırganın önceden bildiği bir oturum kimliğiyle kullanıcıyı giriş yaptırması (session fixation) senaryosunu kapatır. php.ini tarafında session.use_strict_mode = 1 da açık olsun.

6. Dosya yükleme: hiçbir şeye güvenme

Dosya yükleme formu, bir saldırganın sunucuna kod bırakabileceği en kısa yoldur. Ne dosya uzantısına ne de $_FILES['type'] değerine güvenebilirsin, ikisini de istemci belirler. Güvenli bir akış:

$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($_FILES['photo']['tmp_name']);

if (!isset($allowed[$mime]) || $_FILES['photo']['size'] > 5 * 1024 * 1024) {
    throw new RuntimeException('Geçersiz dosya');
}

$name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
move_uploaded_file($_FILES['photo']['tmp_name'], '/var/app/uploads/' . $name);

Üç kural: türü dosyanın içeriğinden belirle, dosya adını sen üret, dosyayı mümkünse web kök dizininin dışında sakla. Dışarıda tutamıyorsan, yükleme klasöründe PHP çalışmasını web sunucusunda kapat. Nginx’te bu kural, genel .php kuralından önce gelmeli, çünkü düzenli ifade eşleşmelerinde ilk uyan kazanır:

location ~* ^/uploads/.*\.php$ {
    deny all;
}

7. Kullanıcı girdisiyle dosya ya da nesne yükleme

İki eski ama hâlâ canlı klasik:

include $_GET['page'] . '.php';   // Yapma
unserialize($_COOKIE['cart']);    // Bunu da yapma

Birincisi, saldırganın sunucudaki başka dosyaları okumasına ya da çalıştırmasına yol açabilir. Çözüm yine izin listesi: gelen değeri bilinen sayfa adlarıyla eşleştir. İkincisi, kullanıcının kontrol ettiği veriden PHP nesnesi oluşturur ve uygun sınıflar varsa kod çalıştırmaya kadar gidebilir. Veri taşıyacaksan json_decode kullan. unserialize kullanmak zorundaysan ['allowed_classes' => false] seçeneğini ver.

8. Üretim ayarları, sürüm ve bağımlılıklar

Hata mesajları geliştirme sırasında dostundur, üretimde saldırganın haritası. Dosya yolları, sorgular, hatta veritabanı kullanıcı adı ekrana basılabilir:

display_errors = Off
log_errors = On
expose_php = Off

Ve en sıkıcı ama en etkili önlem: güncellik. Desteği biten bir PHP sürümü artık güvenlik yaması almaz. Hangi sürümlerin desteklendiğini php.net’teki “Supported Versions” sayfasından kontrol et. Composer kullanıyorsan bağımlılıklardaki bilinen açıkları tek komutla görebilirsin:

composer audit

Bir öğleden sonralık kontrol listesi

  • Metin birleştirerek kurulan SQL sorgusu kalmadı
  • Kullanıcıdan gelen her çıktı temizleniyor
  • Durum değiştiren her formda CSRF belirteci var
  • Parolalar password_hash ile saklanıyor
  • Oturum çerezleri secure, httponly, samesite ile işaretli
  • Yüklenen dosyaların türü içerikten kontrol ediliyor, klasörde PHP çalışmıyor
  • include ve unserialize kullanıcı girdisi görmüyor
  • Üretimde display_errors kapalı, PHP sürümü destekleniyor, composer audit temiz

Bu listenin tamamı işaretliyse, botların ve meraklı öğrencilerin büyük bölümü için ilginç bir hedef değilsin. Tamamen güvende olduğun anlamına gelmiyor tabii. Güvenlik bir kez yapılıp bitirilen bir iş değil, düzenli bakım.

PHP’nin çalıştığı sunucuyu da sağlamlaştırmak için yeni bir Linux sunucuda ilk 30 dakika yazısına bakabilirsin. Mevcut bir projenin güvenlik kontrolü için de teklif formundan yazabilirsin.

Kitapçıkta devamı

  1. 4 dk okuma

    PHP-FPM ayarı: pm.max_children nasıl hesaplanır?

    PHP-FPM'de pm.max_children değerini tahminle değil ölçerek belirlemek. Süreç belleğini ölçme, hesap formülü, pm modları, status sayfası ve slowlog.

  2. 4 dk okuma

    Yapay zeka ile kod yazmak: neyi devrediyorum, neyi asla devretmiyorum

    Yapay zeka kodlama asistanlarını her gün kullanan bir geliştiricinin notları. Hangi işler devredilir, hangileri devredilmez, tuzaklar ve güvenli bir çalışma düzeni.

Şans

Aklında bir proje mi var? Kutuyu açalım: ne yapmak istediğini anlat, sana kapsamı ve yol haritasını içeren bir teklif hazırlayayım.

Teklif iste

ya da doğrudan yaz: e-posta adresi