Bu makalede7 bölüm
- 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.
- Uzantı: Dosya adının son uzantısı küçük harfe çevrilip izin listesiyle karşılaştırılır.
fatura.pdf.phpgibi çift uzantılar ve sonda nokta veya boşluk gibi hileler bu adımda elenir. - İstemcinin bildirdiği MIME türü:
Content-Typebaşlığını istemci belirler ve kolayca değiştirir; yalnız ilk eleme için kullanılır. - İç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.
- 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 |
|---|---|---|
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:
- İstemci dosya adı, boyutu ve türüyle uygulamadan yükleme izni ister.
- 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.
- İstemci dosyayı bu URL’ye doğrudan yükler.
- 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,nosniffve 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.
- 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
Modern Web Uygulamaları