ERETEK
BİLGİ SİSTEMLERİ

Web Uygulamasında Güvenli Dosya Yükleme: Doğrulama, Depolama, Tarama

Güvenli dosya yükleme: izin listesi ve magic byte ile tür doğrulama, web kökü dışında depolama, imzalı URL, ClamAV ile karantina ve güvenli sunum başlıkları.

Bu makalede7 bölüm
Teknik özet
Teknoloji
OWASP, Nginx, Node.js, AWS S3 presigned POST, ClamAV, HTTP güvenlik başlıkları
Konu
Web uygulamasında dosya yükleme özelliğini güvenli geliştirmek
Seviye
Orta–ileri
Ön koşullar
  • HTTP istek/yanıt yapısı ve başlıklar bilgisi
  • Sunucu tarafı web geliştirme deneyimi

Bir web uygulamasında dosya yükleme, kullanıcının sunucuya en geniş kontrolle veri bıraktığı noktadır: içerik, ad, boyut ve tür kullanıcıdan gelir. Fatura eki, CV, teknik çizim veya profil fotoğrafı için açılmış masum bir alan, yanlış kurulduğunda uzaktan kod çalıştırmaya, diskin dolmasına veya diğer kullanıcılara zararlı içerik sunulmasına kapı açar. Web kökündeki uploads/ dizininde çalışan bir .php dosyası, olay incelemelerinde hâlâ en sık bulunan izlerden biridir. Aşağıda riskleri ve katmanlı önlemleri, tür doğrulamadan depolamaya, imzalı URL’den taramaya ve güvenli sunuma kadar yapılandırma örnekleriyle anlatıyoruz.

Dosya yüklemenin risk yüzeyi

Riskleri sınıflandırmak hangi önlemin hangi tehdide karşı olduğunu netleştirir ve “uzantı kontrolü yaptık, yeter” yanılgısını kırar:

Risk Nasıl oluşur? Temel önlem
Sunucuda kod çalıştırma .php, .jsp gibi dosya web kökünde çalıştırılabilir dizine düşer Web kökü dışında saklama, yürütmeyi kapatma
Path traversal / üzerine yazma Dosya adı ../../ içerir veya mevcut dosyayla çakışır Sunucu tarafında üretilen ad
İçerik türü sahteciliği Uzantı ve Content-Type başlığı istemcide değiştirilir İçerikten (magic byte) tür tespiti
Kullanıcıya zararlı içerik HTML/SVG içindeki betik aynı kökenden sunulur (stored XSS) Ayrı alan adı, attachment, nosniff
Kaynak tüketimi (DoS) Çok büyük dosya, çok sayıda yükleme, sıkıştırma bombası Boyut, sayı ve hız sınırları
Zararlı yazılım dağıtımı Yüklenen dosya başka kullanıcılar tarafından indirilir Tarama ve karantina

Bu önlemlerin hiçbiri tek başına yetmez. Biri atlatıldığında arkadaki yakalasın diye üst üste konur. Konunun kapsamlı referansı OWASP File Upload Cheat Sheet belgesidir.

Dosya türü doğrulama katmanları

Tür doğrulamasında izin listesi (allowlist) kullanılır: iş ihtiyacına göre kabul edilecek türler açıkça sayılır. Yasaklı uzantı listesi (blocklist) her zaman eksik kalır; .phtml, .php5, .phar gibi varyantlar ya da sunucu yapılandırmasına bağlı başka uzantılar gözden kaçar. Kara liste bir gün mutlaka delinir.

  1. Uzantı: Dosya adının son uzantısı küçük harfe çevrilip izin listesiyle karşılaştırılır. fatura.pdf.php gibi çift uzantılar ve sonda nokta veya boşluk gibi hileler bu adımda elenir.
  2. İstemcinin bildirdiği MIME türü: Content-Type başlığını istemci belirler ve kolayca değiştirir; yalnız ilk eleme için kullanılır.
  3. İçerik imzası (magic bytes): Dosyanın ilk baytları gerçek biçimi gösterir. Uzantı, bildirilen tür ve içerik imzası tutarlı değilse dosya reddedilir.
  4. Yapısal doğrulama veya yeniden işleme: Görseller bir görüntü kütüphanesiyle açılıp yeniden kodlanırsa (ör. JPEG olarak yeniden kaydetme) içine gizlenmiş ek veri ve meta veri (EXIF’teki konum bilgisi dahil) temizlenir.

Yaygın biçimlerin imzaları:

