Skip to main content
Web Uygulama Güvenliği · 44 dk okuma

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.

SX

SECNODEX Güvenlik Ekibi

Ofansif Güvenlik

Bir kullanıcı profil fotoğrafı yüklüyor.

Backend yalnızca üç kontrol yapıyor:

  1. 1Filename .jpg ile bitiyor mu?
  2. 2Request Content-Type değeri image/jpeg mi?
  3. 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
→ Serve

Her aşamanın farklı security boundary'si vardır.

AşamaTemel risk
ReceiveYetkisiz upload, multipart abuse, oversized request, type spoofing
StorePath traversal, overwrite, public exposure, executable permission, quota exhaustion
ProcessParser exploit, malware, command injection, XXE, decompression bomb, resource exhaustion
ServeBroken 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 preview

File state'i açıkça modellenebilir:

RECEIVING
→ QUARANTINED
→ SCANNING
→ APPROVED
→ AVAILABLE

SCANNING
→ REJECTED

SCANNING
→ SCAN_FAILED

AVAILABLE
→ EXPIRED
→ DELETED

Bu 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-Disposition header
  • 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-data

Endpoint 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 quota

Application 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: false

Bu 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.pdf

Storage 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
.exe

Blacklist 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:
    - pdf

Allowlist'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/jpeg

değ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_type

declared_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-encoding

Bu 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: 0

Version ö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_channel

Integer 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

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.yml

olduğ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_NEW benzeri 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: true

Değ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, nosuid ve 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 object

Scanner 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 record

Approval 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:

  • CLEAN
  • MALICIOUS
  • UNSUPPORTED
  • ENCRYPTED
  • TIMEOUT
  • ENGINE_ERROR
  • POLICY_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:

  • PDF
  • 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 derivative

Kullanı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: 1

Object 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 sunar

T1'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 → APPROVED

Duplicate 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 APPROVED veya AVAILABLE mı?
  • 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-Disposition doğru mu?
  • X-Content-Type-Options: nosniff var 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.pdf

Daha güçlü:

GET /api/files/8f1a4f91-7dc4-4210-9e5d-a551f9414099

Opaque 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: attachment

ile 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.svg

Daha güçlü ayrım:

https://user-content.example-cdn.net/object-id

User 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-Type server tarafından belirlenmeli
  • nosniff kullanı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.4

Derivative:

  • 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ırak

Kontrol 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 QUARANTINED olarak 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 APPROVED

Declared 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 APPROVED state 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-store kullanı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.jpg
  • C:\ emp\ ile.jpg
  • \\server\share\ ile.jpg
  • file.jpg.php
  • file.php.jpg
  • file.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-Length küçük, actual body büyük
  • Content-Length yok
  • Ç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.copy ve transferTo sink'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 version

En 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-Disposition güvenli API ile oluşturuluyor
  • [ ] X-Content-Type-Options: nosniff kullanı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:

  1. 1Yalnızca yetkili kullanıcı intended business object'e upload yapabilir
  2. 2Request, file, concurrency ve storage limitleri bütün katmanlarda uygulanır
  3. 3Client filename hiçbir storage path veya object identity üretmez
  4. 4Type kararı extension, signature, parser ve business policy ile verilir
  5. 5File onaylanana kadar private quarantine alanında kalır
  6. 6Scanner ve parser exact immutable byte version üzerinde çalışır
  7. 7Parser düşük privilege ve network-isolated worker içinde yürütülür
  8. 8Archive ve media için expanded resource limitleri uygulanır
  9. 9Yalnızca approved version authorized download flow'undan sunulur
  10. 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

#File Upload Security#Güvenli Dosya Yükleme#Web Application Security#Secure Coding#Malware Scanning#Path Traversal#Content Validation#OWASP

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.

Siber güvenlik çalışmanızı
SECNODEX ile planlayın

İhtiyacınız tek bir uygulamanın testinden kurum genelinde bir Red Team çalışmasına kadar uzanabilir. Ekibimiz scope’u, kritik varlıkları ve beklenen çıktıları sizinle netleştirir. Ardından iş hedefinize ve risk önceliklerinize uygun, sınırları belirlenmiş bir çalışma planı sunar.