İçeriğe geç
Çılgın Yazılım

Kariyer ve Öğrenme kod örnekli

Adım Adım: CodeIgniter’da Dosya Yükleme Ucunu Güvenle Yazmak

Dosya yükleme, bir formun en tehlikeli alanıdır. Uzantıya güvenmeden, adı yeniden üreterek adım adım yazalım.

Çılgın Yazılım 3 dk okuma

Adım Adım: CodeIgniter’da Dosya Yükleme Ucunu Güvenle Yazmak
İçindekiler
  1. Adım 1: sınırları iki yerde tanımlayın
  2. Adım 2: gerçek içerik tipini doğrulayın
  3. Adım 3: dosya adını siz üretin
  4. Adım 4: web kökünün dışına yazın
  5. Adım 5: sunucu tarafında son savunma
  6. Sonuç

Dosya yükleme, bir web uygulamasında kullanıcıya verilen en geniş yetkidir: karşı taraf sunucunuza istediği baytları gönderir. Bu yüzden yükleme ucu, formun geri kalanından farklı bir dikkatle yazılmalıdır. Aşağıda beş kapıyı sırayla kuruyoruz.

Dosya yükleme güvenlik adımları
Beş kapının biri açık kalırsa, yükleme ucu bir kod çalıştırma açığına dönüşebilir.

Adım 1: sınırları iki yerde tanımlayın

PHP'nin kendi sınırları uygulama kodundan önce devreye girer. Yalnızca uygulama tarafında kontrol yaparsanız, büyük dosya PHP tarafından reddedilir ve kullanıcı anlamsız bir hata görür.

INI
; php.ini — sunucu tarafı üst sınır
upload_max_filesize = 8M
post_max_size = 10M          ; upload_max_filesize'dan BÜYÜK olmalı
max_file_uploads = 5
PHP
// Uygulama tarafı: kullanıcıya anlamlı hata verebilmek için
$kurallar = [
    'belge' => [
        'label' => 'Belge',
        'rules' => 'uploaded[belge]|max_size[belge,5120]|ext_in[belge,jpg,jpeg,png,webp,pdf]',
        'errors' => [
            'uploaded' => 'Bir dosya seçmelisiniz.',
            'max_size' => 'Dosya en fazla 5 MB olabilir.',
            'ext_in'   => 'Yalnızca JPG, PNG, WEBP ve PDF kabul edilir.',
        ],
    ],
];

if (! $this->validate($kurallar)) {
    return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
}

ext_in kuralı yalnızca dosya adındaki uzantıya bakar. Tek başına yeterli değildir: zararli.php dosyası resim.jpg adıyla gönderilebilir. Bir sonraki adım bu yüzden zorunludur.

Adım 2: gerçek içerik tipini doğrulayın

Tarayıcının gönderdiği MIME değeri istemci tarafından belirlenir, yani uydurulabilir. CodeIgniter'ın getMimeType() metodu ise dosyanın içeriğine bakar (finfo).

PHP
private const IZINLI = [
    'image/jpeg'      => 'jpg',
    'image/png'       => 'png',
    'image/webp'      => 'webp',
    'application/pdf' => 'pdf',
];

private function dogrula(?\CodeIgniter\HTTP\Files\UploadedFile $dosya): ?string
{
    if (! $dosya || ! $dosya->isValid() || $dosya->hasMoved()) {
        return null;
    }

    // İstemcinin bildirdiği tip DEĞİL, dosya içeriğinden okunan tip
    $mime = strtolower((string) $dosya->getMimeType());
    if (! isset(self::IZINLI[$mime])) {
        return null;
    }

    // Görsellerde ek kontrol: içerik gerçekten çözümlenebiliyor mu?
    if (str_starts_with($mime, 'image/') && @getimagesize($dosya->getTempName()) === false) {
        return null;
    }

    return self::IZINLI[$mime];   // uzantıyı BİZ belirliyoruz
}

SVG'yi bilerek listeye almadık: SVG bir XML belgesidir ve içinde script taşıyabilir. Kabul etmeniz gerekiyorsa ayrıca temizlemeniz ve farklı bir alan adından sunmanız gerekir.

Adım 3: dosya adını siz üretin

Kullanıcıdan gelen ad iki risk taşır: dizin geçişi (../../) ve çift uzantı (resim.php.jpg). Çözüm basittir — adı hiç kullanmayın.

PHP
$uzanti = $this->dogrula($this->request->getFile('belge'));
if ($uzanti === null) {
    return redirect()->back()->with('error', 'Dosya kabul edilmedi.');
}

$dosya = $this->request->getFile('belge');

// Ad tamamen bizim ürettiğimiz parçalardan kurulur: tarih + rastgele + doğrulanmış uzantı
$ad  = date('Ymd-His') . '-' . bin2hex(random_bytes(6)) . '.' . $uzanti;
$dizin = WRITEPATH . 'yuklemeler/' . date('Y/m');

if (! is_dir($dizin)) {
    mkdir($dizin, 0755, true);
}

$dosya->move($dizin, $ad);

Adım 4: web kökünün dışına yazın

Dosyayı public/ altına koyarsanız, sunucu yapılandırmasındaki tek bir hata onu çalıştırılabilir hâle getirebilir. writable/ altına yazıp erişimi kendi controller'ınızdan vermek bu riski tamamen kaldırır — ayrıca yetki kontrolü yapma imkânı da kazanırsınız.