Biçim İlk baytlar (hex) Not
PDF 25 50 44 46 2D (%PDF-) PDF JavaScript ve gömülü dosya içerebilir
PNG 89 50 4E 47 0D 0A 1A 0A Yeniden kodlama önerilir
JPEG FF D8 FF EXIF meta verisi temizlenmeli
ZIP / DOCX / XLSX 50 4B 03 04 Office belgeleri ZIP kapsayıcısıdır; makro içeren türler ayrıca değerlendirilmeli
// Node.js: uzantı + içerik imzası tutarlılığı (izin listesi)
import { extname } from 'node:path';

const IZINLI = {
  '.pdf':  [[0x25, 0x50, 0x44, 0x46, 0x2d]],
  '.png':  [[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]],
  '.jpg':  [[0xff, 0xd8, 0xff]],
  '.jpeg': [[0xff, 0xd8, 0xff]],
} as const;

export function turDogrula(dosyaAdi: string, bas: Uint8Array): boolean {
  const uzanti = extname(dosyaAdi).toLowerCase() as keyof typeof IZINLI;
  const imzalar = IZINLI[uzanti];
  if (!imzalar) return false;                       // izin listesinde yok
  return imzalar.some((imza) => imza.every((b, i) => bas[i] === b));
}

SVG ayrı dikkat ister: XML tabanlıdır ve <script> ile olay öznitelikleri taşıyabilir. İş ihtiyacı yoksa SVG kabul etmeyin; gerekiyorsa sunucu tarafında bir temizleme kütüphanesinden geçirin veya PNG gibi raster bir biçime dönüştürün. HTML dosyaları da kullanıcı içeriği olarak kabul edilmez.

Nginx önünde çalışan bir uygulamada istek gövdesi sınırı client_max_body_size ile belirlenir; aşılınca Nginx 413 döndürür ve istek uygulamaya hiç ulaşmaz. Yükleme ucuna geniş, diğer uçlara dar bir sınır verin. Kullanıcı “dosya yüklenmiyor” diye aradığında konsolda ilk bakılacak yer de burasıdır: Nginx error log’unda “client intended to send too large body” satırı. Ters vekil yapılandırmasının diğer ayrıntıları Nginx reverse proxy yapılandırması rehberinde.

server {
    client_max_body_size 1m;                 # genel varsayılan

    location /api/belgeler/yukle {
        client_max_body_size 20m;            # yalnız yükleme ucu
        proxy_pass http://app_backend;
    }
}

Depolama yeri ve adlandırma

Yüklenen dosya, web sunucusunun doğrudan dosya olarak sunduğu dizine yazılmaz. Güvenli seçenekler web kökü dışındaki bir dizin veya nesne depolamadır (S3 uyumlu depolama, Azure Blob Storage); dosyalar uygulama üzerinden, yetki kontrolüyle sunulur. Eski bir uygulamada dosyalar web kökünde kalmak zorundaysa o dizinde betik çalıştırmayı kapatın:

# Nginx: yükleme dizininde PHP çalıştırmayı engelle
location ^~ /uploads/ {
    location ~* \.(php|phtml|phar)$ { return 403; }
    add_header X-Content-Type-Options nosniff always;
}

Dosya adını kullanıcıdan almayın. Sunucu rastgele bir kimlik (UUID) üretir ve dosyayı bu kimlikle saklar; özgün ad yalnız veritabanında meta veri olarak tutulur, indirme sırasında temizlenerek Content-Disposition başlığında kullanılır. Path traversal, ad çakışması ve dosya adı üzerinden bilgi sızması aynı anda kapanır. Veritabanı kaydında dosyanın sahibi, bağlı olduğu iş kaydı, boyutu, tespit edilen türü, özet değeri (SHA-256) ve tarama durumu bulunmalıdır.

Özel iş uygulamalarında erişim kontrolü dosya düzeyinde değil, bağlı kayıt düzeyinde yapılır: kullanıcının bir satın alma talebine erişimi varsa o talebin eklerine de erişir. Tahmin edilebilir artan kimlikler (/dosya/1043) kullanmayın; kullanmak zorundaysanız her istekte yetkiyi kontrol edin. Aksi halde IDOR (güvensiz doğrudan nesne referansı) açığı oluşur ve adres çubuğunda sayıyı bir artıran herkes başkasının faturasını indirir.

İmzalı URL ile doğrudan yükleme

