Güvenli Dosya Yükleme için Geliştirici Kontrol Listesi
Güvenli dosya yükleme yalnızca extension veya MIME kontrolü değildir. Request limitinden quarantine alanına, parser isolation'dan malware scanning ve güvenli download'a kadar bütün file lifecycle korunmalıdır.
SECNODEX Güvenlik Ekibi
Ofansif Güvenlik
Bir kullanıcı profil fotoğrafı yüklüyor.
Backend yalnızca üç kontrol yapıyor:
- 1Filename
.jpgile bitiyor mu? - 2Request
Content-Typedeğeriimage/jpegmi? - 3File size 5 MB altında mı?
Dosya bu kontrolleri geçince /uploads dizinine, kullanıcının gönderdiği filename ile kaydediliyor. Aynı dizin web server tarafından doğrudan yayınlanıyor. Application ekibi extension allowlist ve size limit bulunduğu için özelliği güvenli kabul ediyor.
Oysa bu tasarımda cevaplanmamış birçok soru var:
- Request içindeki MIME değeri gerçekten kim tarafından üretildi?
- JPEG signature ile başlayan fakat geçerli bir image olmayan içerik ne olacak?
- File parser malformed content üzerinde nasıl davranıyor?
- Aynı filename mevcut bir object'i overwrite edebilir mi?
../../veya platforma özgü path karakterleri storage key'e ulaşabiliyor mu?- Upload edilen dosya onaylanmadan public URL üzerinden erişilebiliyor mu?
- Malware scanner timeout verirse file kabul mü ediliyor?
- Image dimensions çok büyükse decoder ne kadar memory kullanacak?
- Dosya daha sonra PDF preview, OCR veya thumbnail worker'a gönderildiğinde hangi privilege ile işlenecek?
- Download endpoint'i object ownership ve tenant boundary'yi doğruluyor mu?
Bu soruların hiçbiri yalnızca .jpg kontrolüyle cevaplanamaz.
Kısa cevap
Güvenli dosya yükleme, file'ın request'e girdiği andan silindiği ana kadar kontrollü bir lifecycle kurmaktır. Authentication ve authorization, request ve file size limitleri, business allowlist, filename isolation, content detection, structural parsing, quarantine, malware scanning, CDR, immutable storage, parser isolation ve güvenli download birlikte uygulanmalıdır.
Extension, MIME ve file signature kontrolü gereklidir. Fakat bu sinyallerden hiçbiri tek başına güven kararı değildir. Geçerli bir PDF malware içerebilir. Geçerli bir image parser açığını tetikleyebilir. Geçerli bir ZIP, extraction sonrasında storage'ı tüketebilir. Tamamen zararsız bir HTML dosyası aynı origin'de yayınlandığında stored XSS veya phishing yüzeyi oluşturabilir.
Bu nedenle geliştiricinin sorması gereken soru yalnızca “Dosya gerçekten JPEG mi?” değildir:
Not
Bu file hangi kimlikle yüklendi, hangi byte dizisi kabul edildi, nerede tutuldu, hangi component tarafından hangi privilege ile işlendi, ne zaman güvenilir sayıldı ve kime hangi response header'larıyla sunuldu?
Bu rehberde güvenli dosya yükleme sürecini upload endpoint'inden ibaret görmeyeceğiz. Production ortamında uygulanabilir bir architecture, Java Spring örnekleri, archive extraction limitleri, scanner state machine'i, direct-to-object-storage modeli, güvenli download ve kapsamlı test matrisi oluşturacağız.
File upload neden tek bir validation problemi değildir?
File upload surface'i dört ana aşamadan oluşur:
Receive
→ Store
→ Process
→ ServeHer aşamanın farklı security boundary'si vardır.
| Aşama | Temel risk |
|---|---|
| Receive | Yetkisiz upload, multipart abuse, oversized request, type spoofing |
| Store | Path traversal, overwrite, public exposure, executable permission, quota exhaustion |
| Process | Parser exploit, malware, command injection, XXE, decompression bomb, resource exhaustion |
| Serve | Broken Object Level Authorization, stored XSS, MIME confusion, header injection, cache leakage |
Bir sistem receive aşamasında güçlü validation yapıp serve aşamasında private file'ı public bucket üzerinden açabilir. Malware scanning uygulayıp scan sonucu ile farklı object version'ını eşleştirebilir. Secure filename üretip archive extraction sırasında entry name üzerinden path traversal'a düşebilir.
Bu nedenle file upload güvenliği bir filter listesi değil, uçtan uca security contract'tir.
Önce upload use case'ini daraltın
“Kullanıcı dosya yükleyebilsin” güvenlik requirement'ı değildir.
Önce iş ihtiyacı şu sorularla tanımlanmalıdır:
- Hangi kullanıcılar upload yapabilir?
- File hangi business object'e bağlanacak?
- Hangi type'lara gerçekten ihtiyaç var?
- Maksimum ve minimum file size nedir?
- Tek request içinde kaç file kabul edilecek?
- Kullanıcının günlük ve toplam storage quota'sı nedir?
- Dosya private mı public mi?
- Inline preview gerekli mi?
- Original file saklanacak mı?
- File üzerinde thumbnail, OCR, transcoding, indexing veya extraction yapılacak mı?
- File başka kullanıcılara veya kurum dışına gönderilecek mi?
- Retention süresi nedir?
- Legal hold veya silme yükümlülüğü var mı?
- Malware bulunduğunda ne yapılacak?
- Scanner çalışmadığında sistemin default kararı nedir?
İş ihtiyacı yalnızca profile image ise PDF, SVG, ZIP, DOCX ve generic binary kabul edilmemelidir. “İleride lazım olabilir” gerekçesiyle geniş type listesi oluşturmak, gereksiz parser ve rendering surface'i açar.
Allowlist type bazında değil, use case bazında kurulmalı
Aynı image/png type'ı iki farklı özellikte farklı policy gerektirebilir:
- Profile image için pixel dimensions, metadata removal ve re-encoding gerekir
- Medical image workflow'u için metadata'nın korunması zorunlu olabilir
- Product catalog için transparent PNG kabul edilebilir
- Identity verification için original evidence'in immutable saklanması gerekebilir
Tek bir global “safe file types” listesi her business flow için doğru değildir.
Güvenli upload architecture nasıl görünür?
Güçlü bir tasarımda file, kabul edildiği anda public veya trusted hale gelmez.
Client
↓
Edge request limits
↓
Authenticated upload endpoint
↓
Preliminary policy checks
↓
Private quarantine storage
↓
Type detection ve structural validation
↓
Malware scan ve gerekiyorsa CDR
↓
Approved immutable object
↓
Authorized download veya isolated previewFile state'i açıkça modellenebilir:
RECEIVING
→ QUARANTINED
→ SCANNING
→ APPROVED
→ AVAILABLE
SCANNING
→ REJECTED
SCANNING
→ SCAN_FAILED
AVAILABLE
→ EXPIRED
→ DELETEDBu modelin en önemli kuralı şudur:
Not
QUARANTINED, SCANNING veya SCAN_FAILED durumundaki bir object normal download ve preview flow'undan sunulmamalıdır.
Scanner timeout verdiğinde file'ı sessizce APPROVED yapmak fail-open davranıştır. Scanner outage sırasında bütün upload'ları sonsuza kadar request thread'inde bekletmek de availability problemi oluşturur. Doğru çözüm state, retry, timeout, queue kapasitesi ve kullanıcı deneyimini birlikte tasarlamaktır.
Threat model: Upload edilen file ne yapabilir?
Upload edilen içerik yalnızca “server'da çalışacak script” olarak düşünülmemelidir.
Server-side code execution
File webroot, plugin dizini, template dizini veya application server'ın executable kabul ettiği bir konuma yazılabilir. Server configuration extension, handler veya interpreter eşleşmesi nedeniyle içeriği çalıştırabilir.
Client-side active content
HTML, SVG, XML, PDF ve bazı document formatları script, external reference, form, link veya active content taşıyabilir. Same-origin yayınlanan içerik stored XSS, phishing veya session-bound attack surface oluşturabilir.
Parser exploitation
Image, PDF, media, archive, OCR ve document parser'ları native veya managed code içinde vulnerability barındırabilir. File syntactically geçerli görünse bile parser exploit payload'ı taşıyabilir.
Resource exhaustion
- Büyük request body
- Çok sayıda multipart part
- Yüksek image dimensions
- Çok uzun media duration
- Yüksek page count
- Çok sayıda archive entry
- Yüksek compression ratio
- Deeply nested archive
- Çok uzun filename ve metadata
- Yavaş upload
- Tek kullanıcıdan yüksek concurrency
CPU, memory, temporary disk, file descriptor, queue ve object storage maliyeti ayrı ayrı tüketilebilir.
Path ve object overwrite
Client-controlled filename veya archive entry, intended directory dışına yazabilir. Aynı object key'in tekrar kullanılması mevcut dosyayı veya başka kullanıcıya ait içeriği overwrite edebilir.
Data exfiltration ve internal access
Parser veya document formatı external URL, embedded link veya remote resource çözümleyebilir. Processing worker'ın network erişimi varsa file içeriği SSRF benzeri outbound request tetikleyebilir.
Downstream injection
Filename, metadata veya extracted text şu alanlara taşınabilir:
- Log
- SQL query
- Command line
- Shell script
Content-Dispositionheader- HTML template
- CSV export
- Search index
- Message queue
File byte'ları kadar metadata da untrusted input'tur.
Kontrol 1: Upload endpoint'ini authorization sınırı olarak tasarlayın
Upload endpoint'i authenticated olması gereken bir business operation'dır.
Kontrol edilmesi gerekenler:
- Kullanıcı upload yapmaya yetkili mi?
- File hangi parent object'e bağlanıyor?
- Kullanıcı o parent object üzerinde write yetkisine sahip mi?
- Tenant ID request'ten mi geliyor, verified identity context'ten mi?
- File category kullanıcı tarafından değiştirilebiliyor mu?
- Admin veya support flow'u aynı policy'yi bypass ediyor mu?
- Upload session başka kullanıcı tarafından tamamlanabiliyor mu?
- Direct object storage upload token'ı doğru user ve object'e bağlı mı?
Riskli örnek:
POST /api/cases/8a3f/attachments
Content-Type: multipart/form-dataEndpoint yalnızca kullanıcının authenticated olduğunu kontrol edip caseId ownership'ini doğrulamıyorsa file başka kullanıcının case kaydına eklenebilir.
File upload ve file download authorization'ı ayrı ayrı uygulanmalıdır. Upload etmeye yetkili olmak bütün dosyaları indirmeye yetkili olmak anlamına gelmez.
Object-level authorization hatalarının temel nedenlerini IDOR açığı neden hâlâ bu kadar yaygın? yazımızda ayrıca inceliyoruz.
Kontrol 2: CSRF ve cross-origin upload modelini doğrulayın
Browser session cookie veya başka ambient credential kullanıyorsa upload endpoint'i CSRF açısından state-changing operation'dır.
Saldırgan, kullanıcının browser'ından istenmeyen bir file veya form submit etmeyi deneyebilir. Modern browser davranışı bazı cross-origin request biçimlerini sınırlandırsa da yalnızca CORS'a güvenilmemelidir.
Sorulacak sorular:
- Authentication cookie tabanlı mı?
- CSRF token upload request'e doğru taşınıyor mu?
- Multipart request CSRF middleware'den geçiyor mu?
- Direct upload için oluşturulan short-lived credential yalnızca intended object key'e mi bağlı?
- Cross-origin upload gerçekten gerekli mi?
- Allowed origin exact ve yönetilebilir mi?
- Credentialed CORS policy gereğinden geniş mi?
Bearer-only API'de token browser tarafından otomatik eklenmiyorsa CSRF modeli farklı olabilir. Karar “API olduğu için” değil, credential transport'a göre verilmelidir.
Kontrol 3: Limitleri yalnızca application code içinde koymayın
File size kontrolü birden fazla katmanda bulunmalıdır:
CDN veya edge
→ Load balancer
→ Reverse proxy veya API gateway
→ Web server ve multipart parser
→ Application
→ Temporary storage
→ Processing worker
→ Object storage ve quotaApplication code file.getSize() kontrolüne ulaşmadan önce proxy veya Servlet container request'i disk ya da memory üzerinde buffer etmiş olabilir.
Tanımlanması gereken limitler
- Maximum HTTP request size
- Maximum single file size
- Minimum meaningful file size
- Maximum multipart part count
- Maximum text field size
- Maximum header size
- Maximum filename length
- Maximum concurrent upload
- User ve tenant başına günlük upload count
- User ve tenant başına toplam storage
- Upload timeout
- Processing timeout
- Queue depth
- Temporary disk quota
Spring Boot örneği
spring:
servlet:
multipart:
max-file-size: 6MB
max-request-size: 7MB
file-size-threshold: 0B
resolve-lazily: falseBu değerler örnektir. Business requirement'a göre belirlenmelidir.
max-request-size, file size yanında multipart overhead ve diğer form part'larını da dikkate almalıdır. Edge limitinin framework limitinden çok yüksek olması, application reddetmeden önce gereksiz bandwidth ve buffer tüketimi oluşturabilir.
Spring'in StandardServletMultipartResolver implementation'ı Servlet container'ın multipart parser'ını kullanır. Bu nedenle container davranışı ve temporary storage ayarı da review kapsamındadır.
Content-Length tek güven kaynağı değildir
Client header'ı yanlış verebilir, hiç göndermeyebilir veya chunked transfer kullanabilir. Streaming sırasında gerçek byte count ayrıca sınırlandırılmalıdır.
Kontrol 4: Original filename'i storage path olarak kullanmayın
invoice.pdf kullanıcı deneyimi için anlamlı bir display name olabilir. Storage identity olmamalıdır.
Client-controlled filename şunları içerebilir:
../veya..\- Absolute path
- Drive letter
- UNC path
- Slash ve backslash
- Control character
- CR ve LF
- Unicode separator veya confusable character
- Trailing dot veya space
- Çok sayıda extension
- Platform reserved name
- Alternate data stream syntax
- Çok uzun Unicode sequence
Spring'in güncel MultipartFile.getOriginalFilename() dokümantasyonu da değerin client tarafından sağlandığını ve doğrudan kullanılmaması gerektiğini açıkça belirtir.
Doğru ayrım
storage_key = server-generated opaque identifier
display_name = sanitized user-facing metadataÖrnek:
storage_key:
quarantine/2026/07/8f1a4f91-7dc4-4210-9e5d-a551f9414099
display_name:
temmuz-faturasi.pdfStorage key içinde original filename veya user-controlled extension bulunmaması, web server misconfiguration ve path handling riskini azaltır.
Filename normalization ne için yapılır?
Original filename storage path olarak kullanılmasa bile:
- UI'da gösterilir
- Log'a yazılır
- Download header'ına eklenir
- Search index'e gönderilir
- Audit kaydında tutulur
Bu nedenle control character, path separator, bidi control ve header injection karakterleri temizlenmeli, Unicode normalization uygulanmalı ve uzunluk sınırı konulmalıdır.
Filename sanitization file'ı güvenli hale getirmez. Yalnızca metadata'nın güvenli kullanılmasını sağlar.
Kontrol 5: Extension allowlist kullanın, fakat ona güven kararı vermeyin
Extension business policy için yararlı bir ilk sinyaldir.
Kötü yaklaşım:
deny:
.php
.jsp
.exeBlacklist yaklaşımı şu nedenlerle zayıftır:
- Tehlikeli extension listesi platforma göre değişir
- Yeni veya unutulmuş handler bulunabilir
- Case ve Unicode normalization farkları oluşabilir
- Double extension kullanılabilir
- Web server son extension ile application ilk extension'ı yorumlayabilir
- Trailing dot veya space farklı işlenebilir
- Content başka bir parser tarafından açılabilir
Daha güçlü yaklaşım:
profile-image:
allow:
- jpg
- jpeg
- png
invoice-document:
allow:
- pdfAllowlist'te olmayan type, scanner temiz dese bile kabul edilmemelidir. Antivirus sonucu business type policy'nin yerine geçmez.
Extension nasıl çıkarılmalı?
- Filename önce tutarlı Unicode formuna normalize edilmeli
- Path segment'leri ayrıştırılmamalı, çünkü original name path değildir
- Son extension lower-case ve locale-independent işlenmeli
- Beklenmeyen karakter içeren extension reddedilmeli
- Empty, hidden veya trailing-dot isimler policy'ye göre ele alınmalı
- Multi-extension yalnızca son extension ile çözülmemeli, content validation ile birlikte değerlendirilmelidir
report.php.jpg son extension açısından JPEG görünür. Dosya gerçek JPEG olarak parse ve gerekiyorsa re-encode edilmedikçe güvenilir sayılmamalıdır.
Kontrol 6: Request Content-Type değerini yalnızca signal olarak kullanın
Multipart part içindeki:
Content-Type: image/jpegdeğeri client tarafından gönderilir. Proxy veya framework bu değeri doğrulanmış type'a dönüştürmez.
Bu değer:
- Erken rejection
- Telemetry
- Client error feedback
- Policy mismatch tespiti
için kullanılabilir. Tek başına file type kanıtı olamaz.
Application şu üç değeri ayrı tutmalıdır:
declared_media_type
detected_media_type
approved_media_typedeclared_media_type, client claim'idir.
detected_media_type, signature veya parser sonucudur.
approved_media_type, business policy ve processing sonucu server tarafından verilen karardır.
Kontrol 7: File signature kontrol edin, fakat magic byte'a kör güvenmeyin
File signature, içeriğin başlangıcındaki bilinen byte pattern'ini kontrol eder.
Örneğin:
- JPEG
FF D8 FF - PNG
89 50 4E 47 0D 0A 1A 0A - PDF
%PDF- - ZIP
PK
Bu kontrol extension ve client MIME spoofing'i yakalamaya yardımcı olur. Fakat şu nedenlerle tek başına yeterli değildir:
- Başlangıç byte'ları taklit edilebilir
- File, geçerli header sonrası malicious payload taşıyabilir
- Polyglot file birden fazla format olarak yorumlanabilir
- ZIP tabanlı DOCX, XLSX, JAR ve APK aynı container signature'ını paylaşabilir
- Truncated file signature kontrolünü geçebilir
- Parser'ın yorumladığı structure doğrulanmamış olabilir
- Geçerli file formatı malware veya active content içerebilir
Doğru katmanlar:
Extension allowlist
+ Declared MIME karşılaştırması
+ File signature
+ Structural parser validation
+ Type-specific policy
+ Malware scan
+ Gerekiyorsa CDR veya re-encodingBu katmanlar birbirinin alternatifi değildir.
Kontrol 8: File'ı gerçekten parse edin
File'ın type'ı yalnızca header'dan anlaşılmamalıdır. İlgili format için güvenilir ve güncel parser, structure'ı tamamıyla okuyabilmelidir.
Parser validation şu sorulara cevap arar:
- Format syntactically geçerli mi?
- File truncated mı?
- Beklenmeyen trailing data var mı?
- Declared container structure doğru mu?
- Page, frame, sheet veya entry count limit içinde mi?
- Embedded object var mı?
- External reference var mı?
- Macro veya script var mı?
- Encrypted veya password-protected mı?
- Parser warning ve recovery mode ile mi kabul edildi?
Parser'ın “açabildiği” file her zaman güvenli değildir
Bazı parser'lar malformed input'u tolerance amacıyla onarabilir. Bu davranış başka bir downstream parser'ın aynı byte'ları farklı yorumlamasına yol açabilir.
Validation sonucu şu şekilde kaydedilebilir:
content_validation:
detected_media_type: application/pdf
parser: pdf-validator
parser_version: 4.2.1
structurally_valid: true
encrypted: false
page_count: 12
embedded_files: 0
javascript_actions: 0
external_references: 0Version örnektir. Gerçek sistem parser ve rule version'ını evidence olarak tutmalıdır.
Kontrol 9: Type-specific policy uygulayın
Her file formatının attack surface'i farklıdır.
Raster image
Kontroller:
- Width ve height
- Toplam pixel count
- Frame count
- Color profile
- Metadata
- Decoder timeout
- Decode sonrası memory tahmini
- Re-encoding
5 MB'lık compressed image, decode edildiğinde yüzlerce MB memory isteyebilir. Limit yalnızca compressed byte size üzerinde olmamalıdır.
Risk bazlı model:
decoded_memory_estimate
= width × height × channel_count × bytes_per_channelInteger overflow önlenmeli ve hesap long gibi yeterli genişlikte type ile yapılmalıdır.
Profile image gibi use case'lerde image decode edilip canonical JPEG veya PNG olarak yeniden encode edilebilir. Original metadata ve beklenmeyen trailing content bu süreçte kaldırılabilir. Re-encoding yine güncel parser ve isolated worker gerektirir.
SVG
SVG XML tabanlı active content taşıyabilir:
- Script
- Event handler
- External resource
- Link
- Foreign object
- CSS
- Entity ve parser behavior
SVG'yi “image olduğu için güvenli” kabul etmek hatalıdır. Gerekmiyorsa kabul etmeyin. Gerekiyorsa güvenilir sanitizer, isolated origin ve restrictive rendering policy kullanın. Sanitized output yeniden parse edilerek doğrulanmalıdır.
PDF şu içerikleri barındırabilir:
- JavaScript action
- Embedded file
- Form
- External link
- Launch action
- Rich media
- Encryption
- Malformed object graph
PDF scanner sonucu temiz olsa bile browser veya desktop viewer üzerinde active behavior taşıyabilir. Use case'e göre CDR, flattened preview veya attachment-only download tercih edilebilir.
Office document
DOCX ve XLSX ZIP tabanlı container'lardır. Macro-enabled format, external template, embedded object, formula ve link riskleri ayrıca değerlendirilmelidir.
Sadece ZIP signature görmek document type doğrulaması değildir. Container içindeki expected directory, relationship ve content type yapısı parse edilmelidir.
CSV
CSV executable binary değildir. Fakat spreadsheet programında açıldığında formula injection riski oluşturabilir.
Upload edilen CSV:
- Import parser limitleri
- Column count
- Row count
- Encoding
- Delimiter
- Quote handling
- Formula prefix
- Type conversion
- Business validation
açısından değerlendirilmelidir.
CSV'nin daha sonra yeniden export edilmesi veya analyst'e gönderilmesi downstream risk üretir.
Media file
Audio ve video için:
- Duration
- Resolution
- Frame rate
- Codec
- Track count
- Metadata
- Transcoding cost
- Decoder timeout
sınırları gerekir. Transcoding worker internet erişimi ve secret taşımamalıdır.
Archive
Archive en yüksek riskli file class'larından biridir. Bir sonraki bölümde ayrı ele alınmalıdır.
Kontrol 10: Archive extraction'ı ayrı bir security boundary kabul edin
ZIP file 2 MB olabilir. Extraction sonucu 20 GB, yüz binlerce entry veya çok derin directory tree üretebilir.
OWASP File Upload Cheat Sheet ve CWE-409, compressed input size yanında decompressed output'un da sınırlandırılması gerektiğini vurgular.
Archive için minimum limitler
- Maximum archive size
- Maximum uncompressed total size
- Maximum single entry size
- Maximum entry count
- Maximum directory depth
- Maximum nested archive depth
- Maximum compression ratio
- Maximum filename length
- Maximum extraction time
- Maximum concurrent extraction
Zip Slip kontrolü
Archive entry name doğrudan extraction path'e eklenmemelidir.
Riskli:
Path target = extractionRoot.resolve(entry.getName());
Files.copy(zipInputStream, target);Entry:
../../../../app/config/application.ymlolduğunda intended directory dışına yazabilir.
Minimum lexical boundary:
Path root = extractionRoot.toAbsolutePath().normalize();
Path target = root.resolve(entry.getName()).normalize();
if (!target.startsWith(root)) {
throw new UnsafeArchiveEntryException(entry.getName());
}Bu kontrol gerekli fakat tek başına bütün filesystem risklerini çözmez.
Symlink ve race condition
Extraction dizini başka bir process veya user tarafından değiştirilebiliyorsa parent directory içine symlink eklenebilir. Lexical path root altında görünürken gerçek target dışarı çıkabilir.
Daha güçlü model:
- Her job için yeni ve private extraction directory
- Directory başka principal tarafından writable değil
- Symlink ve hard link entry'leri reddediliyor
- Absolute path ve drive prefix reddediliyor
- Existing file üzerine yazılmıyor
CREATE_NEWbenzeri atomic creation kullanılıyor- Extraction sandbox içinde gerçekleşiyor
- Çıktı final storage'a doğrudan taşınmıyor
Standard ZIP API'si her archive metadata türünü aynı ayrıntıyla sunmayabilir. Kullanılan library'nin symlink, duplicate name, Unicode ve extra field davranışı ayrıca doğrulanmalıdır.
Duplicate entry
Aynı path'e sahip iki entry farklı parser'lar tarafından farklı sırada ele alınabilir. İlk entry scanner tarafından, son entry extraction service tarafından kullanılabilir.
Duplicate normalized path'ler reddedilmelidir.
Nested archive
ZIP içinde ZIP, document içinde embedded archive veya recursive container bulunabilir.
Policy:
archive_policy:
max_archive_bytes: 10485760
max_uncompressed_bytes: 104857600
max_entry_bytes: 20971520
max_entries: 500
max_directory_depth: 8
max_nested_archive_depth: 1
max_compression_ratio: 50
reject_duplicate_paths: true
reject_symlinks: true
reject_absolute_paths: trueDeğerler örnektir. Business ihtiyacına göre performans testiyle belirlenmelidir.
Kontrol 11: File processing'i application process'inden ayırın
Image decode, PDF parse, OCR, document conversion, thumbnail ve media transcoding yüksek riskli işlemlerdir.
Parser worker mümkünse şu özelliklere sahip olmalıdır:
- Ayrı container veya sandbox
- Non-root user
- Read-only root filesystem
- Job'a özel writable scratch alanı
- Network egress kapalı veya çok dar
- Cloud metadata erişimi kapalı
- Secret bulunmuyor
- Database credential bulunmuyor
- CPU limiti
- Memory limiti
- Process ve thread limiti
- File descriptor limiti
- Wall-clock timeout
- Seccomp veya platforma uygun syscall kısıtı
- Güncel parser ve codec
- Job sonrası workspace destruction
Neden network kapalı olmalı?
Document veya parser external resource çözmeye çalışabilir. Processing worker internet veya internal network'e erişebiliyorsa malicious file:
- Internal URL çağırabilir
- Cloud metadata service'e erişebilir
- DNS request üretebilir
- Data exfiltration yapabilir
- Remote font veya template çekebilir
Bu nedenle parser-level “external reference kapalı” ayarı ile network egress birlikte uygulanmalıdır.
SSRF savunmasının DNS, redirect ve network katmanını SSRF'yi kapatmak için yalnızca allowlist yeterli mi? yazımızda ayrıntılı inceliyoruz.
Kontrol 12: Quarantine storage kullanın
File receive edildikten sonra final public storage'a yazılmamalıdır.
Quarantine alanı:
- Private olmalı
- Web server tarafından serve edilmemeli
- Execute permission taşımamalı
- Application'ın static resource path'i olmamalı
- Short-lived retention kullanmalı
- Ayrı encryption key veya bucket policy kullanabilir
- Yalnızca upload service ve scanner tarafından erişilebilir olmalı
- User download flow'undan kapalı olmalı
File'ın quarantine alanında bulunması onun güvenli olduğu anlamına gelmez. Sadece untrusted content'in etkisini sınırlar.
Local filesystem kullanılıyorsa
- Webroot dışında
- Dedicated mount
noexec,nosuidve platforma uygun mount policy- Ayrı OS principal
- Restrictive permission
- Storage quota
- Dedicated temp directory
- Cleanup monitor
kullanılmalıdır.
noexec tek başına yeterli değildir. Interpreter bir file'ı data olarak okuyup çalıştırabilir. Application veya parser da içeriği yorumlayabilir.
Object storage kullanılıyorsa
- Public access kapalı
- Server-generated object key
- Bucket policy least privilege
- Upload ve approved object prefix'leri ayrı
- Versioning ve overwrite policy açık
- Event notification güven sınırı olarak kabul edilmiyor
- Object metadata client-controlled kabul ediliyor
- Server-side encryption authorization yerine geçmiyor
Encryption at rest, file'ın malware veya XSS içermesini engellemez.
Kontrol 13: Malware scanning'i güvenlik kararıyla doğru bağlayın
Antivirus veya malware scanner yararlı bir control'dür. Tek başına “file güvenlidir” garantisi vermez.
Scanner:
- Bilinen signature
- Heuristic
- Archive inspection
- Reputation
- Rule-based detection
sunabilir. Zero-day parser exploit, business abuse veya active HTML behavior her zaman bulunmayabilir.
Scan sonucu hangi object'e ait?
En kritik sorulardan biri budur.
Yanlış model:
scan object key
→ result clean
→ aynı key daha sonra overwrite edilebilir
→ download latest objectScanner temiz bir version'ı görürken kullanıcı aynı key'e farklı content yazabilir.
Daha güçlü model:
object version
+ byte size
+ cryptographic digest
→ scan request
→ scan result
→ approval recordApproval yalnızca scan edilen immutable byte sequence için geçerli olmalıdır.
Örnek scan evidence:
scan:
upload_id: 8f1a4f91-7dc4-4210-9e5d-a551f9414099
object_version: "01J2Q7BYS48A"
sha256: 56d7e85b43d97c58a65c2e9d4d1e2db537e03d67f2744bd9a7ca8c116da18e37
size_bytes: 184223
engine: malware-scanner
engine_version: 7.4.2
signature_version: "2026-07-26T08:00:00Z"
result: clean
scanned_at: "2026-07-26T09:14:33Z"Değerler örnektir.
Scanner timeout ve error aynı şey değildir
State'ler ayrılmalıdır:
CLEANMALICIOUSUNSUPPORTEDENCRYPTEDTIMEOUTENGINE_ERRORPOLICY_REJECTED
TIMEOUT veya ENGINE_ERROR, CLEAN değildir.
Retry nasıl yapılmalı?
- Aynı immutable object version yeniden scan edilmeli
- Retry count sınırlı olmalı
- Exponential backoff kullanılmalı
- Poison file queue'yu sonsuza kadar bloke etmemeli
- Scanner kapasitesi için backpressure olmalı
- Kullanıcıya “processing” state'i gösterilmeli
- Uzun süre çözülemeyen job quarantine retention'a göre kapatılmalı
Kontrol 14: CDR ve re-encoding'i doğru konumlandırın
Content Disarm and Reconstruction, supported document type içindeki aktif veya beklenmeyen bileşenleri kaldırıp yeni bir file üretmeyi hedefler.
CDR özellikle:
- Office document
- Bazı image ve document workflow'ları
için değerlendirilebilir.
Ancak CDR:
- Her formatı desteklemez
- Business data'yı değiştirebilir
- Digital signature'ı bozabilir
- Forensic original'i ortadan kaldırmamalıdır
- Çıktının yeniden doğrulanmasını gerektirir
- Kendi parser ve supply chain riskine sahiptir
Doğru model:
original immutable evidence
→ isolated CDR worker
→ reconstructed output
→ output validation
→ output malware scan
→ approved derivativeKullanıcıya sunulan file original mi derivative mi olduğu metadata içinde açıkça tutulmalıdır.
Kontrol 15: Direct-to-object-storage upload güvenliği
Büyük file'larda application server üzerinden proxy etmek yerine client'a short-lived upload URL verilebilir.
Bu model scalability sağlar. Security control'leri ortadan kaldırmaz.
Güvenli session oluşturma
Upload session şu değerlere bağlanmalıdır:
upload_session:
id: 3c6b3947-dcb2-45ef-9e4a-f9c6e5899238
user_id: 44de7ac3
tenant_id: 193ca7b4
parent_object_id: case-8a3f
object_key: quarantine/193ca7b4/3c6b3947-dcb2-45ef-9e4a-f9c6e5899238
maximum_bytes: 6291456
allowed_declared_types:
- image/jpeg
- image/png
expires_at: "2026-07-26T09:30:00Z"
maximum_upload_attempts: 1Object key client tarafından seçilmemelidir.
Upload tamamlandı callback'ine güvenmeyin
Client:
POST /api/uploads/3c6b3947/completeçağrısıyla “yükleme tamamlandı” diyebilir. Server şu değerleri object storage üzerinden kendisi doğrulamalıdır:
- Exact object key
- Object version
- Actual size
- Upload session expiry
- Expected tenant ve parent object
- Checksum varsa exact checksum
- Object daha önce finalize edilmiş mi?
Client'ın gönderdiği size, MIME ve checksum tek güven kaynağı değildir.
Presigned URL hangi yetkiyi vermeli?
- Tek object key
- Tek HTTP method
- Kısa expiry
- Sınırlı content length
- Gerekli header'lar
- Overwrite engeli
- Public ACL verme yetkisi yok
- Başka prefix'e yazma yetkisi yok
Multipart object-storage upload kullanılıyorsa abandoned part cleanup ve toplam part count ayrıca yönetilmelidir.
Kontrol 16: TOCTOU ve immutable object problemi
Time-of-check to time-of-use file upload pipeline'ında sık görülür.
T1 File scan edilir
T2 File aynı path veya key üzerinde değiştirilir
T3 Application file'ı kullanıcıya sunarT1'de kontrol edilen byte'lar ile T3'te kullanılan byte'lar farklıdır.
Önlemler:
- Content-addressed veya immutable object version
CREATE_NEW- Conditional write
- Object version ID
- Digest binding
- Quarantine'den approved alana server-side copy
- Copy sonrası digest veya provider checksum doğrulaması
- Approved object'in overwrite edilememesi
- Metadata ile object version'ın atomic ilişkilendirilmesi
Digest malware kontrolü değildir. Aynı byte sequence'in korunduğunu kanıtlamak için kullanılır.
Kontrol 17: Database ve object storage state'ini tutarlı yönetin
Şu failure durumları planlanmalıdır:
- Object yazıldı, database insert başarısız
- Database record oluştu, object upload başarısız
- Scan message publish edilemedi
- Scan tamamlandı, result persist edilemedi
- File approved oldu, derivative copy başarısız
- Delete record oluştu, object silinemedi
- Retry duplicate message üretti
State transition idempotent olmalıdır.
Örnek:
receive upload
→ quarantine object oluştur
→ metadata record QUARANTINED
→ transactional outbox SCAN_REQUESTED
→ worker exact version'ı scan et
→ compare-and-set SCANNING → APPROVEDDuplicate event aynı file'ı ikinci kez public yapmamalı veya state'i geriye taşımamalıdır.
Kontrol 18: Güvenli download endpoint'i tasarlayın
Upload edilen file'ın güvenliği download sırasında yeniden değerlendirilir.
Minimum kontroller:
- Kullanıcı authenticated mı?
- File metadata hangi tenant'a ait?
- Kullanıcı bu file veya parent object üzerinde read yetkisine sahip mi?
- File state
APPROVEDveyaAVAILABLEmı? - Object version approval kaydıyla eşleşiyor mu?
- Response media type server-detected değerden mi geliyor?
- Filename güvenli şekilde encode ediliyor mu?
Content-Dispositiondoğru mu?X-Content-Type-Options: nosniffvar mı?- Private content cache policy uygun mu?
- Range request gerekiyorsa limitli mi?
- Download count ve bandwidth abuse kontrol ediliyor mu?
Storage key client'a açılmamalı
Riskli:
GET /download?path=tenant-a/contracts/2026/invoice.pdfDaha güçlü:
GET /api/files/8f1a4f91-7dc4-4210-9e5d-a551f9414099Opaque ID server-side metadata'ya çözülür. Kullanıcı storage path oluşturmaz.
Attachment ve inline ayrımı
Untrusted veya active content mümkünse:
Content-Disposition: attachmentile indirilmelidir.
Inline preview business requirement ise:
- Derivative preview
- Ayrı cookie-less origin
- Restrictive CSP
- Sandbox
- Correct
Content-Type nosniff- No application secrets
- No same-origin access
birlikte düşünülmelidir.
attachment tek başına bütün browser ve downstream davranış risklerini ortadan kaldırmaz. File kullanıcı tarafından indirildikten sonra local application içinde açılabilir.
Kontrol 19: Same-origin serving riskini azaltın
User-controlled HTML veya SVG ana application origin'inden yayınlanırsa browser onu application trust boundary içinde yorumlayabilir.
Örnek riskli origin:
https://app.example.com/uploads/user-file.svgDaha güçlü ayrım:
https://user-content.example-cdn.net/object-idUser content origin:
- Authentication cookie almamalı
- Application local storage'a erişememeli
- Wildcard cookie scope dikkatle tasarlanmalı
- CORS gereksiz yere açılmamalı
- CSP çok dar olmalı
Content-Typeserver tarafından belirlenmelinosniffkullanılmalı
files.example.com kullanmak tek başına izolasyon garantisi değildir. Cookie domain .example.com olarak ayarlanmışsa browser cookie'yi subdomain'e gönderebilir.
Kontrol 20: Content-Disposition header'ını string birleştirme ile üretmeyin
Riskli:
response.setHeader(
"Content-Disposition",
"attachment; filename=\"" + displayName + "\""
);Filename CR, LF, quote veya farklı encoding davranışı içeriyorsa header injection ve bozuk response oluşabilir.
Framework'ün RFC uyumlu builder'ı kullanılmalıdır.
Spring örneği:
ContentDisposition disposition = ContentDisposition
.attachment()
.filename(displayName, StandardCharsets.UTF_8)
.build();
headers.setContentDisposition(disposition);Display name yine sanitize ve length-limited olmalıdır. RFC 6266 da filename bilgisinin advisory kabul edilmesi gerektiğini vurgular.
Kontrol 21: Preview ve derivative üretimini original file'dan ayırın
Thumbnail, text extraction, OCR ve preview ayrı object üretmelidir.
Metadata:
file:
upload_id: 8f1a4f91-7dc4-4210-9e5d-a551f9414099
original_object_version: "01J2Q7BYS48A"
original_sha256: 56d7e85b43d97c58a65c2e9d4d1e2db537e03d67f2744bd9a7ca8c116da18e37
status: APPROVED
derivatives:
- type: thumbnail
object_version: "01J2Q7EFXTMZ"
media_type: image/png
generator_version: thumbnail-worker-3.1
- type: text-preview
object_version: "01J2Q7F47R2Q"
media_type: text/plain
generator_version: text-worker-2.4Derivative:
- Hangi original version'dan üretildiği
- Hangi worker version'ı
- Hangi policy
- Kendi digest'i
- Kendi validation sonucu
ile izlenmelidir.
Original değişirse derivative otomatik güvenilir sayılmamalıdır.
Kontrol 22: Temporary file lifecycle'ını yönetin
Framework veya container request'i application code'dan önce temporary disk'e yazabilir.
Kontrol listesi:
- Temporary location biliniyor mu?
- Container restart sonrası orphan file kalıyor mu?
- Disk quota var mı?
- Temp directory başka workload ile ortak mı?
- Permission doğru mu?
- File request sonrası gerçekten siliniyor mu?
- Crash ve timeout cleanup nasıl yapılıyor?
- Scanner temp kopyasını siliyor mu?
- Sensitive content encrypted volume gerektiriyor mu?
- Temp path monitoring var mı?
Spring MultipartFile içeriği memory veya temporary disk üzerinde tutulabilir. Persistent storage'a kopyalama ve lifecycle sorumluluğu application'a aittir.
Kontrol 23: Quota yalnızca file size değildir
Bir kullanıcı her biri limit altında binlerce file yükleyebilir.
Quota boyutları:
- File başına byte
- Request başına toplam byte
- Saatlik ve günlük upload count
- User başına total stored byte
- Tenant başına total stored byte
- Concurrent upload
- Concurrent scan
- Concurrent transcoding
- Derivative count
- Archive entry count
- Download bandwidth
- API ve object storage cost
Quota race condition'a açık olmamalıdır. İki concurrent request aynı kalan quota'yı okuyup birlikte aşabilir.
Reservation modeli kullanılabilir:
upload session oluştur
→ quota reserve et
→ upload tamamlanırsa actual usage'a çevir
→ timeout olursa reservation bırakKontrol 24: Logging ve privacy dengesini kurun
File content log'a yazılmamalıdır.
Audit için yararlı alanlar:
- Upload ID
- User ve tenant ID
- Parent object ID
- Sanitized display name
- Actual byte size
- Detected media type
- Digest
- State transition
- Scanner result code
- Parser ve scanner version
- Rejection reason category
- Timestamp
- Correlation ID
Kaçınılması gerekenler:
- Raw file body
- Unsanitized filename
- Presigned URL
- Object storage credential
- Antivirus engine raw output içinde sensitive content
- Extracted document text
- Personal data içeren metadata
Digest bazı düşük entropili veya bilinen file'lar için correlation signal olabilir. Log ve erişim policy'si buna göre belirlenmelidir.
Kontrol 25: Retention, deletion ve incident response planlayın
File upload security yalnızca kabul anını kapsamaz.
Tanımlanması gerekenler:
- Quarantine retention
- Rejected file retention
- Malware evidence retention
- Approved original retention
- Derivative retention
- User deletion talebi
- Backup ve replica deletion
- Legal hold
- Scanner signature güncellendiğinde rescan
- Yeni kritik parser CVE'sinde reprocessing
- Incident sırasında ilgili file'ların bulunması
Rescan ne zaman gerekir?
- Yeni malware signature
- Yeni parser vulnerability
- Policy değişikliği
- Scanner false negative olayı
- File yeniden farklı context'te kullanılacaksa
- Original daha yüksek privilege worker tarafından işlenecekse
Geçmişte CLEAN sonucu alan file sonsuza kadar risksiz kabul edilmemelidir.
Java Spring için güvenli upload örneği
Aşağıdaki örnek bütün production infrastructure'ın yerine geçmez. Temel admission flow'unu gösterir:
- Original filename storage key değildir
- Stream sırasında gerçek byte count sınırlandırılır
- SHA-256 exact content'e bağlanır
- File private quarantine alanına yazılır
- Declared MIME yalnızca metadata olarak tutulur
- State
QUARANTINEDolarak başlar - Scanner işi immutable object reference ve digest ile oluşturulur
Domain tipleri
public enum UploadStatus {
QUARANTINED,
SCANNING,
APPROVED,
REJECTED,
SCAN_FAILED
}public record UploadReceipt(
UUID uploadId,
UploadStatus status
) {
}public record StoredUpload(
UUID uploadId,
UUID tenantId,
String objectKey,
String objectVersion,
String displayName,
String declaredMediaType,
long sizeBytes,
String sha256,
UploadStatus status
) {
}Storage ve queue contract'leri
public interface QuarantineStorage {
StoredObject writeNew(
String objectKey,
InputStream input,
long maximumBytes
) throws IOException;
InputStream open(
String objectKey,
String objectVersion
) throws IOException;
void delete(
String objectKey,
String objectVersion
) throws IOException;
}public record StoredObject(
String key,
String version,
long sizeBytes,
String sha256
) {
}public interface UploadRepository {
void insertWithScanRequest(
StoredUpload upload,
ScanRequested event
);
Optional<StoredUpload> findById(UUID uploadId);
}public record ScanRequested(
UUID uploadId,
String objectKey,
String objectVersion,
long sizeBytes,
String sha256
) {
}insertWithScanRequest() upload kaydını ve outbox kaydını aynı database transaction'ında oluşturmalıdır. Ayrı bir publisher outbox event'ini queue'ya iletir. Böylece process database commit'ten sonra çökerse scan talebi kaybolmaz. Publisher at-least-once delivery yapabileceği için worker uploadId, objectVersion ve digest üzerinden idempotent olmalıdır.
Filename metadata normalization
final class UploadNames {
private static final int MAX_CODE_POINTS = 150;
private UploadNames() {
}
static String displayName(String originalFilename) {
String value = Optional.ofNullable(originalFilename)
.orElse("upload")
.replace('/', '_')
.replace('\\', '_');
value = Normalizer.normalize(value, Normalizer.Form.NFC);
String cleaned = value.codePoints()
.filter(UploadNames::isAllowedCodePoint)
.limit(MAX_CODE_POINTS)
.collect(
StringBuilder::new,
StringBuilder::appendCodePoint,
StringBuilder::append
)
.toString()
.trim();
if (cleaned.isBlank()
|| cleaned.equals(".")
|| cleaned.equals("..")) {
return "upload";
}
return cleaned;
}
static String extension(String displayName) {
int lastDot = displayName.lastIndexOf('.');
if (lastDot <= 0 || lastDot == displayName.length() - 1) {
return "";
}
String extension = displayName
.substring(lastDot + 1)
.toLowerCase(Locale.ROOT);
if (!extension.matches("[a-z0-9]{1,10}")) {
return "";
}
return extension;
}
private static boolean isAllowedCodePoint(int codePoint) {
int type = Character.getType(codePoint);
return type != Character.CONTROL
&& type != Character.FORMAT
&& codePoint != 0x7F;
}
}Bu function storage path üretmez. Yalnızca user-facing metadata'yı sınırlar. File type kararı için extension'a ek olarak content inspection gerekir.
Upload service
@Service
public class SecureUploadService {
private static final long MAXIMUM_BYTES = 5L * 1024L * 1024L;
private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
"jpg",
"jpeg",
"png"
);
private final QuarantineStorage storage;
private final UploadRepository repository;
private final CurrentTenant currentTenant;
public SecureUploadService(
QuarantineStorage storage,
UploadRepository repository,
CurrentTenant currentTenant
) {
this.storage = storage;
this.repository = repository;
this.currentTenant = currentTenant;
}
@Transactional
public UploadReceipt receive(MultipartFile file) throws IOException {
validatePreliminarySize(file);
UUID tenantId = currentTenant.requiredTenantId();
UUID uploadId = UUID.randomUUID();
String displayName = UploadNames.displayName(
file.getOriginalFilename()
);
String extension = UploadNames.extension(displayName);
if (!ALLOWED_EXTENSIONS.contains(extension)) {
throw new UnsupportedUploadTypeException();
}
String objectKey = "quarantine/"
+ tenantId
+ "/"
+ uploadId;
StoredObject stored = storage.writeNew(
objectKey,
file.getInputStream(),
MAXIMUM_BYTES
);
StoredUpload upload = new StoredUpload(
uploadId,
tenantId,
stored.key(),
stored.version(),
displayName,
Optional.ofNullable(file.getContentType())
.orElse("application/octet-stream"),
stored.sizeBytes(),
stored.sha256(),
UploadStatus.QUARANTINED
);
try {
repository.insertWithScanRequest(
upload,
new ScanRequested(
upload.uploadId(),
upload.objectKey(),
upload.objectVersion(),
upload.sizeBytes(),
upload.sha256()
)
);
} catch (RuntimeException exception) {
try {
storage.delete(stored.key(), stored.version());
} catch (IOException cleanupException) {
exception.addSuppressed(cleanupException);
}
throw exception;
}
return new UploadReceipt(
upload.uploadId(),
upload.status()
);
}
private static void validatePreliminarySize(MultipartFile file) {
if (file.isEmpty()) {
throw new EmptyUploadException();
}
if (file.getSize() > MAXIMUM_BYTES) {
throw new UploadTooLargeException();
}
}
}Bu örnekte QuarantineStorage.writeNew() gerçek byte count'u streaming sırasında zorunlu olarak sınırlamalı ve client'ın bildirdiği size'a güvenmemelidir.
@Transactional, upload kaydı ile outbox kaydını aynı database transaction'ında tutar. External storage write'ını database transaction'ıyla atomic hale getirmez. Commit failure ve process crash nedeniyle oluşabilecek orphan object'ler için süre bazlı cleanup gerekir. Outbox publisher ve scan worker da idempotent çalışmalıdır.
Bounded copy ve digest örneği
Local veya object storage adapter'ında kullanılabilecek temel loop:
static StoredObject copyBounded(
String key,
String version,
InputStream source,
OutputStream destination,
long maximumBytes
) throws IOException {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException(
"SHA-256 runtime tarafından desteklenmiyor",
exception
);
}
byte[] buffer = new byte[8192];
long total = 0L;
try (
InputStream input = source;
OutputStream output = destination
) {
int read;
while ((read = input.read(buffer)) != -1) {
long nextTotal = Math.addExact(total, read);
if (nextTotal > maximumBytes) {
throw new UploadTooLargeException();
}
digest.update(buffer, 0, read);
output.write(buffer, 0, read);
total = nextTotal;
}
output.flush();
}
String sha256 = HexFormat.of().formatHex(digest.digest());
return new StoredObject(
key,
version,
total,
sha256
);
}Adapter exception durumunda incomplete object'i temizlemeli veya incomplete version'ı hiçbir consumer'a görünür kılmamalıdır. Local filesystem implementation'ı random temporary file'a yazıp doğrulama sonrası atomic move kullanabilir. Destination shared ve attacker-writable olmamalıdır.
Content inspection nerede?
Upload service file'ı yalnızca quarantine'a alır. Content inspection ve malware scan ayrı worker'da exact object version üzerinden yapılır.
Worker sırası:
Open exact immutable object version
→ Recompute ve compare SHA-256
→ Detect media type
→ Parse structure
→ Enforce image dimensions ve type-specific policy
→ Malware scan
→ Re-encode veya CDR gerekiyorsa derivative üret
→ Compare-and-set state to APPROVEDDeclared MIME hiçbir aşamada detected MIME yerine geçirilmemelidir.
Java Spring için güvenli download örneği
@RestController
@RequestMapping("/api/files")
public class FileDownloadController {
private final UploadQueryService uploads;
private final ApprovedStorage storage;
public FileDownloadController(
UploadQueryService uploads,
ApprovedStorage storage
) {
this.uploads = uploads;
this.storage = storage;
}
@GetMapping("/{uploadId}")
@PreAuthorize(
"@fileAuthorization.canRead(#uploadId, authentication)"
)
public ResponseEntity<StreamingResponseBody> download(
@PathVariable UUID uploadId
) {
ApprovedUpload upload = uploads.requireApproved(uploadId);
StreamingResponseBody body = output -> {
try (InputStream input = storage.open(
upload.objectKey(),
upload.objectVersion()
)) {
input.transferTo(output);
}
};
ContentDisposition disposition = ContentDisposition
.attachment()
.filename(
upload.displayName(),
StandardCharsets.UTF_8
)
.build();
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(
upload.approvedMediaType()
))
.contentLength(upload.sizeBytes())
.header(
HttpHeaders.CONTENT_DISPOSITION,
disposition.toString()
)
.header("X-Content-Type-Options", "nosniff")
.cacheControl(CacheControl.noStore())
.body(body);
}
}Bu örnekte:
- Authorization opaque
uploadIdüzerinde uygulanır - Service yalnızca
APPROVEDstate kabul eder - Exact object version açılır
- Media type client'tan değil approval metadata'dan gelir
- Filename framework builder ile encode edilir
- Private content için
no-storekullanılır
Uygulamanın download volume'u yüksekse application proxy yerine authorization sonrası short-lived signed download URL üretilebilir. URL exact approved object version'a bağlanmalı ve kısa süreli olmalıdır.
Geliştirici için code review arama listesi
Java ve Spring repository'sinde başlangıç aramaları:
rg -n 'MultipartFile|MultipartHttpServletRequest|Part\\b|getParts\\(' src
rg -n 'getOriginalFilename|getContentType|getSize|transferTo|getInputStream' src
rg -n 'Files\\.copy|Files\\.write|Path\\.of|Paths\\.get|resolve\\(' src
rg -n 'ZipInputStream|ZipFile|ZipEntry|TarArchive|SevenZFile' src
rg -n 'ImageIO|ObjectInputStream|DocumentBuilderFactory|XMLInputFactory' src
rg -n 'Content-Disposition|setHeader|addHeader|StreamingResponseBody' src
rg -n 'presign|signedUrl|putObject|uploadPart|completeMultipart' src
rg -n 'scan|antivirus|malware|quarantine|thumbnail|transcode|convert' src
rg -n 'spring\\.servlet\\.multipart|max-file-size|max-request-size' .Bu aramalar finding değildir. Entry point, storage sink, parser ve serving flow haritası oluşturur.
Java Spring source code review'in genel başlangıç sırasını Java Spring projelerinde güvenlik kod incelemesine nereden başlanır? rehberimizde bulabilirsiniz.
Güvenli file upload test matrisi
Authentication ve authorization
- Anonymous upload reddediliyor mu?
- Cross-tenant parent object'e attachment eklenebiliyor mu?
- Başka kullanıcının upload session'ı tamamlanabiliyor mu?
- Rejected veya quarantined file indirilebiliyor mu?
- Direct object storage key tahmin edilebiliyor mu?
- Deleted file eski signed URL ile erişilebilir mi?
Multipart parser
- Missing boundary
- Malformed boundary
- Çok uzun boundary
- Duplicate part name
- Çok sayıda part
- File part yerine text part
- Duplicate filename parameter
- Filename ve filename* çelişkisi
- Missing
Content-Type - Chunked body
- Yavaş upload
- Erken bağlantı kesilmesi
Filename
../../file.jpg..\..\ile.jpg/etc/file.jpgC:\ emp\ile.jpg\\server\share\ile.jpgfile.jpg.phpfile.php.jpgfile.jpg.file.jpg.jpg..- Control character
- CR ve LF
- Bidi control
- Çok uzun Unicode filename
- Aynı normalized name
- Platform reserved name
Extension, MIME ve signature
- Allowed extension ve yanlış MIME
- Allowed MIME ve yanlış extension
- Allowed signature ve malformed body
- Polyglot file
- Truncated file
- Valid image header sonrası trailing content
- Uppercase extension
- Unicode confusable dot
- ZIP signature taşıyan farklı container type
- Encrypted veya password-protected document
Image
- Aşırı width
- Aşırı height
- Aşırı pixel count
- Çok frame'li image
- ICC profile
- Büyük EXIF
- Parser timeout
- Corrupted image
- SVG script
- SVG external reference
- SVG foreign object
PDF ve document
- Embedded file
- JavaScript action
- External link
- Launch action
- Macro-enabled format
- External template
- Encrypted document
- Çok yüksek page count
- Parser recovery gerektiren malformed structure
- Digital signature bulunan document
Archive
../entry- Absolute path
- Windows drive path
- Symlink entry
- Hard link entry
- Duplicate normalized entry
- Çok sayıda entry
- Çok derin directory
- Yüksek compression ratio
- Nested archive
- Zero-byte entry flood
- Çok uzun entry name
- Extraction timeout
- Existing file overwrite
Resource limit
- File limitinin bir byte üstü
- Request limitinin bir byte üstü
Content-Lengthküçük, actual body büyükContent-Lengthyok- Çok sayıda eş zamanlı upload
- User quota sınırında concurrent request
- Temporary disk dolu
- Scanner queue dolu
- Object storage error
- Network timeout
Scanner ve state
- Malware result
- Clean result
- Timeout
- Engine error
- Unsupported format
- Encrypted file
- Duplicate scan event
- Out-of-order event
- Scan sonrası object overwrite denemesi
- Digest mismatch
- Scanner signature update sırasında job
- Quarantine retention expiry
Download ve preview
- Cross-user read
- Cross-tenant read
- Quarantined object read
- Rejected object read
- Original yerine yanlış derivative
- Filename header injection
- Wrong
Content-Type - Missing
nosniff - Unexpected inline rendering
- Shared cache leakage
- Range request abuse
- Expired signed URL
- Same-origin active content
Bu matrix her application için aynı biçimde uygulanmaz. Use case'e göre test set'i daraltılır veya genişletilir. Fakat her control için positive ve negative test bulunmalıdır.
CI/CD içinde hangi kontroller otomatikleştirilebilir?
Unit test
- Filename normalization
- Extension extraction
- Policy allowlist
- State transition
- Quota reservation
- Header builder
- Archive path normalization
Integration test
- Multipart size limit
- Authentication ve authorization
- Quarantine state
- Database ile storage failure
- Scanner event
- Approved-only download
- Exact object version
SAST
- Client filename'in filesystem path'e ulaşması
- User input ile storage key üretimi
Files.copyvetransferTosink'leri- Dynamic command ve parser call
- Unsafe archive extraction
- Header concatenation
- Missing authorization annotation için custom policy
SCA
- Parser
- Image codec
- PDF library
- Archive library
- Multipart parser
- Antivirus client
dependency risklerini izler.
Security regression corpus
Kurum, güvenli biçimde saklanan test fixture set'i oluşturabilir:
- Path traversal archive
- Oversized image dimensions
- Malformed JPEG
- SVG active content
- PDF embedded object
- Double extension
- MIME mismatch
- Scanner test signature
Gerçek malware production developer cihazlarında rastgele dolaştırılmamalıdır. Test artifact'ları yetkili, izole ve kontrollü ortamda yönetilmelidir.
CI/CD security gate tasarımında yapılan hataları CI/CD'ye SAST eklerken en sık yapılan 7 hata yazımızda ayrıca ele alıyoruz.
File upload finding'i nasıl raporlanmalı?
“File upload kontrolü yetersiz” başlığı uygulanabilir değildir.
Güçlü finding şu alanları içerir:
- Upload endpoint
- Gerekli role
- Parent object ve tenant
- Kabul edilen type
- Bypass edilen control
- Stored object location
- Processing component
- Serving behavior
- Reproduction request
- Exact file hash
- Security impact
- Root cause
- Systemic pattern
- Remediation katmanları
- Regression criteria
Örnek:
finding:
title: Quarantine bypass through public object prefix
severity: high
endpoint: POST /api/cases/{caseId}/attachments
precondition:
authentication: valid customer session
root_cause:
- direct upload target uses publicly readable prefix
- object becomes readable before malware scan
- download path does not verify upload status
impact:
- attacker can host active HTML content
- attacker can distribute unscanned files through trusted domain
remediation:
- use private quarantine prefix
- require immutable object version scan
- expose only APPROVED objects
- serve active content from isolated origin
regression:
- quarantined object returns access denied
- scanner timeout never produces APPROVED state
- approved download resolves exact scanned object versionEn sık yapılan yanlışlar
Yalnızca extension kontrolü
Extension client-controlled metadata'dır. File structure ve business policy ayrıca doğrulanmalıdır.
Yalnızca Content-Type kontrolü
Multipart MIME değeri client tarafından üretilebilir.
Yalnızca magic byte kontrolü
Header taklit edilebilir. Polyglot, malformed structure ve active content problemi devam eder.
Antivirus temizse kabul etmek
Scanner business type, object authorization, parser safety ve resource limit yerine geçmez.
Original filename ile kaydetmek
Path traversal, overwrite, normalization ve header riskleri üretir.
Upload dizinini public yapmak
File scan edilmeden veya authorization uygulanmadan erişilebilir hale gelebilir.
Webroot dışındaysa tamamen güvenli saymak
Parser exploit, stored XSS, download authorization ve resource exhaustion riskleri devam eder.
noexec mount'u yeterli görmek
Interpreter veya application file'ı data olarak okuyabilir. Client-side active content de etkilenmez.
Size limitini yalnızca controller'da koymak
Request application'a ulaşmadan proxy, parser, memory veya temp disk tüketebilir.
Scanner error'ı clean kabul etmek
TIMEOUT, ENGINE_ERROR ve UNSUPPORTED ayrı state'lerdir.
Scan edilen path'i overwrite edilebilir bırakmak
Kontrol edilen byte ile sunulan byte farklı olabilir.
Direct upload callback'ine güvenmek
Server actual object key, version ve size bilgisini storage üzerinden doğrulamalıdır.
Download authorization'ı unutmak
Upload ID enumeration veya cross-tenant object erişimi private file disclosure oluşturabilir.
SVG'yi sıradan image kabul etmek
SVG active XML ve browser content yüzeyi taşır.
ZIP size'ını compressed byte ile ölçmek
Uncompressed total, entry count, nested depth ve compression ratio ayrıca sınırlandırılmalıdır.
Satın alınan sızma testinde file upload scope'u nasıl yazılmalı?
File upload test kapsamı yalnızca “dosya yükleme kontrol edilecektir” olmamalıdır.
Minimum kapsam:
- Upload endpoint ve role
- Kabul edilen type listesi
- File ve request limitleri
- Direct upload flow
- Filename ve path handling
- Object storage permission
- Quarantine ve approval state
- Malware scanning
- Archive extraction
- Thumbnail, OCR, CDR ve transcoding
- Download ve preview
- Cross-tenant authorization
- Same-origin serving
- Failure ve retry state
- Cleanup ve retention
Web uygulaması sızma testinde bu kapsamın nasıl tanımlanacağını Web uygulaması sızma testinde kapsam nasıl belirlenir? yazımızda ayrıntılı olarak açıklıyoruz.
Uygulanabilir geliştirici kontrol listesi
Business requirement
- [ ] Yalnızca gerçekten gerekli file type'lar kabul ediliyor
- [ ] Her upload use case'i için ayrı policy tanımlı
- [ ] Maximum ve minimum file size tanımlı
- [ ] File count ve storage quota tanımlı
- [ ] Original, derivative ve preview retention tanımlı
- [ ] Inline preview ihtiyacı gerekçelendirilmiş
Identity ve authorization
- [ ] Upload authenticated business operation
- [ ] Parent object ownership doğrulanıyor
- [ ] Tenant identity request'ten alınmıyor
- [ ] Upload session user ve tenant'a bağlı
- [ ] Download ayrıca authorize ediliyor
- [ ] Quarantined ve rejected state hiçbir normal read flow'undan açılmıyor
Request handling
- [ ] Edge request limitleri var
- [ ] Reverse proxy limitleri var
- [ ] Framework multipart limitleri var
- [ ] Application streaming byte limitini tekrar uyguluyor
- [ ] Multipart part count sınırlandırılıyor
- [ ] Upload concurrency ve timeout sınırlandırılıyor
- [ ] Temporary storage quota ve cleanup izleniyor
Filename ve metadata
- [ ] Original filename storage path değil
- [ ] Storage key server-generated
- [ ] Display name normalize ve sanitize ediliyor
- [ ] Control character ve CRLF kaldırılıyor
- [ ] Metadata uzunluğu sınırlandırılıyor
- [ ] Filename log ve header'a güvenli API ile yazılıyor
Type validation
- [ ] Extension allowlist use case'e özgü
- [ ] Client MIME yalnızca signal
- [ ] File signature kontrol ediliyor
- [ ] Structure güvenilir parser ile doğrulanıyor
- [ ] Detected ve approved media type ayrı tutuluyor
- [ ] Parser recovery ile kabul edilen file'lar policy'ye göre yönetiliyor
- [ ] Polyglot ve trailing data senaryoları test ediliyor
Type-specific processing
- [ ] Image dimensions ve pixel count sınırlandırılıyor
- [ ] Frame, page, sheet, track veya entry count sınırlandırılıyor
- [ ] SVG active content policy'si var
- [ ] PDF embedded content policy'si var
- [ ] Office macro ve external reference policy'si var
- [ ] CSV formula riskine göre işleniyor
- [ ] Archive uncompressed limitleri var
- [ ] Archive path, symlink ve duplicate entry kontrolleri var
Storage
- [ ] Quarantine private
- [ ] Upload webroot dışında
- [ ] Execute permission yok
- [ ] Object key overwrite edilemiyor
- [ ] Immutable version veya equivalent control var
- [ ] Public access kapalı
- [ ] Storage permission least privilege
- [ ] Orphan object cleanup var
Scanner ve processing
- [ ] Scanner exact object version ve digest üzerinde çalışıyor
- [ ] Scanner result version'lanıyor
- [ ] Timeout ve error clean sayılmıyor
- [ ] Retry sınırlı ve idempotent
- [ ] Parser worker sandbox içinde
- [ ] Worker network egress kapalı veya dar
- [ ] Worker secret taşımıyor
- [ ] CPU, memory, process ve time limitleri var
- [ ] CDR veya re-encoding çıktısı yeniden doğrulanıyor
Download ve preview
- [ ] Yalnızca approved object sunuluyor
- [ ] Exact approved version okunuyor
- [ ] Media type server metadata'sından geliyor
- [ ]
Content-Dispositiongüvenli API ile oluşturuluyor - [ ]
X-Content-Type-Options: nosniffkullanılıyor - [ ] Private content cache policy'si doğru
- [ ] Active content ayrı origin'de
- [ ] Application cookie'leri user-content origin'e gitmiyor
- [ ] Signed URL short-lived ve exact version'a bağlı
Lifecycle ve operasyon
- [ ] State machine açıkça tanımlı
- [ ] Database ve storage failure durumları ele alınıyor
- [ ] Event'ler idempotent
- [ ] Quarantine retention var
- [ ] Rejected file politikası var
- [ ] Rescan kriterleri var
- [ ] Parser ve scanner version'ı kaydediliyor
- [ ] Incident sırasında file digest ile bulunabiliyor
- [ ] User deletion backup ve derivative'leri kapsıyor
Sonuç: Güvenli dosya yükleme bir endpoint değil, kontrollü file lifecycle'dır
Güvenli dosya yükleme için tek bir kontrol yoktur.
Extension allowlist gereklidir, fakat file'ın içeriğini doğrulamaz.
MIME kontrolü yararlıdır, fakat client claim'idir.
Magic byte kontrolü format sinyali sağlar, fakat structure ve active content'i kanıtlamaz.
Malware scanner değerlidir, fakat authorization, parser isolation ve business policy'nin yerine geçmez.
Private storage etkiyi sınırlar, fakat download ve preview güvenliğini kendiliğinden çözmez.
Profesyonel bir implementation şu güvenlik sözünü vermelidir:
- 1Yalnızca yetkili kullanıcı intended business object'e upload yapabilir
- 2Request, file, concurrency ve storage limitleri bütün katmanlarda uygulanır
- 3Client filename hiçbir storage path veya object identity üretmez
- 4Type kararı extension, signature, parser ve business policy ile verilir
- 5File onaylanana kadar private quarantine alanında kalır
- 6Scanner ve parser exact immutable byte version üzerinde çalışır
- 7Parser düşük privilege ve network-isolated worker içinde yürütülür
- 8Archive ve media için expanded resource limitleri uygulanır
- 9Yalnızca approved version authorized download flow'undan sunulur
- 10State, evidence, retry, retention ve incident response izlenebilir durumdadır
Secnodex, file upload güvenliğini yalnızca yasaklı extension listesi veya scanner çıktısı üzerinden değerlendirmez. Upload request'inden object storage'a, parser worker'dan malware scanning ve CDR akışına, authorization'dan download header ve isolated preview davranışına kadar bütün lifecycle incelenir. Bulgular reproducible evidence, affected code path, runtime behavior, root cause ve regression kriterleriyle raporlanır.
Web application, API veya source code kapsamınızdaki file upload surface'ini değerlendirmek için Secnodex ile iletişime geçebilirsiniz.
Kaynaklar
- OWASP File Upload Cheat Sheet
- OWASP Application Security Verification Standard 5.0
- OWASP WSTG – Test Upload of Unexpected File Types
- OWASP WSTG – Test Upload of Malicious Files
- OWASP Unrestricted File Upload
- OWASP Denial of Service Cheat Sheet
- OWASP Content Security Policy Cheat Sheet
- CWE-434 – Unrestricted Upload of File with Dangerous Type
- CWE-22 – Path Traversal
- CWE-409 – Improper Handling of Highly Compressed Data
- CWE-400 – Uncontrolled Resource Consumption
- Spring Framework – MultipartFile
- Spring Framework – StandardServletMultipartResolver
- Spring Boot – Common Application Properties
- RFC 7578 – Returning Values from Forms: multipart/form-data
- RFC 6266 – Content-Disposition in HTTP
- MDN – X-Content-Type-Options
Sık sorulan sorular
Güvenli dosya yükleme için extension kontrolü yeterli mi?
Hayır. Extension business allowlist için kullanılır. Client MIME, file signature, structural parsing, type-specific policy, malware scanning ve güvenli storage ile birlikte değerlendirilmelidir.
MIME type güvenilir mi?
Multipart request içindeki MIME değeri client tarafından gönderilir. Erken kontrol ve telemetry için yararlıdır. Doğrulanmış content type değildir.
Magic byte kontrolü file'ı güvenli yapar mı?
Hayır. File signature yalnızca başlangıç pattern'ini doğrular. Malformed structure, polyglot, active content, malware ve parser exploit riskini çözmez.
Antivirus taraması zorunlu mu?
Risk ve use case'e göre malware scanning güçlü bir control'dür. Özellikle file başka kullanıcılara sunuluyor veya parser tarafından işleniyorsa önemlidir. Scanner tek başına yeterli değildir.
Dosya webroot dışında saklanırsa risk biter mi?
Server-side execution riski azalır. Parser exploit, malware dağıtımı, stored XSS, broken authorization, archive bomb ve resource exhaustion devam edebilir.
Original filename saklanabilir mi?
User-facing metadata olarak sanitize edilip saklanabilir. Storage key veya filesystem path olarak kullanılmamalıdır.
Dosya adını UUID yapmak yeterli mi?
Path ve overwrite riskini azaltır. Content validation, scanning, authorization, processing ve serving kontrollerinin yerine geçmez.
SVG güvenli bir image formatı mıdır?
SVG active XML içeriği, script ve external reference taşıyabilir. Gerekmiyorsa kabul edilmemeli. Gerekiyorsa sanitizer, isolated origin ve restrictive rendering policy kullanılmalıdır.
PDF dosyaları güvenli kabul edilebilir mi?
Hayır. Geçerli PDF active action, embedded file, external link veya parser exploit içerebilir. Use case'e göre CDR, attachment-only serving veya flattened preview uygulanabilir.
ZIP bomb nasıl engellenir?
Compressed file size yanında total uncompressed size, single entry size, entry count, directory depth, nesting ve compression ratio sınırlandırılır. Extraction isolated worker içinde yapılır.
Presigned upload URL file'ı güvenli hale getirir mi?
Hayır. Yalnızca upload transport ve authorization modelini değiştirir. Object server tarafından doğrulanmalı, quarantine'da tutulmalı ve scan edilmeden available olmamalıdır.
Scanner çalışmıyorsa file kabul edilmeli mi?
Business risk kararına göre upload SCAN_FAILED veya processing state'inde tutulabilir. Scanner error clean sonucuna çevrilmemelidir.
File hash malware kontrolü müdür?
Hayır. Digest exact byte sequence'i tanımlar. Known malicious hash listesi ayrı bir reputation signal olabilir. Digest tek başına content'in güvenli olduğunu göstermez.
Image re-encoding bütün riskleri kaldırır mı?
Hayır. Metadata ve trailing content'i azaltabilir. Decoder vulnerability ve resource exhaustion re-encoding öncesinde tetiklenebilir. İşlem isolated worker'da limitlerle yapılmalıdır.
Quarantine file'a kullanıcı erişebilir mi?
Normal şartlarda hayır. Quarantine, untrusted object'lerin scanner ve authorized processing service dışında erişilemediği private alandır.
File upload testi yalnızca web shell denemek midir?
Hayır. Authorization, path, object overwrite, MIME confusion, parser behavior, archive extraction, resource exhaustion, scan bypass, state race, stored XSS ve download güvenliği birlikte test edilir.
Secure Code Review file upload açığını bulabilir mi?
Manual review filename-to-path flow, storage permission, parser invocation, state machine, authorization ve failure handling problemlerini gösterebilir. Runtime test gerçek deployment ve serving davranışını doğrular. İki çalışma birlikte daha güçlü kanıt üretir.
Okumaya devam et
Kaynak Kod Analizi
Secure Code Review ile Otomatik SAST Taraması Farkı
SAST kurallarla ifade edilebilen code pattern ve data flow'ları ölçekli biçimde tarar. Secure Code Review ise security requirement, business logic ve implementation context'ini insan muhakemesiyle değerlendirir.
Yazıyı okuKaynak Kod Analizi
Java Spring Projelerinde Güvenlik Kod İncelemesine Nereden Başlanır?
Java Spring projelerinde güvenlik kod incelemesi yalnızca controller veya SecurityConfig dosyasını okumakla başlamaz. Build graph, aktif profile, filter chain, authorization modeli, data binding ve runtime configuration birlikte incelenmelidir.
Yazıyı oku