PHP
public function indir(int $id)
{
    $kayit = $this->belgeModel->find($id);
    if (! $kayit) {
        throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
    }

    // Yetki kontrolü: dosya doğrudan erişilebilir olsaydı bu adım hiç çalışmazdı
    if (! $this->policy->erisebilir(session()->get('user_id'), $kayit)) {
        return $this->response->setStatusCode(403)->setBody('Yetkiniz yok.');
    }

    $yol = WRITEPATH . 'yuklemeler/' . $kayit['yol'];

    // realpath + prefix kontrolü: yol manipülasyonuna karşı son kapı
    $gercek = realpath($yol);
    if ($gercek === false || ! str_starts_with($gercek, realpath(WRITEPATH . 'yuklemeler'))) {
        throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
    }

    return $this->response->download($gercek, null)->setFileName($kayit['gorunen_ad']);
}

Büyük dosyaları uygulama sunucusundan geçirmek istemiyorsanız imzalı URL ile doğrudan yükleme yapabilirsiniz.

Adım 5: sunucu tarafında son savunma

Bash
# Yükleme dizininde PHP çalıştırmayı kapat (.htaccess)
<FilesMatch "\.(php|phar|phtml|cgi|pl)$">
    Require all denied
</FilesMatch>
php_flag engine off

Bu katman, yukarıdaki dört adımdan biri atlansa bile en kötü sonucu (uzaktan kod çalıştırma) engeller. Genel açık kategorileri için web uygulama güvenliği yazımız iyi bir çerçeve sunuyor; benzer bir adım adım anlatım için CRUD uygulaması nasıl yazılır ve kimlik doğrulamalı API rehberlerimize bakabilirsiniz. Framework tarafındaki resmî anlatım CodeIgniter dosya yükleme belgelerindedir.

Sonuç

Güvenli bir yükleme ucu, tek bir kontrolden değil beş kapının birlikte çalışmasından doğar: sınırları hem PHP hem uygulama tarafında tanımlayın, içerik tipini dosyanın kendisinden okuyun, dosya adını tamamen siz üretin, dosyaları web kökünün dışına yazıp erişimi controller üzerinden verin ve yükleme dizininde kod çalıştırmayı sunucu seviyesinde kapatın. Bu beş adım yarım saatlik bir iştir ve karşılığında bir uygulamanın en sık sömürülen giriş noktasını kapatır. Projenizdeki mevcut yükleme uçlarını bu listeye göre bir kez gözden geçirin.

Beğeniniz benzer içerikleri öne çıkarmamıza yardımcı olur.

Sık sorulan sorular

getMimeType() her zaman doğru sonuç verir mi?

Dosya içeriğine baktığı için istemci beyanından çok daha güvenilirdir, ancak kusursuz değildir; bazı biçimler benzer imzalar taşıyabilir. Bu yüzden görsellerde getimagesize gibi ikinci bir içerik kontrolü eklemek, tek başına MIME kontrolünden belirgin biçimde güvenlidir.

Dosyaları veritabanında saklamak daha mı güvenli?

Güvenlik açısından bazı riskleri azaltır ama veritabanı boyutunu hızla büyütür ve yedekleme sürelerini uzatır. Yaygın ve dengeli çözüm, dosyayı disk üzerinde web kökü dışında tutmak ve erişimi uygulama üzerinden vermektir.

Kullanıcının orijinal dosya adını hiç saklayamaz mıyım?

Saklayabilirsiniz — ama diskte değil, veritabanında ayrı bir sütunda. İndirme sırasında bu adı yalnızca görünen ad olarak kullanırsınız. Böylece kullanıcı dosyasını tanıdık adıyla indirir, disk üzerindeki ad ise tamamen kontrolünüzde kalır.

Büyük dosyalarda ne yapmalı?

Yüklemeyi doğrudan depolama servisine yaptırmak (imzalı yükleme adresi) hem sunucu kaynaklarını korur hem de zaman aşımı sorunlarını ortadan kaldırır. Bu durumda da tip ve boyut sınırının imzanın parçası olması gerekir; aksi hâlde kontrol tamamen kaybolur.

Yazan

Çılgın Yazılım

Ben Evren Çılgın; yazılım geliştirme, web teknolojileri ve sistem tasarımı alanlarında uzmanlaşmış bir geliştiriciyim. Uzun yıllardır hem frontend hem de backend tarafında üretken, ölçeklenebilir ve kullanıcı dostu çözümler üretiyorum.

Hakkımızda Bize ulaşın

Bu konuda bir projeniz mi var?

İhtiyacınızı birkaç cümleyle anlatın; uygun yaklaşımı birlikte belirleyelim.

Projenizi Anlatın

İlgili yazılar

Yorumlar

Henüz yorum yok. Sorunuzu ya da deneyiminizi ilk siz yazın.

Yorum yazın

Yorumunuz onaylandıktan sonra yayınlanır. Ekibimiz gerekirse konuyla ilgili bir yanıt da paylaşır.

Yayınlanmaz; yalnızca gerektiğinde size ulaşmak için.

En az 10 karakter.

Tüm yazılar