Php İle Güvenli Bir Dosya İndirme Sistemi Nasıl Yapılır?

Php İle Güvenli Bir Dosya İndirme Sistemi Nasıl Yapılır?
Php İle Güvenli Bir Dosya İndirme Sistemi Nasıl Yapılır?

Gereksinimler ve Ön Hazırlık

Güvenli bir dosya indirme sistemi kurmak için temel gereksinimler şunlardır:

  • PHP 8.2 veya üzeri bir sürüm (modern güvenlik özellikleri için).
  • Dosyaların web erişimine kapalı bir dizinde (document root dışında) saklanması.
  • Dosya isimlerinin veritabanında tutulması ve gerçek isimlerin kullanıcıdan gizlenmesi.
  • Sunucu üzerinde dosya okuma izinlerinin doğru yapılandırılması.

Dosyalarınızı asla public_html veya www klasörü içinde tutmayın. Bunun yerine, sunucu kök dizininin bir üstünde, örneğin /var/www/dosyalar/ gibi bir dizin oluşturun.

Dosya Yapısını ve Veritabanını Hazırlama

Güvenlik için dosya isimlerini sunucuda karmaşık (hash) değerlerle tutmalıyız. Kullanıcıya ise orijinal isimlerini göstermeliyiz. Aşağıdaki tablo, veritabanı tasarımınız için bir temel oluşturur.

Alan Adı Tür Açıklama
id INT Birincil anahtar.
dosya_adi VARCHAR Kullanıcıya gösterilecek isim.
dosya_yolu VARCHAR Sunucudaki gizli dosya ismi.
erisim_yetkisi TINYINT Dosya için gereken rol seviyesi.

Adım Adım Güvenli İndirme Betiği Oluşturma

İndirme işlemini gerçekleştirecek olan download.php dosyasının temel mantığı, kullanıcının isteğini doğrulamak ve ardından dosyayı akış (stream) yöntemiyle sunmaktır. Bu yöntem, büyük dosyaların belleği doldurmasını engeller.


// download.php başlangıcı
$fileId = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);

// Veritabanından dosya bilgilerini çekme (PDO kullanımı)
$stmt = $pdo->prepare("SELECT dosya_adi, dosya_yolu FROM dosyalar WHERE id = ?");
$stmt->execute([$fileId]);
$file = $stmt->fetch();

if (!$file) {
    die("Dosya bulunamadı veya yetkiniz yok.");
}

$filePath = '/var/www/ozel_depo/' . $file['dosya_yolu'];

if (file_exists($filePath)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="' . basename($file['dosya_adi']) . '"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($filePath));
    readfile($filePath);
    exit;
}

Bu kod bloğunda readfile() fonksiyonu, dosyayı parçalar halinde okuyarak tarayıcıya gönderir. Content-Disposition başlığı, tarayıcıya dosyanın bir web sayfası olarak değil, indirilebilir bir içerik olarak işlenmesi gerektiğini söyler.

Dosya Yolu Manipülasyonuna Karşı Önlemler

En yaygın güvenlik açıklarından biri "Directory Traversal" (dizin geçişi) saldırısıdır. Kullanıcının ../ gibi ifadelerle sunucu dosyalarına erişmesini engellemek için basename() fonksiyonunu kullanmalı ve dosya yollarını asla doğrudan kullanıcı girdisinden almamalısınız.


// Güvenli dosya yolu kontrolü
$guvenliDizin = realpath('/var/www/ozel_depo/');
$talepEdilenDosya = realpath($guvenliDizin . '/' . $dosyaAdi);

if ($talepEdilenDosya === false || strpos($talepEdilenDosya, $guvenliDizin) !== 0) {
    die("Yetkisiz erişim girişimi.");
}

Bu kontrol, dosyanın hedef dizinin dışında olup olmadığını denetler. realpath() fonksiyonu, dosya yolunun gerçek ve fiziksel konumunu döndürür, böylece saldırganın dizin dışına çıkma çabalarını boşa çıkarır.

Yetkilendirme Kontrollerini Entegre Etme

Sistemin güvenli olması için sadece dosyanın varlığı yeterli değildir; indiren kişinin bu dosyaya erişim hakkı olup olmadığını kontrol etmelisiniz. Oturum (session) yönetimi ile bunu kolayca yapabilirsiniz.


