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

Базата данни често съдържа чувствителни данни и позволява извършването на опасни операции. За безопасна работа с Nette Database е ключово:

  • Да се разбира разликата между безопасно и опасно API
  • Да се използват параметризирани заявки
  • Да се валидират правилно входните данни

Какво е SQL Injection?

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

  • Да получи неоторизиран достъп до данни
  • Да модифицира или изтрие данни в базата данни
  • Да заобиколи автентикацията
// ❌ ОПАСЕН КОД - уязвим към SQL инжекция
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");

// Нападателят може да въведе например стойност: ' OR '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 предлага няколко начина за тяхното използване.

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

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

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

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

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

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

// ✅ Безопасен INSERT в Explorer
$table->insert([
	'name' => $name,
	'email' => $email,
]);

Валидация на стойностите на параметрите

Параметризираните заявки са основният градивен елемент за безопасна работа с базата данни. Въпреки това, стойностите, които вмъкваме в тях, трябва да преминат през няколко нива на проверка:

Проверка на типа

Най-важното е да се гарантира правилният тип данни на параметрите – това е необходимо условие за безопасното използване на Nette Database. Базата данни предполага, че всички входни данни имат правилния тип данни, съответстващ на дадената колона.

Например, ако $name в предишните примери неочаквано беше масив вместо низ, Nette Database щеше да се опита да вмъкне всички негови елементи в SQL заявката, което би довело до грешка. Затова никога не използвайте невалидирани данни от $_GET, $_POST или $_COOKIE директно в заявките към базата данни.

Проверка на формата

На второ ниво проверяваме формата на данните – например дали низовете са в UTF-8 кодиране и тяхната дължина съответства на дефиницията на колоната, или дали числовите стойности са в допустимия диапазон за дадения тип данни на колоната.

На това ниво на валидация можем частично да разчитаме и на самата база данни – много бази данни ще отхвърлят невалидни данни. Въпреки това, поведението може да варира, някои могат тихо да скъсят дълги низове или да отрежат числа извън диапазона.

Домейн проверка

Третото ниво представляват логически проверки, специфични за вашето приложение. Например, проверка дали стойностите от select полетата съответстват на предлаганите опции, дали числата са в очаквания диапазон (напр. възраст 0–150 години) или дали взаимните зависимости между стойностите имат смисъл.

Препоръчителни начини за валидация

  • Използвайте Nette Forms, които автоматично осигуряват правилната валидация на всички входове
  • Използвайте Presenters и посочвайте типовете данни за параметрите в методите action*() и render*()
  • Или реализирайте собствен слой за валидация с помощта на стандартни PHP инструменти като filter_var()

Безопасна работа с колони

В предишната секция показахме как правилно да валидираме стойностите на параметрите. При използване на масиви в SQL заявки обаче трябва да обърнем същото внимание и на техните ключове.

// ❌ ОПАСЕН КОД - ключовете в масива не са обработени
$database->query('INSERT INTO users', $_POST);

При командите INSERT и UPDATE това е критична грешка в сигурността – нападателят може да вмъкне или промени всяка колона в базата данни. Може например да зададе is_admin = 1 или да вмъкне произволни данни в чувствителни колони (т.нар. Mass Assignment Vulnerability).

В условията 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']);
версия: 4.0