Büyük dosyaların uygulama sunucusundan geçmesi bellek, bant genişliği ve zaman aşımı sorunu yaratır. Nesne depolama kullanılıyorsa daha iyi desen, istemcinin dosyayı doğrudan depolamaya yüklemesidir. Akış şöyle işler:

  1. İstemci dosya adı, boyutu ve türüyle uygulamadan yükleme izni ister.
  2. Uygulama kullanıcının yetkisini ve bildirilen meta veriyi kontrol eder, rastgele bir nesne anahtarı üretir, kaydı “bekliyor” durumunda oluşturur ve kısa ömürlü bir imzalı URL döndürür.
  3. İstemci dosyayı bu URL’ye doğrudan yükler.
  4. Yükleme bitince (istemci bildirimi veya depolama olay bildirimiyle) sunucu tarafı işleyici nesneyi doğrular, tarar ve durumu günceller.
// AWS SDK v3: boyut sınırı uygulanabilen imzalı POST
import { S3Client } from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
import { randomUUID } from 'node:crypto';

const s3 = new S3Client({ region: 'eu-central-1' });

export async function yuklemeIzni(kullaniciId: string) {
  const key = `karantina/${kullaniciId}/${randomUUID()}`;
  return createPresignedPost(s3, {
    Bucket: 'kurum-belgeler',
    Key: key,
    Conditions: [
      ['content-length-range', 1, 20 * 1024 * 1024],   // 1 bayt – 20 MB
      ['eq', '$Content-Type', 'application/pdf'],
    ],
    Fields: { 'Content-Type': 'application/pdf' },
    Expires: 300,                                     // saniye
  });
}

İmzalı PUT URL’si (getSignedUrl ile PutObjectCommand) daha basittir, ancak S3’te izin verilen boyut aralığını bir politika koşulu olarak tanımlayamaz. Boyut sınırı önemliyse imzalı POST ve content-length-range koşulunu seçin. Her iki durumda da istemcinin bildirdiği tür ve boyut yalnız ön kontroldür; dosya karantina önekine düşer, sunucu tarafı doğrulama ve taramadan geçmeden kimseye sunulmaz. Kova herkese açık olmaz; okuma da kısa ömürlü imzalı URL’lerle yapılır. Sahada en sık gördüğümüz yapılandırma hatası, test sırasında “geçici” açılan public erişimin kovada unutulmasıdır.

Zararlı yazılım taraması ve karantina

Başka kullanıcıların indireceği veya iç sistemlere aktarılacak dosyalar zararlı yazılım taramasından geçer. Açık kaynak seçenek ClamAV’dir; clamd servisi çalışır halde tutulur ve dosyalar clamdscan veya servis soketi üzerinden taranır. Bulut depolamada sağlayıcıların kendi tarama hizmetleri de değerlendirilebilir.

# ClamAV imza güncelleme ve tek dosya tarama
sudo freshclam
clamdscan --fdpass --no-summary /var/app/karantina/3f2a9c1e-...
# Çıkış kodu: 0 = temiz, 1 = zararlı bulundu, 2 = hata

Karantina akışını bir durum makinesi olarak tasarlayın: bekliyor → taraniyor → temiz | zararli | hata. Yalnız temiz durumundaki dosyalar indirilebilir veya iş akışına aktarılır. Tarama hatası “temiz” sayılmaz. Yeniden denenir, kalıcı hata durumunda operasyon ekibine bildirilir. İmza güncellemesi (freshclam) durmuşsa tarama çalışıyor görünür ama eski imzalarla tarar; güncelleme zamanını izlemeye alın. Tarama imza tabanlı olduğu için yeni zararlıları kaçırabilir, bu yüzden tür kısıtlaması, yeniden işleme ve güvenli sunumun yerini tutmaz. Parola korumalı arşivler taranamaz; iş ihtiyacı yoksa reddedin, arşiv kabul ediyorsanız açılmış toplam boyutu ve dosya sayısını sınırlayarak sıkıştırma bombalarına karşı önlem alın.

Güvenli sunum başlıkları ve kontrol listesi

Dosya güvenle kabul edilip saklansa bile yanlış başlıklarla sunulursa tarayıcı onu çalıştırabilir. Kullanıcı içeriği için önerilen başlıklar:

Content-Type: application/pdf                   # tespit edilen gerçek tür
Content-Disposition: attachment; filename="fatura-ornek.pdf"; filename*=UTF-8''fatura-%C3%B6rnek.pdf
X-Content-Type-Options: nosniff
Content-Security-Policy: sandbox; default-src 'none'
Cache-Control: private, no-store