session_start();

// Kullanıcının giriş yapıp yapmadığını ve yetkisini kontrol et
if (!isset($_SESSION['user_id'])) {
    header("Location: login.php");
    exit;
}

// Veritabanından dosya yetkisini kontrol et
$stmt = $pdo->prepare("SELECT erisim_yetkisi FROM dosyalar WHERE id = ?");
$stmt->execute([$fileId]);
$dosya = $stmt->fetch();

if ($_SESSION['user_role'] < $dosya['erisim_yetkisi']) {
    die("Bu dosyayı indirmek için gerekli yetkiye sahip değilsiniz.");
}

Bu kod, kullanıcının oturum bilgilerini veritabanındaki dosya kısıtlamalarıyla karşılaştırır. $_SESSION değişkenlerini kullanarak kullanıcı bazlı bir indirme kısıtlaması uygulamak, sisteminizi kurumsal standartlara taşır.

Kritik Güvenlik Uyarısı: Üretim ortamında (production) hata mesajlarını asla doğrudan kullanıcıya göstermeyin. display_errors ayarını Off yapın ve hataları bir log dosyasına yazdırın. Aksi takdirde, dosya yollarınız veya veritabanı yapınız ifşa olabilir.

Dosya İndirme Hızını ve Sunucu Yükünü Yönetme

Büyük dosyalar için readfile() bazen bellek sınırlarına takılabilir. Daha büyük dosyalar için tamponlama (buffering) yöntemini kullanmak çok daha verimlidir.


$handle = fopen($filePath, 'rb');
while (!feof($handle)) {
    echo fread($handle, 8192); // 8KB'lık parçalar halinde oku
    flush(); // Çıktıyı tarayıcıya gönder
}
fclose($handle);

Bu yöntem, sunucunun RAM kullanımını minimumda tutar. flush() fonksiyonu, her parçanın anında istemciye gönderilmesini sağlar, bu da bağlantı kopmalarını azaltır.

Sıkça Sorulan Sorular

Dosya isimlerini neden veritabanında tutmalıyım?

Dosya isimlerini veritabanında tutmak, dosya adlarında boşluk veya özel karakter (Türkçe karakterler gibi) sorunu yaşamanızı engeller. Ayrıca gerçek dosya ismini gizleyerek, sunucu yapınızın ifşa olmasını önlemiş olursunuz.

İndirme linklerini nasıl gizleyebilirim?

İndirme linklerini doğrudan dosya yolu olarak değil, download.php?id=123 gibi bir parametre ile vererek gizleyebilirsiniz. Bu, dosyanın gerçek fiziksel yolunun asla tarayıcıda görünmemesini sağlar.

Dosya indirme sisteminde SQL Injection'dan nasıl korunurum?

Daima PDO (PHP Data Objects) kullanarak hazırlanmış ifadeler (prepared statements) kullanın. Asla kullanıcıdan gelen veriyi doğrudan SQL sorgusuna dahil etmeyin.

Büyük dosyalar için indirme kesilirse ne yapmalıyım?

HTTP 206 (Partial Content) başlıklarını destekleyen bir yapı kurmanız gerekir. Ancak bu ileri düzey bir konudur; başlangıç için yukarıdaki tamponlama yöntemi yeterli olacaktır.

Dosya türlerini nasıl kısıtlarım?

Veritabanında dosya türünü (MIME type) saklayın ve dosyayı göndermeden önce finfo_file() fonksiyonu ile dosyanın gerçek içeriğini doğrulayın.

İleri Düzey İndirme İstatistikleri ve Loglama Sistemi

Güvenli bir dosya indirme sistemi kurduktan sonra, bu dosyaların kim tarafından, ne zaman ve hangi sıklıkla indirildiğini takip etmek, hem sistemin yönetimi hem de kullanıcı davranış analizi için kritiktir. Basit bir downloads_log tablosu ile bu süreci şeffaf hale getirebilirsiniz.

Veritabanı Log Yapısı

İndirme işlemlerini kayıt altına almak için aşağıdaki SQL yapısını kullanabilirsiniz:

