- Konu sahibi
- #1
İndirme işlemi için PHP’de dosyanın yolunu doğrulayıp uygun HTTP başlıklarını göndermeniz, ardından dosya içeriğini
Aşağıdaki örnekte, ziyaretçinin doğrudan dosya yolu göndermesine izin verilmez. Kullanıcı yalnızca uygulamanın izin verdiği bir dosya adıyla eşleştirilir. Bu yaklaşım,
Örnek klasör yapısı şöyledir:
Bu dosyayı çalıştırdığınızda
Koddan önce herhangi bir HTML, boşluk, hata çıktısı veya
Kullanıcıya aşağıdaki gibi bir bağlantı verebilirsiniz:
Bağlantıya tıklandığında PHP,
Aynı yöntemi bir düğmeyle de kullanabilirsiniz:
Dosya tarayıcıda açılmak yerine indirilmelidir. Tarayıcının dosyayı doğrudan göstermesi isteniyorsa
Bazen dosya adları veritabanında tutulur ve indirme adresine bir kimlik gönderilir. Bu durumda kullanıcıdan gelen değeri doğrudan dosya yoluna eklemek güvenli değildir:
Bu kullanım, uygun denetim yapılmadığında
Basit bir dosya adı doğrulaması gerekiyorsa
Örneğin dosya yolu veritabanından geldikten sonra şu kontroller uygulanabilir:
Burada
Dosya uzantısını kullanıcıdan gelen değere göre belirlemek yerine, izin verilen dosya kaydında sabit tutmak daha güvenlidir. Böylece kullanıcı
İşlemden sonra şu sonuçları birbirinden ayırın:
İndirme sırasında erişim kontrolü veya özel kayıt tutma gerekmiyorsa büyük ve herkese açık dosyaları doğrudan web sunucusunun sunması daha verimli olabilir. Dosya özel olacaksa PHP üzerinden yetkilendirme yapıp dosyayı sunucuya aktarmak veya erişimi süreli, imzalı bir bağlantıyla sınırlamak tercih edilebilir. Bu seçim, dosyanın gizliliğine ve sunucu mimarisine bağlıdır.
Dosya indirme adresi oturum gerektiriyorsa, indirme betiği dosya içeriğini göndermeden önce oturumu kontrol etmelidir:
Giriş yapmış olmak tek başına dosyaya erişim hakkı anlamına gelmez. Kullanıcının ilgili dosyaya sahip olup olmadığı veya dosya için gerekli role sahip bulunup bulunmadığı ayrıca kontrol edilmelidir.
readfile() ile yanıt olarak aktarmanız yeterlidir. İstemci dosyayı indirmeye başladığında PHP betiği başka çıktı üretmemelidir; aksi durumda dosya bozulabilir veya tarayıcı beklenen indirme yanıtını alamayabilir.PHP ile güvenli dosya indirme
Aşağıdaki örnekte, ziyaretçinin doğrudan dosya yolu göndermesine izin verilmez. Kullanıcı yalnızca uygulamanın izin verdiği bir dosya adıyla eşleştirilir. Bu yaklaşım,
../ gibi yollarla uygulama dışındaki dosyaların okunmasını önlemeye yardımcı olur.Örnek klasör yapısı şöyledir:
Kod:
/proje
/downloads
rapor.pdf
sozlesme.docx
indir.php
indir.php dosyasına şu kodu yerleştirebilirsiniz:
PHP:
<?php
$dosyalar = [
'rapor' => [
'yol' => __DIR__ . '/downloads/rapor.pdf',
'ad' => 'rapor.pdf',
'tür' => 'application/pdf',
],
'sozlesme' => [
'yol' => __DIR__ . '/downloads/sozlesme.docx',
'ad' => 'sozlesme.docx',
'tür' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
],
];
$kod = $_GET['dosya'] ?? '';
if (!isset($dosyalar[$kod])) {
http_response_code(404);
exit('Dosya bulunamadı.');
}
$dosya = $dosyalar[$kod];
if (!is_file($dosya['yol']) || !is_readable($dosya['yol'])) {
http_response_code(404);
exit('Dosya mevcut değil veya okunamıyor.');
}
$boyut = filesize($dosya['yol']);
header('Content-Description: File Transfer');
header('Content-Type: ' . $dosya['tür']);
header('Content-Disposition: attachment; filename="' . $dosya['ad'] . '"');
header('Content-Length: ' . $boyut);
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
readfile($dosya['yol']);
exit;
Bu dosyayı çalıştırdığınızda
indir.php?dosya=rapor adresine gelen istek, downloads/rapor.pdf dosyasını tarayıcıya gönderir. Content-Disposition: attachment başlığı tarayıcıya içeriği sayfada göstermeye çalışmak yerine indirme olarak ele almasını söyler. İndirilen dosyanın adı ise filename bölümünden gelir.Koddan önce herhangi bir HTML, boşluk, hata çıktısı veya
echo kullanılmamalıdır. PHP bir HTTP başlığı gönderdikten sonra gövdeye fazladan metin eklerse indirme yanıtı beklenmedik hâle gelebilir. Bu nedenle dosya indirme betiğini HTML şablonundan ayrı tutmak genellikle daha güvenlidir.İndirme bağlantısı nasıl oluşturulur?
Kullanıcıya aşağıdaki gibi bir bağlantı verebilirsiniz:
HTML:
<a href="indir.php?dosya=rapor">Raporu indir</a>
Bağlantıya tıklandığında PHP,
rapor anahtarını izin verilen dosya listesinde arar. Anahtar bulunursa dosyayı gönderir; bulunamazsa 404 yanıtı verir. Bu nedenle bağlantıda gerçek sunucu yolu olan /var/www/... gibi bir değer kullanmak yerine uygulamanın tanıdığı bir anahtar kullanılır.Aynı yöntemi bir düğmeyle de kullanabilirsiniz:
HTML:
<a href="indir.php?dosya=sozlesme">Sözleşmeyi indir</a>
Dosya tarayıcıda açılmak yerine indirilmelidir. Tarayıcının dosyayı doğrudan göstermesi isteniyorsa
Content-Disposition başlığındaki attachment yerine inline kullanılabilir; ancak bu davranış dosya türüne ve tarayıcının desteğine bağlıdır.Bir dosya yolu doğrudan URL’den alınacaksa
Bazen dosya adları veritabanında tutulur ve indirme adresine bir kimlik gönderilir. Bu durumda kullanıcıdan gelen değeri doğrudan dosya yoluna eklemek güvenli değildir:
PHP:
$dosya = $_GET['dosya'];
readfile(__DIR__ . '/downloads/' . $dosya);
Bu kullanım, uygun denetim yapılmadığında
../ içeren yolların denenmesine ve uygulamanın erişebildiği başka dosyaların okunmasına yol açabilir. Daha güvenli yöntem, önce bir kimlik almak, bu kimliği veritabanında aramak ve veritabanından gelen dosya yolunun izin verilen ana dizin içinde kaldığını doğrulamaktır.Basit bir dosya adı doğrulaması gerekiyorsa
basename() yalnızca dosya adını ayırmaya yardımcı olabilir; tek başına yetkilendirme sağlamaz. Kullanıcının hangi dosyayı indirmeye yetkili olduğu ayrıca kontrol edilmelidir.Örneğin dosya yolu veritabanından geldikten sonra şu kontroller uygulanabilir:
PHP:
$anaDizin = realpath(__DIR__ . '/downloads');
$dosyaYolu = realpath($veritabanindanGelenYol);
if ($anaDizin === false || $dosyaYolu === false) {
http_response_code(404);
exit('Dosya bulunamadı.');
}
$anaDizin = rtrim($anaDizin, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (strncmp($dosyaYolu, $anaDizin, strlen($anaDizin)) !== 0) {
http_response_code(403);
exit('Bu dosyaya erişilemez.');
}
if (!is_file($dosyaYolu) || !is_readable($dosyaYolu)) {
http_response_code(404);
exit('Dosya okunamıyor.');
}
Burada
realpath() sembolik yolları çözerek gerçek yolu verir. Gerçek yol izin verilen indirme diziniyle başlamıyorsa dosya gönderilmez. Bu kontrol, dosyanın yalnızca adını denetlemekten daha anlamlıdır; yine de erişim yetkisi, oturum ve dosyanın kime ait olduğu ayrıca uygulanmalıdır.Başlıkları dosya türüne göre ayarlama
Content-Type, gönderilen içeriğin türünü belirtir. Her dosyada application/octet-stream kullanılabilir; bu genel bir ikili veri türüdür ve tarayıcıya içeriği yorumlamaya çalışmaması gerektiğini bildirir. Ancak bilinen dosya türlerinde doğru MIME türünü vermek daha açıklayıcıdır.| Dosya türü | Örnek Content-Type | Tipik davranış |
|---|---|---|
| application/pdf | attachment ile indirme, inline ile tarayıcıda görüntüleme mümkün olabilir | |
| ZIP | application/zip | Genellikle indirme olarak ele alınır |
| PNG | image/png | Tarayıcı görüntüleyebilir; attachment kullanılırsa indirilir |
| Genel ikili dosya | application/octet-stream | Tarayıcı içeriği yorumlamadan indirmeye yönelir |
rapor.pdf.exe gibi farklı bir ad göndererek uygulamanın beklemediği bir dosyayı seçemez.İndirme sonucunu nasıl kontrol edersiniz?
İşlemden sonra şu sonuçları birbirinden ayırın:
- Dosya indirme penceresi açılıyor ve dosya doğru adla kaydediliyorsa PHP yanıtı temel olarak doğru çalışıyor demektir. Bu durum, dosyanın herkese açık olması gerektiğini kanıtlamaz; yetkisiz erişim kontrolü ayrıca yapılmalıdır.
- 404 yanıtı alıyorsanız gönderilen anahtarın izin verilen listede bulunup bulunmadığını ve fiziksel dosyanın beklenen klasörde gerçekten yer alıp almadığını kontrol edin.
- 403 yanıtı alıyorsanız uygulama dosyayı bilerek erişime kapatıyordur. Kullanıcının oturumunu, dosyaya erişim hakkını ve dosya yolunun izin verilen dizin içinde kalıp kalmadığını inceleyin.
- İndirilen dosya bozuksa PHP dosyasının başında veya sonunda görünmeyen boşluk, UTF-8 BOM, hata mesajı ya da
echoçıktısı olup olmadığını kontrol edin. Ayrıca gönderilenContent-Lengthdeğerinin dosyanın gerçek boyutuyla eşleştiğini doğrulayın. - Tarayıcı dosyayı açıyor, indirmiyorsa
Content-Disposition: attachmentbaşlığının gerçekten gönderildiğini ve sunucuda daha sonra farklı bir başlıkla değiştirilmediğini kontrol edin.
Büyük dosyalarda dikkat edilmesi gerekenler
readfile() çoğu normal dosya için yeterlidir. Ancak çok büyük dosyalarda PHP işlem süresi, bellek, web sunucusu ve istemci bağlantısı için tanımlanmış sınırlar devreye girebilir. Dosyayı önce belleğe almak yerine readfile() gibi akış tabanlı bir yöntem kullanmak daha uygundur; file_get_contents() ile tüm dosyayı bir değişkene almak gereksiz bellek tüketebilir.İndirme sırasında erişim kontrolü veya özel kayıt tutma gerekmiyorsa büyük ve herkese açık dosyaları doğrudan web sunucusunun sunması daha verimli olabilir. Dosya özel olacaksa PHP üzerinden yetkilendirme yapıp dosyayı sunucuya aktarmak veya erişimi süreli, imzalı bir bağlantıyla sınırlamak tercih edilebilir. Bu seçim, dosyanın gizliliğine ve sunucu mimarisine bağlıdır.
Dosya indirme adresi oturum gerektiriyorsa, indirme betiği dosya içeriğini göndermeden önce oturumu kontrol etmelidir:
PHP:
<?php
session_start();
if (empty($_SESSION['kullanici_id'])) {
http_response_code(401);
exit('İndirme için giriş yapmalısınız.');
}
// Bundan sonra dosya kaydı ve yetki kontrolü yapılır.
Giriş yapmış olmak tek başına dosyaya erişim hakkı anlamına gelmez. Kullanıcının ilgili dosyaya sahip olup olmadığı veya dosya için gerekli role sahip bulunup bulunmadığı ayrıca kontrol edilmelidir.