Риск за сигурността

Базите данни често съдържат чувствителни данни и позволяват опасни операции. Базата данни Nette предоставя редица функции за сигурност. От решаващо значение е обаче да се разбере разликата между безопасни и опасни API.

SQL инжектиране

SQL инжектирането е най-сериозният риск за сигурността при работа с бази данни. То възниква, когато непроверен потребителски вход стане част от SQL заявка. Атакуващият може да инжектира свои собствени SQL команди, като по този начин получава или променя данни в базата данни.

// ❌ БЕЗОПАСЕН КОД - уязвим към SQL инжекция
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");

// Нападателят може да въведе нещо подобно на: ' ИЛИ '1'='1
// Получената заявка ще бъде:
// SELECT * FROM users WHERE name = '' OR '1'='1'
// Това връща всички потребители!

Същото се отнася и за Database Explorer:

// ❌ КОД ЗА БЕЗОПАСНОСТ
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");

Безопасни параметризирани заявки

Безопасният начин за вмъкване на стойности в SQL заявките е чрез параметризирани заявки. Базата данни Nette предоставя няколко начина за използването им.

Заместващи въпросителни знаци

Най-простият метод е да се използват заместващи въпросителни знаци:

// ✅ Безопасни параметризирани заявки
$database->query('SELECT * FROM users WHERE name = ?', $_GET['name']);

// ✅ Безопасно условие в Explorer
$table->where('name = ?', $_GET['name']);

Същото важи и за всички други методи в Database Explorer, които позволяват вмъкване на изрази с въпросителни знаци и параметри.

Стойностите трябва да са от скаларен тип (string, int, float, bool) или null. Ако например $_GET['name'] е масив, Nette Database ще включи всички негови елементи в SQL заявката, което може да е нежелателно.

Масиви от стойности

За клаузите INSERT, UPDATE или WHERE можем да използваме масиви от стойности:

// ✅ Безопасен INSERT
$database->query('INSERT INTO users', [
	'name' => $_GET['name'],
	'email' => $_GET['email'],
]);

// ✅ Безопасен UPDATE
$database->query('UPDATE users SET', [
	'name' => $_GET['name'],
	'email' => $_GET['email'],
], 'WHERE id = ?', $_GET['id']);

Nette Database автоматично ескапира всички стойности, предадени чрез параметризирани заявки. Трябва обаче да осигурим правилния тип данни на параметрите.

Ключовете на масива не са сигурен API

Докато стойностите в масивите са безопасни, същото не може да се каже за ключовете:

// ❌ БЕЗОПАСЕН КОД - ключовете могат да съдържат SQL инжекция
$database->query('INSERT INTO users', $_GET);
$database->query('SELECT * FROM users WHERE', $_GET);
$table->where($_GET);

За командите INSERT и UPDATE това е критичен недостатък в сигурността – атакуващият може да вмъкне или промени всяка колона в базата данни. Например, той може да зададе is_admin = 1 или да вмъкне произволни данни в чувствителни колони.

В условията на WHERE това е още по-опасно, защото позволява SQL enumeration – техника за постепенно извличане на информация за базата данни. Нападателят може да се опита да проучи заплатите на служителите, като инжектира в $_GET по този начин:

$_GET = ['salary >', 100000];   // започва да определя диапазони на заплатите.

Основният проблем обаче е, че WHERE условията поддържат SQL изрази в ключовете:

// Законосъобразно използване на оператори в ключовете
$table->where([
    'age > ?' => 18,
    'ROUND(score, ?) > ?' => [2, 75.5],
]);

// ❌ БЕЗОПАСНО: атакуващият може да инжектира свой собствен SQL
$_GET = ['1) UNION SELECT name, salary FROM users WHERE (is_admin = ?' => 1];
$table->where($_GET); // позволява на нападателя да получи администраторски заплати

Това отново е SQL инжекция.

Създаване на бял списък на колоните

Ако искате да разрешите на потребителите да избират колони, винаги използвайте бял списък:

// ✅ Безопасна обработка - само разрешени колони
$allowedColumns = ['name', 'email', 'active'];
$values = array_intersect_key($_GET, array_flip($allowedColumns));

$database->query('INSERT INTO users', $values);

Динамични идентификатори

За динамични имена на таблици и колони използвайте заместителя ?name:

// ✅ Безопасно използване на надеждни идентификатори
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);

// ❌ UNSAFE - никога не използвайте потребителски вход
$database->query('SELECT ?name FROM users', $_GET['column']);

Символът ?name трябва да се използва само за доверени стойности, дефинирани в кода на приложението. За стойности, предоставени от потребителя, отново използвайте бял списък.

версия: 4.0