CREATE TABLE download_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    file_id INT NOT NULL,
    user_id INT NOT NULL,
    download_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    ip_address VARCHAR(45),
    user_agent TEXT
);

İndirme İşlemini Loglama Fonksiyonu

İndirme betiğinizin içine, dosya gönderilmeden hemen önce aşağıdaki fonksiyonu tetikleyerek veritabanına kayıt ekleyebilirsiniz:

function logDownload($pdo, $fileId, $userId) {
    $stmt = $pdo->prepare("INSERT INTO download_logs (file_id, user_id, ip_address, user_agent) VALUES (?, ?, ?, ?)");
    $stmt->execute([
        $fileId, 
        $userId, 
        $_SERVER['REMOTE_ADDR'], 
        $_SERVER['HTTP_USER_AGENT']
    ]);
}

Performans Optimizasyonu: X-Sendfile Kullanımı

PHP aracılığıyla büyük dosyaları okumak (readfile()), sunucu üzerinde ciddi bir bellek yükü oluşturabilir. Apache veya Nginx sunucularında bulunan X-Sendfile modülü, dosya sunma işlemini PHP'den alıp doğrudan web sunucusuna devreder. Bu, PHP'nin dosyayı belleğe almadan işlemi bitirmesini sağlar.

X-Sendfile ile Dosya Sunma

Eğer sunucunuzda X-Sendfile modülü aktifse, dosya indirme betiğinizi şu şekilde optimize edebilirsiniz:

// Dosya yolunun mutlak olduğunu varsayalım
$filePath = '/var/www/uploads/gizli_dosya.pdf';

// İlgili header'ı gönderin
header("X-Sendfile: $filePath");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename='indirilen_dosya.pdf'");
exit;
Not: X-Sendfile kullanırken, web sunucunuzun (Apache/Nginx) dosya dizinine erişim yetkisi olduğundan emin olun. Aksi takdirde, PHP üzerinden yetkilendirme yapsanız bile web sunucusu dosyayı okuyamayacağı için 403 veya 404 hataları alabilirsiniz.

Sistem Testleri ve Hata Ayıklama

Sisteminizi yayına almadan önce mutlaka şu senaryoları test edin:

  • Yetkisiz Erişim: Oturum açmamış bir kullanıcı, doğrudan dosya indirme URL'sine gittiğinde 403 hatası alıyor mu?
  • Dosya Mevcudiyeti: Veritabanında kaydı olan ancak sunucuda fiziksel olarak silinmiş bir dosya istendiğinde sistem "Dosya bulunamadı" mesajını düzgün veriyor mu?
  • MIME Türü Kontrolü: Farklı dosya türleri (PDF, ZIP, PNG) için Content-Type başlıkları doğru ayarlanıyor mu?

Hata ayıklama sürecinde, PHP'nin error_log() fonksiyonunu kullanarak indirme sırasında oluşan hataları (dosya okuma izinleri, veritabanı bağlantı kopmaları vb.) takip etmeniz, sistemin kararlılığını artıracaktır.

Sonuç

PHP ile güvenli bir dosya indirme sistemi oluşturmak, sunucu taraflı kontrollerin titizlikle uygulanmasını gerektirir. Kullanıcının doğrudan dosya yollarına erişimini engellemek, oturum kontrolleri yapmak ve dosyaları akış yöntemiyle sunmak, sisteminizi hem güvenli hem de performanslı kılar. Bir sonraki adım olarak, indirme istatistiklerini tutmak için veritabanına bir "indirme logu" tablosu ekleyebilir ve hangi kullanıcının hangi dosyayı ne zaman indirdiğini takip edebilirsiniz.

Sorumluluk Reddi: Bu makalede paylaşılan kod örnekleri eğitim amaçlıdır. Uygulamanızın canlı ortamda güvenliğini sağlamak için her zaman güncel güvenlik yamalarını takip edin ve profesyonel sızma testleri gerçekleştirin. Yazılım güvenliği sürekli bir süreçtir.

Bu yazıya tepkinizi paylaşın:
Selin Yılmaz

Sürdürülebilir yaşam ve pratik ev yönetimi üzerine içerik stratejileri geliştiriyorum. Yalın anlatımı ve uygulanabilirliği ön planda tutan bir yazı dilim var.

Yorumlar (0)

Yorum Yaz