Sunucuya SSH ile bağlandınız, projeyi git clone ile çektiniz, Apache'nin belge kökünü de o klasöre yönlendirdiniz. Site açılıyor, formlar çalışıyor, iş bitti. Ama aynı klasörün içinde .git dizini de duruyor ve dışarıdan biri https://siteniz.com/.git/HEAD adresini istediğinde web sunucusu onu sıradan bir metin dosyası gibi servis ediyor. Klasör listeleme kapalı olsa bile fark etmez: Git'in iç dosya isimleri standarttır, kimsenin dizini görmesine gerek yoktur.
.env tarafı daha da sessiz ilerler. Apache ve nginx bu uzantıyı tanımadığı için dosyayı PHP yorumlayıcısına vermez; olduğu gibi indirir. İçinde veritabanı kullanıcı adı ve parolası, SMTP hesabı, ödeme sağlayıcısının canlı API anahtarı, JWT imzalama sırrı vardır. Tek bir GET isteği, uygulamanızın tüm kimlik bilgilerini düz metin olarak karşı tarafa verir.
Bu iki açık, tarayıcı botlarının en çok denediği yollardandır; kimse sizi özellikle hedeflemek zorunda değildir, sürekli dönen tarayıcılar milyonlarca alan adında aynı yolları dener. Aşağıda önce kendi sitenizi otuz saniyede test edeceksiniz, sonra Apache ve nginx için engelleme bloklarını koyacaksınız. En kritik bölüm ise ortada: dosya bir kez açıldıysa, kapatmak tek başına yetmez.
Önce Test: Kendi Sitenizi 30 Saniyede Kontrol Edin#
Terminalinizden aşağıdaki döngüyü çalıştırın. Alan adını kendinizinkiyle değiştirmeniz yeterli:
SITE="https://ornek.com"
for p in .git/HEAD .git/config .git/index .env .env.bak .env.example \
.env.local .env.production composer.json .svn/entries .DS_Store; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "$SITE/$p")
printf '%-4s %s\n' "$code" "/$p"
done
Sonra durum kodu 200 dönen her yol için içeriğe de bakın. Kod tek başına yanıltıcıdır:
curl -s "$SITE/.git/HEAD" | head -c 120
curl -s "$SITE/.env" | head -c 200
Beklenen çıktılar şunlardır:
| Çıktı | Anlamı |
|---|---|
ref: refs/heads/main | .git açık. Depo dışarıdan yeniden oluşturulabilir. |
APP_KEY=base64:..., DB_PASSWORD=... | .env açık. Tüm sırlar sızmış kabul edilmeli. |
<!DOCTYPE html> ile başlayan HTML | SPA veya uygulama yönlendirmesi devrede; dosya servis edilmiyor. |
| Boş gövde + 403 | Sunucu erişimi engelliyor, dosya var ama okunamıyor. |
| Boş gövde + 404 | Yol bulunamıyor. Genellikle güvenli, ama aşağıdaki uyarıyı okuyun. |
Windows kullanıyorsanız aynı testi PowerShell ile de yapabilirsiniz:
"$( (Invoke-WebRequest -Uri 'https://ornek.com/.git/HEAD' -UseBasicParsing).Content )"
"404 Döndü" Her Zaman Güvenli Anlamına Gelmez#
Testi tek bir noktadan yapıp rahatlamak en sık düşülen hatadır. Üç durum sonucu bozar.
Birincisi, CDN veya WAF katmanı. Cloudflare gibi bir katman önde duruyorsa istek origin sunucuya hiç ulaşmadan durdurulabilir. Bu iyi bir şeydir ama koruma kiralık bir kapıcıdır: DNS kaydı bir dakikalığına açığa çıkarsa veya saldırgan origin IP'nizi bulursa doğrudan kapıya dayanır. Origin'i de ayrıca test edin:
curl -sk --resolve ornek.com:443:203.0.113.10 https://ornek.com/.git/HEAD | head -c 120
Buradaki 203.0.113.10 sunucunuzun gerçek IP'sidir. Bu istek CDN'i atlar ve sunucunuzun kendi cevabını gösterir.
İkincisi, tek dosya değil dizin engeli. Bazı kurulumlarda /.env engellenmiştir ama /.git/config serbesttir; ya da tersi. Yukarıdaki listenin tamamını çalıştırın, ilk 404'te durmayın.
Üçüncüsü, alt alan adları ve eski dizinler. www.ornek.com temiz olabilir ama test.ornek.com, eski.ornek.com veya ornek.com/yedek/ altında aylar önce bırakılmış bir kopya durabilir. Aynı testi her alt alan adı için tekrarlayın.
.git Açıkken Saldırgan Tam Olarak Neyi Ele Geçirir?#
Bir Git deposu, bulunduğu dizindeki .git klasöründe kendini eksiksiz taşır. O klasör okunabiliyorsa proje geçmişiyle birlikte yeniden inşa edilebilir. Klasör listelemesi kapalı olmasının hiçbir önemi yoktur, çünkü dosya adları sabittir:
| Dosya | İçindeki bilgi |
|---|---|
.git/HEAD | Aktif dalın adı; deponun var olduğunun kanıtı |
.git/config | Uzak depo adresi — bazen https://kullanici:token@... biçiminde erişim jetonuyla birlikte |
.git/index | Depodaki tüm dosyaların yolu ve nesne kimliği |
.git/packed-refs | Dalların ve etiketlerin listesi |
.git/logs/HEAD | Commit geçmişinin özeti, geliştirici e-postaları |
.git/objects/... | Dosya içeriklerinin kendisi, sıkıştırılmış hâlde |
.git/index dosyası bu zincirin kilididir: içinde her dosyanın yolu ve içerik özeti bulunur, dolayısıyla saldırganın tahmin yürütmesi gerekmez, tek tek nesneleri indirir ve projeyi kurar. Bu iş için hazır betikler yıllardır dolaşımdadır ve saniyeler sürer.
Kaynak kodun sızması tek başına da ciddidir, ama asıl mesele geçmiştir. Aylar önce yanlışlıkla commit edilip sonraki commit'te silinen bir parola, deponun geçmişinde durmaya devam eder. Saldırgan projeyi indirdikten sonra tek bir komutla geçmişteki tüm sırları tarar. Yani "biz o dosyayı zaten silmiştik" cümlesi burada koruma sağlamaz. Git'in bu davranışının neden böyle olduğunu Git temelleri yazısında ayrıntılı bulabilirsiniz.
Bunun yanında kaynak kod, uygulamanızdaki doğrulama mantığını, admin panel yollarını, henüz yayınlanmamış özellikleri ve varsa yazılım hatalarını da açık eder. Bir saldırgan için bu, kapalı kutu denemesi yerine doğrudan plana bakmaktır.
.env Sızdıysa: Neyi, Hangi Sırayla Değiştirmeniz Gerekiyor?#
Dosyanın açık olduğunu gördüyseniz varsayım net olmalı: o dosya okunmuş kabul edilir. Sunucu erişim günlüklerinde iz bulamamanız aksini kanıtlamaz; kayıtlar döndürülmüş, saldırgan CDN önbelleğinden almış veya siz görmeden aylar geçmiş olabilir. Doğru refleks, kapatma işlemiyle birlikte rotasyona başlamaktır.
| Sır | Aciliyet | Dikkat edilecek nokta |
|---|---|---|
| Ödeme / faturalama API anahtarı | Hemen | Sağlayıcı panelinden iptal edin, yenisini üretin |
| Veritabanı parolası | Hemen | Uzaktan bağlantı açıksa önce erişimi IP ile daraltın |
| SMTP hesabı parolası | Hemen | Sızan SMTP hesabı spam gönderimi için kullanılır, alan adı itibarınız yanar |
| Nesne depolama (S3 vb.) anahtarları | Hemen | Fatura riski taşır; kullanım raporunu da inceleyin |
| Uygulama şifreleme anahtarı | Planlı | Aşağıdaki uyarıyı okumadan değiştirmeyin |
| Webhook imza sırları | Aynı gün | Sağlayıcı tarafında da güncellemeyi unutmayın |
| Oturum / JWT imzalama sırrı | Aynı gün | Değişince tüm kullanıcılar çıkış yapar, bu beklenen davranıştır |
Uygulama şifreleme anahtarı özel bir dikkat ister. Laravel'in APP_KEY değeri gibi anahtarlar yalnızca oturumları değil, veritabanında şifreli tutulan sütunları da açar. Anahtarı düşünmeden değiştirirseniz o sütunlar okunamaz hâle gelir. Doğru sıra şudur: önce yedek alın, sonra şifreli verileri eski anahtarla çözüp yeni anahtarla yeniden yazacak bir geçiş adımı planlayın, ardından anahtarı değiştirin.
Rotasyondan sonra sunucuda kalıcı bir iz bırakılmış olabileceğini de düşünün. Şüpheniz varsa zararlı dosya ve arka kapı arayışını hacklenmiş site kurtarma adımlarıyla yürütün.
Apache'de .git ve .env Erişimini Kapatma#
Apache'nin varsayılan ayarları yalnızca .ht ile başlayan dosyaları engeller; .env ve .git bu kapsamda değildir. En hızlı çözüm belge kökündeki .htaccess dosyasına aşağıdaki satırları eklemektir:
# Nokta ile başlayan hassas yolları 404 ile kapat
RedirectMatch 404 (?i)/\.(git|svn|hg|env|bzr|DS_Store)
# Yaygın yedek ve editör artıklarını da kapat
<FilesMatch "(?i)(\.env.*|\.git.*|composer\.(json|lock)|package-lock\.json|.*\.(bak|old|save|swp|orig|sql))$">
Require all denied
</FilesMatch>
RedirectMatch yerine Require all denied kullanmak 403 döndürür ve dosyanın varlığını dolaylı olarak doğrular; 404 tercih etmek tarayıcı botlarına daha az bilgi verir. Yukarıdaki blok ikisini birlikte kullanır: dizinler 404, dosya kalıpları 403 olur.
.htaccess dosyasının işlenmesi için sanal sunucu tanımında AllowOverride yetkisinin açık olması gerekir. Kapalıysa dosyayı yazarsınız ama hiçbir şey değişmez. Kalıcı ve daha performanslı yol, kuralı doğrudan sanal sunucu bloğuna koymaktır:
<VirtualHost *:443>
ServerName ornek.com
DocumentRoot /var/www/ornek/public
<DirectoryMatch "/\.(git|svn|hg)">
Require all denied
</DirectoryMatch>
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
</VirtualHost>
DirectoryMatch yalnızca sunucu yapılandırmasında geçerlidir, .htaccess içinde çalışmaz — iki bloğun neden farklı yazıldığı budur. Değişiklikten sonra yapılandırmayı sınayıp servisi yeniden yükleyin:
sudo apachectl configtest
sudo systemctl reload apache2
.htaccess mantığına yeniyseniz kural yazımının ayrıntıları için htaccess dosyası oluşturma yazısına bakabilirsiniz.
Nginx'te Dotfile Erişimini Kapatma#
Nginx'te .htaccess benzeri bir dosya yoktur; kural sunucu bloğuna yazılır. Doğru blok şudur:
# Nokta ile başlayan her yolu kapat, Let's Encrypt doğrulamasını hariç tut
location ~ /\.(?!well-known) {
deny all;
access_log off;
log_not_found off;
return 404;
}
# Yedek ve dışa aktarma artıkları
location ~* \.(bak|old|save|swp|orig|sql|sql\.gz)$ {
return 404;
}
Burada iki tuzak vardır ve ikisi de sessizce çalışır.
Birincisi sıra. Nginx düzenli ifadeli konum bloklarını dosyada yazıldıkları sırayla dener ve ilk eşleşende durur. Dotfile bloğunu location ~ \.php$ bloğunun altına koyarsanız, .git dizini içindeki bir PHP dosyası önce PHP bloğuna düşer. Engelleme bloklarını her zaman uygulama bloklarından önce yazın.
İkincisi try_files yönlendirmesi. Tipik bir PHP kurulumunda location / bloğu try_files $uri $uri/ /index.php?$query_string; içerir. .env diskte gerçekten var olduğu için $uri adımında eşleşir ve düz metin olarak servis edilir; uygulamaya hiç uğramaz. Yani "her şey index.php'ye gidiyor" varsayımı burada geçerli değildir.
(?!well-known) istisnası önemlidir: bu ifadeyi kaldırırsanız /.well-known/acme-challenge/ yolu da kapanır ve Let's Encrypt sertifika yenilemeleri sessizce başarısız olmaya başlar. Sertifikanız 60. günde yenilenmeyi denediğinde bu hatayı hatırlamak zor olur.
Değişikliği uygulamadan önce her zaman sınayın:
sudo nginx -t && sudo systemctl reload nginx
Nginx tarafında güvenlikle ilgili diğer başlıkları da toparlamak isterseniz nginx rate limit ve güvenlik başlıkları yazısı iyi bir devam noktasıdır.
Kalıcı Çözüm: Kaynak Kodu Belge Kökünün Dışına Almak#
Yukarıdaki bloklar yarayı kapatır ama sorunun kaynağını çözmez. Asıl sorun, çalıştırılabilir olmayan proje dosyalarının web sunucusunun erişebildiği bir yerde durmasıdır. Bir kural unutulduğunda, sanal sunucu yeniden oluşturulduğunda veya siteyi başka bir sunucuya taşıdığınızda açık geri gelir.
Doğru yerleşim, belge kökünü projenin genel klasörüne göstermektir:
/var/www/ornek/
├── .git/ <- web'den ERİŞİLEMEZ
├── .env <- web'den ERİŞİLEMEZ
├── app/ <- web'den ERİŞİLEMEZ
├── vendor/ <- web'den ERİŞİLEMEZ
└── public/ <- DocumentRoot burası
├── index.php
└── assets/
Modern PHP çatılarının neredeyse tamamı (Laravel, Symfony, Slim) zaten bu yapıyı varsayar. WordPress gibi kökten çalışan uygulamalarda ise .git ve .env dosyalarını kök dışına taşımak ve kodu ayrı bir klasörde tutup yalnızca gerekli dosyaları kopyalamak gerekir.
İkinci adım, sunucuda depo tutmayı bırakmaktır. Sunucuya git clone atmak yerine derlenmiş çıktıyı gönderin:
# Sürüm arşivini .git olmadan üret
git archive --format=tar HEAD | gzip > surum.tar.gz
# veya rsync ile, hassas yolları dışarıda bırakarak
rsync -avz --delete \
--exclude '.git' --exclude '.env' --exclude 'node_modules' \
./ deploy@sunucu:/var/www/ornek/
Bu düzende .env dosyası sunucuda elle oluşturulur veya CI tarafından yazılır; depoya hiç girmez. .git ise sunucuya hiç ulaşmaz, dolayısıyla kapatılacak bir şey de kalmaz.
Depoya Bir Daha Sır Girmemesi İçin#
Sunucu tarafını düzeltmek, aynı hatanın tekrarını engellemez. Deponun kök dizinindeki .gitignore dosyasına gerçek yapılandırma dosyalarını yazın, örnek dosyayı ise takip etmeye devam edin:
.env
.env.*
!.env.example
*.sql
*.bak
storage/logs/
Bir dosya daha önce commit edilmişse .gitignore eklemek onu geçmişten silmez; Git yalnızca takip edilmeyen dosyaları yok sayar. Halihazırda izlenen bir dosyayı takipten çıkarmak için:
git rm --cached .env
git commit -m "Yapilandirma dosyasi takipten cikarildi"
Bu komut dosyayı diskinizden silmez, yalnızca depodan çıkarır. Geçmişteki commit'lerde duran eski kopya yine yerindedir; o yüzden sızan parolaları yeniden üretmek her hâlükârda gereklidir.
Ekip çalışıyorsa bir adım daha ekleyin: commit öncesi çalışan basit bir kontrol, .env benzeri dosyaların yanlışlıkla eklenmesini durdurur. Çoğu ekip için bu, CI hattında çalışan tek bir tarama adımıdır ve yılda birkaç kez gerçekten işe yarar.
Sunucunuzda Başka Nerede Açık .git Var?#
Tek siteyi düzeltmek genellikle yetmez. Aynı sunucuda yıllar içinde bırakılmış kopyalar, test kurulumları ve müşteri siteleri olabilir. Envanteri çıkarın:
# Web köklerinin altındaki tüm .git dizinleri
sudo find /var/www /home/*/public_html -maxdepth 5 -type d -name ".git" 2>/dev/null
# .env ve türevleri
sudo find /var/www /home/*/public_html -maxdepth 5 -type f -name ".env*" 2>/dev/null
Listedeki her yol için, o yolun web'den erişilebilir olup olmadığını yukarıdaki curl testiyle ayrıca doğrulayın.
Son olarak geçmişte istismar edilip edilmediğine bakın. Erişim günlüğünde .git veya .env içeren ve 404 dışında bir kodla cevaplanmış istekler asıl aradığınız satırlardır:
sudo awk '$7 ~ /\.(git|env)/ && $9 != 404 {print $9, $1, $7}' \
/var/log/nginx/access.log | sort | uniq -c | sort -rn | head -30
# Döndürülmüş eski günlükleri de tarayın
sudo zgrep -hE '\.(git|env)' /var/log/nginx/access.log.*.gz | grep -v ' 404 ' | head -30
Apache kullanıyorsanız yol /var/log/apache2/access.log olur; alan dizilimi aynıdır. Burada 200 kodlu tek bir satır bile görürseniz, önceki bölümdeki rotasyon listesini bir tatbikat olarak değil gerçek bir olay müdahalesi olarak yürütün.
Sıkça Sorulan Sorular#
Dizin listeleme kapalıysa .git klasörü yine de okunabilir mi?#
Evet. Dizin listeleme yalnızca "bu klasörün içinde ne var" sorusunu engeller. Git'in iç dosya adları standarttır: HEAD, config, index, packed-refs, objects/ gibi yollar her depoda aynıdır. Saldırganın klasörü görmesine gerek yoktur, doğrudan bu adları ister. Bu nedenle listeleme kapatmak bir koruma sayılmaz; erişimi açıkça reddetmeniz gerekir.
.git dosyasını sunucudan silsem yeter mi?#
Açığı kapatır ama olayı kapatmaz. Silmeden önce dosya okunmuşsa kaynak kodunuz ve geçmişteki sırlar zaten karşı taraftadır. Silme işlemini mutlaka yapın, ancak yanına iki adım daha ekleyin: erişim günlüklerinden geçmişte istek olup olmadığını kontrol edin ve .env içindeki tüm kimlik bilgilerini yenileyin.
Cloudflare kullanıyorum, bu isteği zaten engelliyor değil mi?#
Cloudflare'in kural setleri bu tür istekleri çoğunlukla durdurur, fakat bu bir katman korumasıdır, kaynağın kendisi hâlâ savunmasızdır. Origin IP'niz sızarsa, DNS geçmişinizden bulunursa veya bir alt alan adı doğrudan sunucuya bakıyorsa istek kapıya ulaşır. Sunucu tarafındaki engellemeyi mutlaka ayrıca yapın ve testi origin IP üzerinden tekrarlayın.
.env dosyasının izinlerini 600 yapmak sorunu çözer mi?#
Hayır. Dosya izinleri, dosyaya hangi sistem kullanıcısının erişebileceğini belirler. Web sunucusu süreci zaten uygulamanın çalışması için o dosyayı okuyabilen kullanıcı altında koşar. Dolayısıyla istek geldiğinde sunucu dosyayı okur ve gönderir. Çözüm izin değil, dosyayı belge kökünün dışına almak veya web sunucusunda erişimi reddetmektir.
Paylaşımlı hosting kullanıyorum, sunucu yapılandırmasına erişimim yok. Ne yapabilirim?#
Paylaşımlı hosting hesaplarında .htaccess genellikle etkindir; yazının Apache bölümündeki RedirectMatch ve FilesMatch bloklarını public_html içindeki .htaccess dosyasına ekleyebilirsiniz. Ekledikten sonra curl testini mutlaka tekrarlayın, çünkü bazı sağlayıcılarda AllowOverride kısıtlı olabilir. Test başarısızsa dosyaları FTP veya dosya yöneticisiyle web kökünden tamamen kaldırın.
Kaynak kodum zaten GitHub'da herkese açık, .git sızması yine de sorun mu?#
Kodun kendisi açıksa kod sızması sizin için sorun olmayabilir, ancak iki risk devam eder. Birincisi, sunucudaki depoda genel depoya gitmemiş yerel commit'ler, yapılandırma dosyaları veya geçici düzeltmeler bulunabilir. İkincisi, .git/config dosyası çoğu zaman uzak depo adresini ve bazı kurulumlarda erişim jetonunu içerir; bu jeton özel depolarınıza yazma yetkisi verebilir.