Уровни: Начальный → Средний → Продвинутый (Enterprise-grade)
Актуальность: PHP 8.1+ (современные стандарты)
Это минимальный набор действий, который должен быть в каждом проекте. Игнорирование этих правил — 90% всех взломов.
Никогда не вставляйте переменные напрямую в SQL-строку. Даже если вы уверены в данных. Используйте PDO или MySQLi с подготовленными запросами.
<?php
// ❌ СМЕРТЕЛЬНО ОПАСНО:
$user = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $user";
// ✅ ПРАВИЛЬНО (PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
// Для динамических LIKE-запросов - используйте явное экранирование через addcslashes
$search = addcslashes($_GET['q'], '%_');
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ?");
$stmt->execute(["%$search%"]);
?>
Опасно не само значение, а контекст, куда оно попадает. Для HTML-контекста всегда используйте htmlspecialchars().
<?php
// ❌ ОПАСНО:
echo "<div>" . $_GET['comment'] . "</div>";
// ✅ БЕЗОПАСНО:
echo "<div>" . htmlspecialchars($_GET['comment'], ENT_QUOTES | ENT_HTML5, 'UTF-8') . "</div>";
// Для атрибутов HTML:
echo '<input value="' . htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8') . '">';
?>
Забудьте про md5(), sha1() и даже "соленый" md5. Используйте алгоритм, устойчивый к перебору.
<?php
// ✅ РЕГИСТРАЦИЯ:
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID); // Argon2id — лучший на 2026 год
// ✅ АУТЕНТИФИКАЦИЯ:
if (password_verify($_POST['password'], $hash_from_db)) {
// Успех — при необходимости обновите хеш на более новый
if (password_needs_rehash($hash_from_db, PASSWORD_ARGON2ID)) {
$new_hash = password_hash($_POST['password'], PASSWORD_ARGON2ID);
// сохраните $new_hash в БД
}
}
?>
Для форм, меняющих состояние (POST, PUT, DELETE), генерируйте одноразовые токены.
<?php
// Генерация токена:
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// В форме:
?>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<!-- остальные поля -->
</form>
<?php
// Проверка при отправке:
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
die('CSRF token mismatch');
}
?>
Эти методы требуют понимания инфраструктуры, но дают защиту от более сложных атак.
Никогда не доверяйте $_FILES['file']['type'] — это подделывается браузером. Проверяйте реальное содержимое файла.
<?php
function isSafeImage($filePath) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $filePath);
finfo_close($finfo);
// Белый список MIME-типов
$allowed = ['image/jpeg', 'image/png', 'image/webp'];
return in_array($mime, $allowed, true);
}
// Дополнительная защита: переименование файлов и хранение вне document_root
$newName = bin2hex(random_bytes(16)) . '.' . pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$uploadPath = '/var/secure_uploads/' . $newName; // За пределами public_html
?>
Не используйте unserialize() на пользовательских данных — это приводит к удаленному выполнению кода (RCE). Используйте json_decode() или sodium_crypto_secretbox() для подписанных данных.
<?php
// ❌ ОПАСНО (RCE через десериализацию):
$data = unserialize($_COOKIE['user_data']);
// ✅ БЕЗОПАСНО (JSON):
$data = json_decode($_COOKIE['user_data'] ?? '{}', true);
// Если нужна именно сериализация объектов — используйте подпись:
$signed = sodium_crypto_sign($serialized_data, $secret_key);
?>
Эти заголовки заставляют браузер включать встроенные механизмы защиты.
<?php
// Запрещаем браузеру MIME-сниффинг (предотвращает атаки типа "image upload to XSS")
header('X-Content-Type-Options: nosniff');
// Включаем CSP (Content Security Policy) — разрешаем только свои скрипты
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'? 'unsafe-eval'?; style-src 'self'");
// HTTP Strict Transport Security (только если у вас HTTPS)
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
// Защита от clickjacking
header('X-Frame-Options: DENY');
// Referrer Policy — не передаём referer при переходе на другой домен
header('Referrer-Policy: same-origin');
?>
<?php
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => $_SERVER['HTTP_HOST'],
'secure' => true, // только по HTTPS
'httponly' => true, // недоступны для JavaScript (защита от XSS)
'samesite' => 'Strict' // защита от CSRF
]);
session_start();
?>
Требует глубокого понимания криптографии, инфраструктуры и PHP internals. Эти методы характерны для высоконагруженных проектов, финтеха, медицинских систем.
HTTPS защищает данные при передаче, но не в базе данных. Если злоумышленник получит дамп БД — данные должны быть нечитаемы. Используйте libsodium (встроен в PHP).
<?php
// Генерация мастер-ключа (храните вне БД, например в .env или AWS KMS)
$secretKey = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
// Шифрование
function encryptSensitive($plaintext, $key) {
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$ciphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
$plaintext,
'', // дополнительные данные (можно указать ID пользователя)
$nonce,
$key
);
return base64_encode($nonce . $ciphertext);
}
// Дешифрование
function decryptSensitive($encrypted, $key) {
$decoded = base64_decode($encrypted);
$nonce = substr($decoded, 0, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$ciphertext = substr($decoded, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
return sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($ciphertext, '', $nonce, $key);
}
?>
На уровне PHP + Redis или Memcached. Используйте алгоритм "скользящего окна" или token bucket.
<?php
// Пример с Redis (токен-бакет)
$redis = new Redis();
$key = 'rate_limit:' . $_SERVER['REMOTE_ADDR'] . ':login';
$maxRequests = 10; // 10 попыток
$window = 60; // за 60 секунд
$current = $redis->get($key);
if ($current === false) {
$redis->setex($key, $window, 1);
$allow = true;
} elseif ($current < $maxRequests) {
$redis->incr($key);
$allow = true;
} else {
$allow = false;
http_response_code(429); // Too Many Requests
die('Превышен лимит попыток. Попробуйте через ' . $redis->ttl($key) . ' секунд');
}
?>
Стандартные операторы сравнения (==, ===) могут утечь информацию через время выполнения. Для секретных данных (токены, хеши) используйте hash_equals() и sodium_memcmp().
<?php
// ❌ Уязвимо к timing attack:
if ($submitted_token === $real_token) { ... }
// ✅ Константное время сравнения:
if (hash_equals($real_token, $submitted_token)) { ... }
// Для очистки чувствительных данных из памяти (после использования):
sodium_memzero($password_variable);
?>
Для критических проектов — запускайте PHP под управлением WAF. Пример с Coraza (Go-совместимый ModSecurity) или PHP-fpm + nginx + ModSecurity. На чистом PHP можно написать "примитивный WAF":
<?php
class WAF {
private static $patterns = [
'/select.*from/i' => 'SQLi attempt',
'/union.*select/i' => 'SQLi attempt',
'/\b(wget|curl|nc|bash|sh)\b/i' => 'Command injection',
'/\.\.\/|\.\.\\\/' => 'Path traversal',
'/<\?php/i' => 'Code injection',
];
public static function scan($input) {
foreach (self::$patterns as $pattern => $reason) {
if (preg_match($pattern, $input)) {
error_log("WAF blocked: $reason - Input: $input");
http_response_code(403);
die('Request blocked by security policy');
}
}
}
}
// Применять ко всем входным данным:
WAF::scan($_SERVER['REQUEST_URI']);
array_walk_recursive($_GET, 'WAF::scan');
array_walk_recursive($_POST, 'WAF::scan');
?>
<?php
// При работе с XML (например, SOAP, XML-RPC) — обязательно отключайте внешние сущности
$dom = new DOMDocument();
$dom->loadXML($xmlInput, LIBXML_NOENT | LIBXML_DTDLOAD);
// Или через SimpleXML:
$sxe = simplexml_load_string($xmlInput, 'SimpleXMLElement', LIBXML_NOENT);
// Для старых версий: libxml_disable_entity_loader(true);
?>
rand() и mt_rand() предсказуемы для криптографических целей. Всегда используйте CSPRNG.
<?php
// ❌ Предсказуемо:
$token = md5(rand(10000, 99999));
// ✅ Криптографически безопасно:
$token = bin2hex(random_bytes(32));
// Если random_bytes недоступен (старый PHP) — проверяем:
try {
$bytes = random_bytes(32);
} catch (Exception $e) {
// fallback: использовать /dev/urandom или openssl
$bytes = openssl_random_pseudo_bytes(32);
}
?>
Распечатайте и проверяйте каждый пункт при деплое.
htmlspecialchars()password_hash() с PASSWORD_ARGON2IDdisplay_errors = Off в production, ошибки пишутся в логeval(), exec(), system() с пользовательским вводомHttpOnly, Secure, SameSite=Strictrandom_bytes() для генерации токеновhash_equals()composer audit)
Комментарии
Пока нет комментариев. Будьте первым!