PHP Güvenlik Sertleştirme: Tehlikeli Fonksiyonları Kapatma
Üretim ortamında bir PHP uygulaması çalıştırıyorsanız ve sunucunuzu henüz sertleştirmediyseniz, şu an bir saatli bombanın üzerinde oturuyorsunuz demektir. PHP, esnekliği sayesinde web geliştirmenin vazgeçilmezi haline geldi; ancak bu esneklik beraberinde ciddi güvenlik riskleri taşıyor. Özellikle exec(), system(), eval() gibi fonksiyonlar yanlış ellerde sunucunuzun komple ele geçirilmesine zemin hazırlayabilir. Bu yazıda PHP güvenlik sertleştirmesini adım adım ele alacağız, tehlikeli fonksiyonları nasıl kapatacağınızı ve yapılandırmanızı nasıl güçlendireceğinizi gerçek dünya senaryolarıyla açıklayacağız.
Neden PHP Güvenlik Sertleştirmesi Zorunlu?
Birkaç yıl önce müşterime ait bir e-ticaret sitesinde ilginç bir olay yaşandı. Sunucuya bağlandığımda /tmp dizini altında onlarca shell scripti bulmak çok şaşırtıcıydı. Saldırgan, uygulamadaki bir dosya yükleme açığını kullanarak sunucuya PHP web shell yüklemişti. Peki bunu mümkün kılan neydi? Basit: system() fonksiyonu kapatılmamıştı ve PHP open_basedir kısıtlaması yoktu.
PHP güvenlik sertleştirmesi temel olarak üç katmanda ele alınmalı:
- php.ini seviyesi: Tehlikeli fonksiyonları devre dışı bırakma, bellek ve çalışma süresi limitleri
- Web sunucusu seviyesi: PHP’nin erişebileceği dizinleri kısıtlama
- Uygulama seviyesi: Kod içinde güvenli kodlama pratikleri
Bu yazı ağırlıklı olarak php.ini ve sistem yapılandırması üzerine odaklanacak.
Tehlikeli PHP Fonksiyonları: Hangileri Kapatılmalı?
PHP’nin standart kütüphanesinde sistem kaynaklarına doğrudan erişim sağlayan, kod çalıştıran veya ağ bağlantısı kuran pek çok fonksiyon bulunuyor. Bunları üç gruba ayırabiliriz:
Sistem komutu çalıştıran fonksiyonlar:
- exec(): Harici bir sistem komutu çalıştırır ve çıktısını döner
- system(): Komutu çalıştırır, çıktıyı doğrudan tarayıcıya gönderir
- passthru(): Ham çıktıyı direkt aktarır, binary çıktılar için kullanılır
- shell_exec(): Shell üzerinden komut çalıştırır, tüm çıktıyı string olarak döner
- popen(): Bir process açar, pipe ile iletişim kurar
- proc_open():
popen()‘ın daha gelişmiş versiyonu, process yönetimi sağlar - pcntl_exec(): Mevcut process’i yeni bir programla değiştirir
Kod değerlendiren fonksiyonlar:
- eval(): String içindeki PHP kodunu çalıştırır, en tehlikeli fonksiyonların başında gelir
- assert(): PHP 7 öncesinde string argüman alarak kod çalıştırabilir
- preg_replace():
emodifier ile birlikte kullanıldığında kod çalıştırırdı (PHP 7’de kaldırıldı ama eski sürümlerde hala risk) - create_function(): Dinamik fonksiyon oluşturur, esasen
eval()ile aynı riski taşır
Dosya ve ağ işlemleri:
- file_get_contents(): Uzak URL’lerden dosya okuyabilir (allow_url_fopen ile)
- curl_exec(): Ağ istekleri için kullanılır, SSRF saldırılarında araç olabilir
- fsockopen(): Raw socket bağlantısı açar
- stream_socket_client(): Ağ stream’i oluşturur
php.ini Üzerinden Fonksiyon Kapatma
PHP’de tehlikeli fonksiyonları kapatmanın en etkili yolu disable_functions direktifidir. Bu direktif, virgülle ayrılmış fonksiyon isimlerinin listesini alır ve bu fonksiyonları tamamen devre dışı bırakır.
# Mevcut php.ini dosyasını bulma
php --ini
# veya
php -i | grep "Loaded Configuration File"
# Ubuntu/Debian sistemlerde genellikle:
# /etc/php/8.1/fpm/php.ini (PHP-FPM için)
# /etc/php/8.1/cli/php.ini (CLI için)
# /etc/php/8.1/apache2/php.ini (Apache modülü için)
php.ini dosyasını açın ve disable_functions satırını bulup düzenleyin:
sudo nano /etc/php/8.1/fpm/php.ini
Aşağıdaki satırı ekleyin veya mevcut satırı güncelleyin:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,pcntl_exec,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority
Değişiklikleri uygulamak için servisi yeniden başlatın:
# PHP-FPM için
sudo systemctl restart php8.1-fpm
# Apache mod_php için
sudo systemctl restart apache2
# Nginx + PHP-FPM kombinasyonu için
sudo systemctl restart php8.1-fpm
sudo systemctl reload nginx
eval() ile Mücadele: Suhosin ve disable_functions Sınırları
disable_functions direktifinin önemli bir kısıtlaması var: Dil yapılarını (language constructs) kapatamaz. eval() teknik olarak bir fonksiyon değil, PHP’nin dil yapısıdır. Bu yüzden disable_functions listesine eval eklemeniz hiçbir işe yaramaz.
Bu sorunu aşmak için birkaç yöntem mevcut:
Seçenek 1: Suhosin Extension Suhosin, PHP için geliştirilmiş bir güvenlik uzantısıdır ve eval() dahil pek çok tehlikeli özelliği kontrol altına alabilir. Ancak PHP 8.x ile uyumluluk sorunları yaşanabiliyor, bu yüzden alternatif araçlara yönelmek daha mantıklı.
Seçenek 2: disable_classes Kullanımı Belirli sınıfları devre dışı bırakabilirsiniz:
# php.ini içinde
disable_classes = DirectoryIterator,FilesystemIterator
Seçenek 3: open_basedir ile Hasarı Sınırlama eval() kullanılsa bile erişebileceği alanı kısıtlayarak zararı minimize edebilirsiniz:
# php.ini içinde
open_basedir = /var/www/html:/tmp:/usr/share/php
Bu direktif, PHP’nin erişebileceği dizinleri belirtilen yollarla sınırlar. Bir saldırgan kod çalıştırabilse bile /etc/passwd gibi kritik dosyalara erişemez.
PHP-FPM Pool Bazında Güvenlik Yapılandırması
Paylaşımlı hosting veya çok kiracılı ortamlarda her uygulama için ayrı PHP-FPM pool tanımlamak ve her pool’a farklı kısıtlamalar uygulamak en iyi yaklaşımdır.
# Yeni bir pool dosyası oluşturma
sudo nano /etc/php/8.1/fpm/pool.d/myapp.conf
[myapp]
user = www-myapp
group = www-myapp
listen = /run/php/php8.1-fpm-myapp.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
; Uygulama bazında open_basedir
php_admin_value[open_basedir] = /var/www/myapp:/tmp/myapp
php_admin_value[upload_tmp_dir] = /tmp/myapp/uploads
; Bu pool için ek fonksiyon kısıtlamaları
php_admin_value[disable_functions] = exec,system,shell_exec,passthru,proc_open,popen
; Session güvenliği
php_admin_value[session.save_path] = /tmp/myapp/sessions
php_admin_value[session.use_strict_mode] = 1
php_admin_value kullanmanın avantajı, uygulama kodunun bu değerleri ini_set() ile değiştirememesidir. Normal php_value ile tanımlanan değerler uygulama tarafından override edilebilir; bu da bir güvenlik açığı oluşturur.
Kritik php.ini Güvenlik Parametreleri
Fonksiyon kapatmanın ötesinde, genel PHP güvenlik yapılandırması için dikkat edilmesi gereken pek çok parametre var:
# /etc/php/8.1/fpm/php.ini içinde yapılacak değişiklikler
# PHP sürüm bilgisini HTTP header'larında gizle
expose_php = Off
# Hata mesajlarını kullanıcıya gösterme (üretim ortamı)
display_errors = Off
display_startup_errors = Off
# Hataları loglara yaz
log_errors = On
error_log = /var/log/php/php_errors.log
# Uzak dosya açmayı devre dışı bırak
allow_url_fopen = Off
allow_url_include = Off
# Maksimum dosya yükleme boyutu
upload_max_filesize = 10M
max_file_uploads = 20
# POST veri boyutu limiti
post_max_size = 12M
# Script çalışma süresi limiti (saniye)
max_execution_time = 30
# Bellek limiti
memory_limit = 128M
# Session güvenliği
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
session.cookie_samesite = "Strict"
session.name = SESSID
# Varsayılan karakter seti
default_charset = "UTF-8"
Bu parametreleri uyguladıktan sonra yapılandırmanızı doğrulayın:
# Belirli bir parametrenin değerini kontrol etme
php -r "echo ini_get('display_errors');"
# Tüm güvenlik ilgili ayarları listeleme
php -i | grep -E "disable_functions|open_basedir|allow_url|expose_php"
# PHP-FPM konfigürasyonunu test etme
php-fpm8.1 -t
Web Sunucusu Seviyesinde Ek Güvenlik Önlemleri
PHP güvenliği sadece php.ini ile sınırlı kalmamalı. Nginx veya Apache tarafında da gerekli önlemleri almak gerekiyor.
Nginx için:
# /etc/nginx/sites-available/myapp.conf
server {
listen 80;
server_name example.com;
root /var/www/myapp/public;
# PHP dosyalarını doğrudan çalıştırmayı engelle (yükleme dizini için kritik)
location /uploads {
location ~ .php$ {
deny all;
}
}
# Gizli dosyalara erişimi engelle
location ~ /. {
deny all;
access_log off;
log_not_found off;
}
# Hassas PHP dosyalarına erişimi engelle
location ~* (wp-config.php|php.ini|.htaccess|composer.json|composer.lock) {
deny all;
}
# PHP-FPM bağlantısı
location ~ .php$ {
fastcgi_pass unix:/run/php/php8.1-fpm-myapp.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
# FastCGI timeout değerleri
fastcgi_read_timeout 30;
fastcgi_send_timeout 30;
}
}
Apache için:
# .htaccess veya VirtualHost konfigürasyonu
<Directory /var/www/myapp/uploads>
# PHP çalıştırmayı tamamen engelle
php_flag engine off
# Veya alternatif olarak:
<FilesMatch ".php$">
Order Allow,Deny
Deny from all
</FilesMatch>
</Directory>
# Hassas dosyaları koru
<FilesMatch "(php.ini|.htpasswd|composer.(json|lock)|.env)">
Order Allow,Deny
Deny from all
</FilesMatch>
Gerçek Dünya Senaryosu: Web Shell Tespiti ve Önleme
Bir müşteri sunucusunda web shell tespit ettiğimde genellikle şu belirtilere rastlıyorum: beklenmedik CPU/network trafiği, /tmp veya yükleme dizinlerinde şüpheli dosyalar ve PHP hata loglarında eval() veya base64_decode() kombinasyonları.
Tipik bir web shell şuna benzer:
# Bu bir saldırgan örneği - eğitim amaçlı gösteriliyor
# <?php eval(base64_decode($_POST['cmd'])); ?>
# Yukarıdaki kodu hiçbir zaman sunucunuza koymayın!
# Web shell'leri tespit etmek için basit bir tarama:
grep -rn "eval(base64_decode" /var/www/ --include="*.php"
grep -rn "systems*(" /var/www/ --include="*.php" | grep -v "vendor/"
grep -rn "$_POST[" /var/www/ --include="*.php" | grep -v "vendor/" | grep -E "eval|exec|system"
# Son değiştirilen PHP dosyalarını kontrol etme
find /var/www/ -name "*.php" -newer /var/www/html/index.php -ls
# İzin dışı çalıştırılabilir PHP dosyalarını bulma
find /var/www/uploads/ -name "*.php" -type f
Bu tür saldırıların önüne geçmek için disable_functions listesine ek olarak open_basedir ve yükleme dizinleri için PHP engine’ini kapatmak kritik önem taşıyor.
Yapılandırmanızı Test Etme
Tüm değişiklikleri yaptıktan sonra her şeyin doğru çalıştığını test etmeniz gerekiyor. Bunun için basit bir test scripti kullanabilirsiniz:
# Test dosyası oluştur - sonra silin!
cat > /tmp/security_test.php << 'EOF'
<?php
$functions = ['exec', 'system', 'shell_exec', 'passthru', 'proc_open'];
echo "=== PHP Güvenlik Testi ===n";
echo "PHP Versiyonu: " . phpversion() . "nn";
echo "--- Devre Dışı Fonksiyonlar ---n";
foreach ($functions as $func) {
if (function_exists($func)) {
echo "[UYARI] $func aktif!n";
} else {
echo "[OK] $func devre disin";
}
}
echo "n--- Güvenlik Parametreleri ---n";
$params = [
'display_errors' => '0',
'allow_url_fopen' => '0',
'allow_url_include' => '0',
'expose_php' => '0',
];
foreach ($params as $param => $expected) {
$value = ini_get($param);
$status = ($value == $expected) ? "[OK]" : "[UYARI]";
echo "$status $param = $value (beklenen: $expected)n";
}
echo "n--- open_basedir ---n";
$basedir = ini_get('open_basedir');
echo $basedir ? "[OK] open_basedir: $basedirn" : "[UYARI] open_basedir tanimli degil!n";
EOF
# CLI üzerinden çalıştır
php /tmp/security_test.php
# Test dosyasını sil
rm /tmp/security_test.php
Logları İzleme ve Alarm Oluşturma
Güvenlik sertleştirmesi statik bir işlem değil. Sürekli izleme gerektirir. PHP hata loglarını düzenli takip etmek için basit bir bash scripti işinizi görür:
#!/bin/bash
# /usr/local/bin/php-security-monitor.sh
LOG_FILE="/var/log/php/php_errors.log"
ALERT_EMAIL="[email protected]"
SUSPICIOUS_PATTERNS=(
"eval("
"base64_decode"
"system("
"exec("
"shell_exec"
"preg_replace.*/e"
)
for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do
COUNT=$(grep -c "$pattern" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$COUNT" -gt 0 ]; then
echo "UYARI: '$pattern' loglarda $COUNT kez tespit edildi!" |
mail -s "PHP Güvenlik Uyarisi - $(hostname)" "$ALERT_EMAIL"
fi
done
# Logu temizle (eski logları arşivle)
if [ -f "$LOG_FILE" ] && [ $(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE") -gt 104857600 ]; then
mv "$LOG_FILE" "${LOG_FILE}.$(date +%Y%m%d)"
touch "$LOG_FILE"
chown www-data:www-data "$LOG_FILE"
fi
Scripti cron’a ekleyin:
# Her saat başı çalıştır
echo "0 * * * * root /usr/local/bin/php-security-monitor.sh" >> /etc/cron.d/php-security
chmod +x /usr/local/bin/php-security-monitor.sh
Composer ve Bağımlılık Güvenliği
Modern PHP projeleri Composer kullandığından, bağımlılık güvenliği de sertleştirme kapsamına giriyor:
# Güvenlik açığı olan paketleri tara
composer audit
# Geliştirme bağımlılıklarını üretimde yükleme
composer install --no-dev --optimize-autoloader
# vendor dizinine doğrudan web erişimini engelle (Nginx)
location /vendor {
deny all;
return 404;
}
Sonuç
PHP güvenlik sertleştirmesi tek seferlik bir görev değil, sürekli bir süreç. Özetle yapmanız gerekenler şunlar: disable_functions ile sistem komutlarını çalıştıran fonksiyonları kapatın, open_basedir ile PHP’nin erişebileceği dizinleri kısıtlayın, allow_url_fopen ve allow_url_include direktiflerini kapatın, display_errors değerini üretim ortamında Off yapın ve web sunucusu tarafında yükleme dizinleri için PHP çalıştırmayı tamamen engelleyin.
Bu adımların hiçbiri uygulamanızı yüzde yüz güvenli yapmaz; hiçbir şey yapmaz. Ancak saldırganın işini önemli ölçüde zorlaştırır ve olası bir başarılı saldırıda hasarı minimize eder. Güvenlik katmanlı bir yapıdır: birden fazla önlem bir arada çalıştığında gerçek koruma sağlanır.
Son olarak şunu vurgulamak isterim: bu yapılandırmaları uygulamadan önce test ortamında deneyin. Özellikle disable_functions listesi bazı meşru uygulamaları etkileyebilir. Logları düzenli izleyin ve güvenlik bültenlerini takip edin. PHP’nin her yeni sürümü yeni güvenlik özellikleri getiriyor; güncel kalmak savunmanızın temel taşlarından biri.
