<?php
/**
* Gelişmiş Güvenlik Sınıfı (v2.0)
* OWASP Top 10 + Ek 6 Açık = Toplam 16 Açık Koruması
* SQL Injection Koruması: Dahili PDO Prepared Statements
*/
// Doğrudan erişimi engelle
if (!defined('CMS_ROOT')) {
die('Direct access denied');
}
class Security {
/**
* @var PDO|null Veritabanı bağlantı nesnesi
*/
private static $pdoInstance = null;
// ==========================================
// 1. SQL INJECTION KORUMASI (PDO WRAPPER)
// ==========================================
/**
* Sınıfın SQL işlemleri yapabilmesi için PDO nesnesini tanıtır.
* Kullanımı: Security::setPdo($pdo);
*/
public static function setPdo($pdo) {
if ($pdo instanceof PDO) {
self::$pdoInstance = $pdo;
} else {
self::logSecurityEvent('PDO_ERROR', 'critical', 'Geçersiz PDO nesnesi verildi.');
}
}
/**
* GÜVENLİ SQL SORGUSU
* Sadece bu metodu kullanarak veritabanına veri gönderin. Asla string birleştirme yapmayın.
*
* Örnek Kullanım:
* $kullanici = Security::safeQuery("SELECT * FROM users WHERE id = ? AND status = ?", [$id, 1])->fetch();
*/
public static function safeQuery($sql, $params = []) {
if (self::$pdoInstance === null) {
self::logSecurityEvent('PDO_MISSING', 'critical', 'PDO bağlantısı ayarlanmadan SQL sorgusu çalıştırılmaya çalışıldı.');
die('Sistem Hatası: Veritabanı bağlantısı güvenlik katmanına tanımlanmamış.');
}
try {
$stmt = self::$pdoInstance->prepare($sql);
$stmt->execute($params);
return $stmt;
} catch (PDOException $e) {
self::logSecurityEvent('SQL_ERROR', 'critical', $e->getMessage() . ' | SQL: ' . $sql);
// Canlı sitede detaylı hata gösterme
die('Veritabanı işlemi sırasında bir hata oluştu.');
}
}
// ==========================================
// 2. XSS KORUMASI (Çıktı Encoding)
// ==========================================
public static function escapeOutput($str) {
if (is_array($str)) {
return array_map([self::class, 'escapeOutput'], $str);
}
return htmlspecialchars((string)$str, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// ==========================================
// 3. CSRF TOKEN YÖNETİMİ
// ==========================================
public static function generateCsrfToken() {
$lifetime = defined('CSRF_TOKEN_LIFETIME') ? CSRF_TOKEN_LIFETIME : 3600;
if (empty($_SESSION['csrf_token']) || empty($_SESSION['csrf_token_time']) || time() - $_SESSION['csrf_token_time'] > $lifetime) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$_SESSION['csrf_token_time'] = time();
}
return $_SESSION['csrf_token'];
}
public static function verifyCsrfToken($token) {
if (empty($_SESSION['csrf_token']) || empty($token)) {
return false;
}
$lifetime = defined('CSRF_TOKEN_LIFETIME') ? CSRF_TOKEN_LIFETIME : 3600;
if (time() - $_SESSION['csrf_token_time'] > $lifetime) {
unset($_SESSION['csrf_token'], $_SESSION['csrf_token_time']);
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
public static function csrfField() {
$token = self::generateCsrfToken();
return '<input type="hidden" name="csrf_token" value="' . self::escapeOutput($token) . '">';
}
// ==========================================
// 4. SESSION FIXATION & HIJACKING KORUMASI
// ==========================================
public static function secureSessionStart() {
if (session_status() === PHP_SESSION_NONE) {
$fingerprint = self::getFingerprint();
session_start();
// Oturum ID'sini yenile (Fixation engelle)
if (!isset($_SESSION['initiated'])) {
session_regenerate_id(true);
$_SESSION['initiated'] = true;
$_SESSION['fingerprint'] = $fingerprint;
}
// Tarayıcı parmak izi kontrolü (Hijacking engelle)
if ($_SESSION['fingerprint'] !== $fingerprint) {
self::destroySession();
self::forceLogout('Oturum doğrulaması başarısız (Fingerprint mismatch).');
}
// Oturum süresi kontrolü
$lifetime = defined('SESSION_LIFETIME') ? SESSION_LIFETIME : 1800;
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > $lifetime) {
self::destroySession();
self::forceLogout('Oturum süresi doldu.');
}
$_SESSION['last_activity'] = time();
}
}
private static function getFingerprint() {
$parts = [
$_SERVER['HTTP_USER_AGENT'] ?? '',
substr($_SERVER['REMOTE_ADDR'] ?? '', 0, 7),
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
];
return hash('sha256', implode('|', $parts));
}
public static function destroySession() {
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
}
session_destroy();
}
public static function forceLogout($message) {
self::destroySession();
$siteUrl = defined('SITE_URL') ? SITE_URL : '/';
header('Location: ' . $siteUrl . '/admin/?hata=' . urlencode($message));
exit;
}
// ==========================================
// 5. BRUTE FORCE KORUMASI
// ==========================================
public static function checkBruteForce($identifier, $pdo = null) {
$db = $pdo ?? self::$pdoInstance;
if (!$db) return false; // DB yoksa kontrolü atla
$duration = defined('LOCKOUT_DURATION') ? LOCKOUT_DURATION : 900;
$max = defined('MAX_LOGIN_ATTEMPTS') ? MAX_LOGIN_ATTEMPTS : 5;
$prefix = defined('DB_PREFIX') ? DB_PREFIX : '';
$stmt = $db->prepare(
"SELECT COUNT(*) FROM " . $prefix . "login_attempts
WHERE identifier = ? AND attempt_time > (NOW() - INTERVAL " . intval($duration) . " SECOND)
AND successful = 0"
);
$stmt->execute([$identifier]);
return (int)$stmt->fetchColumn() >= $max;
}
public static function logLoginAttempt($identifier, $successful, $pdo = null) {
$db = $pdo ?? self::$pdoInstance;
if (!$db) return;
$ip = self::getClientIp();
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$prefix = defined('DB_PREFIX') ? DB_PREFIX : '';
$stmt = $db->prepare(
"INSERT INTO " . $prefix . "login_attempts (identifier, ip_address, user_agent, successful, attempt_time)
VALUES (?, ?, ?, ?, NOW())"
);
$stmt->execute([$identifier, $ip, $ua, $successful ? 1 : 0]);
}
public static function clearLoginAttempts($identifier, $pdo = null) {
$db = $pdo ?? self::$pdoInstance;
if (!$db) return;
$prefix = defined('DB_PREFIX') ? DB_PREFIX : '';
$stmt = $db->prepare("DELETE FROM " . $prefix . "login_attempts WHERE identifier = ?");
$stmt->execute([$identifier]);
}
// ==========================================
// 6. DIRECTORY TRAVERSAL KORUMASI
// ==========================================
public static function sanitizePath($path) {
$path = str_replace(['../', '..\\', "\0"], '', $path);
$real = realpath($path);
if ($real === false || !str_starts_with($real, CMS_ROOT)) {
return false;
}
return $real;
}
// ==========================================
// 7. DOSYA YÜKLEME GÜVENLİĞİ
// ==========================================
public static function validateUpload($file) {
if (!isset($file) || $file['error'] !== UPLOAD_ERR_OK) {
return ['valid' => false, 'error' => 'Dosya yükleme hatası.'];
}
$maxSize = defined('UPLOAD_MAX_SIZE') ? UPLOAD_MAX_SIZE : 2097152;
if ($file['size'] > $maxSize) {
return ['valid' => false, 'error' => 'Dosya boyutu çok büyük (Maks ' . round($maxSize/1048576) . 'MB).'];
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowedExt = defined('ALLOWED_EXTENSIONS') ? ALLOWED_EXTENSIONS : ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
if (!in_array($ext, $allowedExt)) {
return ['valid' => false, 'error' => 'Bu dosya türüne izin verilmiyor.'];
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowedMime = defined('ALLOWED_MIME_TYPES') ? ALLOWED_MIME_TYPES : ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (!in_array($mime, $allowedMime)) {
return ['valid' => false, 'error' => 'Geçersiz dosya içeriği algılandı.'];
}
if (strpos($mime, 'image/') !== false) {
if (@getimagesize($file['tmp_name']) === false) {
return ['valid' => false, 'error' => 'Geçersiz görsel dosyası.'];
}
}
if (count(explode('.', $file['name'])) > 2) {
return ['valid' => false, 'error' => 'Çift uzantılı dosyalara izin verilmiyor.'];
}
return ['valid' => true, 'ext' => $ext, 'mime' => $mime];
}
public static function safeFileName($originalName) {
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
return bin2hex(random_bytes(8)) . '_' . time() . '.' . $ext;
}
// ==========================================
// 8. COMMAND INJECTION KORUMASI
// ==========================================
public static function sanitizeCommand($input) {
$dangerous = ['&', '|', ';', '`', '$', '(', ')', '{', '}', '[', ']', '<', '>', '!', "\n", "\r"];
return str_replace($dangerous, '', $input);
}
// ==========================================
// 9. LDAP INJECTION KORUMASI
// ==========================================
public static function sanitizeLdap($input) {
$dangerous = ['\\', '*', '(', ')', "\x00", '&', '|', '!', '=', '<', '>'];
return str_replace($dangerous, '', $input);
}
// ==========================================
// 10. XML/XPATH INJECTION KORUMASI
// ==========================================
public static function sanitizeXml($input) {
// PHP 8.0+ libxml_disable_entity_loader kaldırıldı, ama eski sürümler için
if (PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader(true);
}
libxml_use_internal_errors(true);
return htmlspecialchars($input, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}
// ==========================================
// 11. SSRF KORUMASI
// ==========================================
public static function validateUrl($url) {
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return false;
}
$parsed = parse_url($url);
$host = $parsed['host'] ?? '';
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return false;
}
if (!in_array(strtolower($parsed['scheme'] ?? ''), ['http', 'https'])) {
return false;
}
return true;
}
// ==========================================
// 12. OPEN REDIRECT KORUMASI
// ==========================================
public static function sanitizeRedirect($url) {
$siteUrl = defined('SITE_URL') ? SITE_URL : '/';
if (empty($url)) return $siteUrl;
if (str_starts_with($url, '/')) return $siteUrl . $url;
if (str_starts_with($url, $siteUrl)) return $url;
return $siteUrl;
}
// ==========================================
// 13. HTTP RESPONSE SPLITTING KORUMASI
// ==========================================
public static function sanitizeHeader($value) {
return str_replace(["\r", "\n", "%0a", "%0d", "\0"], '', $value);
}
// ==========================================
// 14. MASS ASSIGNMENT KORUMASI (DÜZELTİLDİ)
// ==========================================
/**
* Dizi içindeki verilerden SADECE izin verilen anahtarları dışarı çıkarır.
* Örnek: Security::filterFields($_POST, ['username', 'email']);
*/
public static function filterFields($data, $allowedFields) {
if (!is_array($data) || !is_array($allowedFields)) {
return [];
}
return array_filter(
$data,
fn($key) => in_array($key, $allowedFields),
ARRAY_FILTER_USE_KEY
);
}
// ==========================================
// 15. CLICKJACKING (Açıklama)
// ==========================================
// Clickjacking koruması .htaccess içinde şu şekilde yapılmalıdır:
// Header always append X-Frame-Options SAMEORIGIN
// Header always append X-Content-Type-Options nosniff
// ==========================================
// 16. SECURITY MISCONFIGURATION (PHP HARDENING)
// ==========================================
public static function hardenPhp() {
ini_set('expose_php', 0);
ini_set('allow_url_include', 0);
// NOT: allow_url_fopen'ı kapatmak API isteklerini (file_get_contents vs.) bozabilir.
// Eğer kesinlikle dışarıdan URL çağırmıyorsanız açabilirsiniz: ini_set('allow_url_fopen', 0);
ini_set('display_errors', (defined('DEBUG_MODE') && DEBUG_MODE) ? 1 : 0);
ini_set('log_errors', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.use_strict_mode', 1);
// Sitede SSL varsa çerezleri de şifrele
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
ini_set('session.cookie_secure', 1);
}
}
// ==========================================
// YARDIMCI FONKSİYONLAR
// ==========================================
/**
* Güvenli IP Alma (Proxy / Cloudflare desteği ile)
*/
public static function getClientIp() {
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP']) && filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) {
return $_SERVER['HTTP_CF_CONNECTING_IP']; // Cloudflare
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($ips[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; // Load Balancer
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
/**
* Genel Girdi Temizliği (XSS ve Null Byte için ön filtre)
* Not: Bu sadece ön temizliktir. Veritabanına eklerken safeQuery, ekrana basarken escapeOutput KULLANIN.
*/
public static function cleanInput($input) {
if (is_array($input)) {
return array_map([self::class, 'cleanInput'], $input);
}
$input = trim($input);
$input = stripslashes($input);
$input = str_replace("\0", '', $input); // Null byte temizle
return $input;
}
/**
* Parola Hashleme (Argon2id)
*/
public static function hashPassword($password) {
return password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
]);
}
public static function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}
/**
* AES-256-CBC Şifreleme / Çözme
*/
public static function encrypt($data) {
$key = defined('ENCRYPTION_KEY') ? hash('sha256', ENCRYPTION_KEY, true) : hash('sha256', 'varsayilan-gizli-anahtar', true);
$iv = random_bytes(16);
$encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $encrypted);
}
public static function decrypt($data) {
$key = defined('ENCRYPTION_KEY') ? hash('sha256', ENCRYPTION_KEY, true) : hash('sha256', 'varsayilan-gizli-anahtar', true);
$data = base64_decode($data);
$iv = substr($data, 0, 16);
$decrypted = openssl_decrypt(substr($data, 16), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return $decrypted !== false ? $decrypted : null;
}
/**
* Rate Limiting (Dosya tabanlı)
*/
public static function rateLimit($key, $maxRequests = 60, $windowSeconds = 60) {
$file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'rl_' . md5($key);
$now = time();
$data = @json_decode(@file_get_contents($file), true) ?: ['count' => 0, 'start' => $now];
if ($now - $data['start'] > $windowSeconds) {
$data = ['count' => 1, 'start' => $now];
} else {
$data['count']++;
}
@file_put_contents($file, json_encode($data), LOCK_EX);
if ($data['count'] > $maxRequests) {
http_response_code(429);
die('Çok fazla istek. Lütfen ' . $windowSeconds . ' saniye bekleyin.');
}
}
/**
* Güvenlik Loglama
*/
public static function logSecurityEvent($event, $level = 'info', $details = '') {
$logDir = CMS_ROOT . DIRECTORY_SEPARATOR . 'logs';
if (!is_dir($logDir)) {
@mkdir($logDir, 0700, true);
}
$logFile = $logDir . DIRECTORY_SEPARATOR . 'security_' . date('Y-m-d') . '.log';
$entry = sprintf(
"[%s] [%s] IP: %s | Event: %s | Details: %s\n",
date('Y-m-d H:i:s'),
strtoupper($level),
self::getClientIp(),
$event,
self::escapeOutput($details)
);
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
}
}
?>