attachment dosyanın tarayıcıda açılmak yerine indirilmesini sağlar; satır içi gösterim gerekiyorsa (görsel önizleme) yalnız güvenli olduğu bilinen türlerde inline kullanın. nosniff tarayıcının içerik koklamayla türü değiştirmesini engeller. filename* parametresi, RFC 6266’nın RFC 8187 kodlamasına dayanarak tanımladığı ve ASCII dışı karakterleri taşıyan biçimdir; eski istemciler için düz filename da birlikte gönderilir. Kullanıcı içeriğini ana uygulamadan ayrı bir alan adından sunmak, olası bir betik çalıştırmanın uygulama oturum çerezlerine erişmesini engeller. Kişisel veri içeren dosyalarda paylaşılan önbelleklerin yanıtı saklamaması için önbellek başlıklarını bilinçli ayarlayın; ayrıntılar Cache-Control ve CDN başlıkları rehberinde.

Kontrol listesi

  • Kabul edilen türler iş ihtiyacına göre izin listesiyle tanımlı; SVG, HTML ve çalıştırılabilir türler kapsam dışı.
  • Uzantı, bildirilen MIME ve içerik imzası sunucuda birlikte doğrulanıyor; görseller yeniden kodlanıyor.
  • Boyut sınırı ters vekilde, uygulamada ve (varsa) imzalı yükleme koşulunda tanımlı; kullanıcı başına hız ve sayı sınırı var.
  • Dosya adları sunucuda üretiliyor; özgün ad yalnız meta veri.
  • Dosyalar web kökü dışında veya özel nesne depolamada; yükleme dizininde betik çalıştırma kapalı.
  • Her indirme isteğinde bağlı kayda yetki kontrol ediliyor.
  • Karantina ve tarama akışı var; yalnız temiz dosyalar sunuluyor, imza güncelliği izleniyor.
  • Sunumda attachment, nosniff ve kısıtlayıcı CSP kullanılıyor; tercihen ayrı alan adı.
  • Yükleme, reddetme ve tarama sonuçları loglanıyor.

Yükleme alanını formun geri kalanıyla aynı doğrulama sözleşmesine bağlamak için form doğrulamasını tek şemayla yönetmek rehberi tamamlayıcıdır. Mevcut bir uygulamanın yükleme özelliklerini gözden geçirmek veya WAF katmanında ek kurallar tanımlamak için web uygulama güvenliği ve WAF kapsamında inceleme yapıyoruz.

Sık sorulan sorular

Dosya uzantısını kontrol etmek yeterli mi?

Hayır. Uzantı ve istemcinin bildirdiği içerik türü kolayca değiştirilir. Uzantı izin listesi, içerik imzası kontrolü, sunucuda üretilen dosya adı, çalıştırılamayan depolama yeri ve güvenli sunum başlıkları birlikte uygulanmalıdır.

Yüklenen dosyalar veritabanında mı saklanmalı?

Genellikle hayır. Dosyanın kendisi web kökü dışındaki bir dizinde veya özel nesne depolamada, meta verisi veritabanında tutulur. Bu ayrım veritabanı boyutunu ve yedekleme süresini kontrol altında tutar.

İmzalı URL ile yüklemede dosya boyutu nasıl sınırlanır?

S3 uyumlu depolamada imzalı POST politikasına content-length-range koşulu eklenerek azami boyut depolama tarafında zorlanabilir. İmzalı PUT URL’sinde böyle bir aralık koşulu olmadığından boyut kontrolü büyük ölçüde yükleme sonrası doğrulamaya kalır.

Antivirüs taraması diğer önlemlerin yerini tutar mı?

Hayır. İmza tabanlı tarama bilinen zararlıları yakalar, yeni veya özel hazırlanmış dosyaları kaçırabilir. Tarama; tür kısıtlaması, yeniden işleme, yetki kontrolü ve güvenli sunum başlıklarıyla birlikte bir katman olarak kullanılır.

Teknik özet
Teknoloji
OWASP, Nginx, Node.js, AWS S3 presigned POST, ClamAV, HTTP güvenlik başlıkları
Konu
Web uygulamasında dosya yükleme özelliğini güvenli geliştirmek
Seviye
Orta–ileri
Ön koşullar
  • HTTP istek/yanıt yapısı ve başlıklar bilgisi
  • Sunucu tarafı web geliştirme deneyimi
Eretek uzman desteği

Bu konu sizin ortamınızda nasıl çalışır, birlikte bakalım.

Mevcut yapınızı inceliyor, darboğazı tespit ediyor ve uygulanabilir bir iş planı öneriyoruz.