Рискове за сигурността
Базите данни често съдържат чувствителни данни и позволяват извършването на опасни операции. За сигурна работа с Nette Database основните аспекти са:
- Разбиране на разликата между сигурен и несигурен 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:
// ❌ ОПАСЕН КОД - уязвим към SQL инжекция
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Сигурни параметризирани заявки
Основната защита срещу SQL инжектиране са параметризираните заявки. Базата данни Nette предоставя няколко начина за използването им.
Най-простият начин е да се използват заместващи знаци за въпроси:
// ✅ Сигурна параметризирана заявка
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Защитено условие в Explorer
$table->where('name = ?', $name);
Това важи за всички други методи в Database Explorer, които позволяват вмъкване на изрази със заместители с въпросителни знаци и параметри.
За клаузите INSERT
, UPDATE
или WHERE
можете да предадете
стойности в масив:
// ✅ Secure INSERT
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ Secure INSERT в Explorer
$table->insert([
'name' => $name,
'email' => $email,
]);
Стойност на параметъра Утвърждаване
Заявките с параметри са в основата на сигурната работа с бази данни. Стойностите, предавани в тях, обаче трябва да преминат през няколко нива на валидиране:
Проверка на типа
Осигуряването на правилния тип данни на параметрите е от решаващо значение – това е необходимо условие за безопасното използване на базата данни Nette. Базата данни приема, че всички входни данни имат правилния тип данни, съответстващ на колоната.
Например, ако $name
в предишните примери неочаквано се превърне
в масив вместо в низ, Nette Database ще се опита да вмъкне всички негови
елементи в SQL заявката, което ще доведе до грешка. Ето защо никога не
използвайте невалидирани данни от $_GET
, $_POST
или
$_COOKIE
директно в заявки към базата данни.
Утвърждаване на формата
Второто ниво проверява формата на данните – например дали низовете са кодирани в UTF-8 и дали дължината им съответства на дефиницията на колоната, или дали цифровите стойности попадат в допустимия диапазон за типа данни на колоната.
На това ниво можете частично да разчитате на самата база данни – много бази данни отхвърлят невалидни данни. Поведението обаче може да варира: някои могат да съкращават дълги низове без звук или да изрязват числа, които са извън обхвата.
Специфично за домейна валидиране
Третото ниво включва логически проверки, специфични за вашето приложение. Например проверка дали стойностите от полета за избор съответстват на наличните опции, дали числата попадат в очакван диапазон (например възраст 0–150 години) или дали връзките между стойностите имат смисъл.
Препоръчителни методи за валидиране
- Използвайте формуляри на Nette, които автоматично се справят с правилното валидиране на всички входни данни.
- Използвайте Presenters и декларирайте
типовете данни на параметрите в методите
action*()
иrender*()
. - Или имплементирайте собствен слой за валидиране, като използвате
стандартни инструменти на PHP като
filter_var()
.
Безопасна работа с колони
В предишния раздел разгледахме как да валидираме правилно стойностите на параметрите. Когато обаче използвате масиви в SQL заявки, трябва да се обърне еднакво внимание на техните ключове.
// ❌ ОПАСЕН КОД - ключовете на масивите не са обработени
$database->query('INSERT INTO users', $_POST);
За командите INSERT и UPDATE това е сериозен пропуск в сигурността –
атакуващият може да вмъкне или промени всяка колона в базата данни. Той
може например да зададе is_admin = 1
или да вмъкне произволни данни в
чувствителни колони (известно като уязвимост при масово задаване).
При условията WHERE това е още по-опасно, тъй като те могат да съдържат оператори:
// ❌ ОПАСЕН КОД - ключовете на масивите не са обработени
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// Изпълнява заявка WHERE (`salary` > 100000)
Атакуващият може да използва този подход, за да разкрива систематично заплатите на служителите. Той може да започне със заявка за заплати над 100 000, след това под 50 000 и чрез постепенно стесняване на обхвата да разкрие приблизителните заплати на всички служители. Този тип атака се нарича SQL enumeration.
Методите where()
и whereOr()
са още по-гъвкави и поддържат SQL изрази,
включително оператори и функции, както в ключовете, така и в
стойностите. Това дава възможност на нападателя да извършва сложни SQL
инжекции:
// ❌ ОПАСЕН КОД - атакуващият може да вмъкне свой собствен SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// изпълнява заявка WHERE (0) UNION SELECT name, salary FROM users WHERE (1)
Тази атака прекратява оригиналното условие с 0)
, добавя своя
собствена SELECT
с помощта на UNION
, за да получи чувствителни
данни от таблицата users
, и приключва със синтактично правилна
заявка с помощта на WHERE (1)
.
Бял списък на колони
За безопасна работа с имена на колони е необходим механизъм, който да гарантира, че потребителите могат да взаимодействат само с разрешените колони и не могат да добавят свои собствени. Опитите за откриване и блокиране на опасни имена на колони (създаване на черен списък) са ненадеждни – нападателят винаги може да измисли нов начин за записване на опасно име на колона, който не сте предвидили.
Затова е много по-безопасно да обърнете логиката и да дефинирате изричен списък на разрешените колони (бял списък):
// Колони, които потребителят има право да модифицира
$allowedColumns = ['name', 'email', 'active'];
// Премахване на всички неразрешени колони от входа
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// ✅ Вече е безопасно да се използва в заявки, като например:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Динамични идентификатори
За динамични имена на таблици и колони използвайте заместителя
?name
. Това гарантира правилното ескапиране на идентификаторите в
съответствие с дадения синтаксис на базата данни (например използване
на задни тирета в MySQL):
// ✅ Безопасно използване на надеждни идентификатори
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Резултат в MySQL: SELECT `name` FROM `users`
Важно: Използвайте символа ?name
само за доверени стойности,
дефинирани в кода на приложението. За стойности, предоставени от
потребителя, отново използвайте бял списък. В
противен случай рискувате уязвимости в сигурността:
// ❌ ОПАСНО - никога не използвайте потребителски вход
$database->query('SELECT ?name FROM users', $_GET['column']);