Чи готові заяви PDO, щоб запобігти введенню SQL?


661

Скажімо, у мене такий код:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Документація PDO говорить:

Параметри підготовлених висловлювань не потрібно цитувати; водій обробляє це за вас.

Це справді все, що мені потрібно зробити, щоб уникнути ін'єкцій SQL? Невже це так просто?

Ви можете припустити MySQL, якщо це має значення. Крім того, мені цікаво лише використання підготовлених операторів проти ін'єкції SQL. У цьому контексті мене не хвилює XSS чи інші можливі вразливості.


5
Краще сьомий номер відповіді stackoverflow.com/questions/134099 / ...
NullPoiіteя

Відповіді:


807

Коротка відповідь - НІ , підготовка PDO не захистить вас від усіх можливих атак SQL-ін'єкції. Для деяких незрозумілих крайових випадків.

Я адаптую цю відповідь, щоб поговорити про PDO ...

Довга відповідь не така проста. Він заснований на нападах, продемонстрованих тут .

Атака

Отже, почнемо, показуючи атаку ...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

За певних обставин це поверне більше 1 ряду. Давайте розберемо, що тут відбувається:

  1. Вибір набору символів

    $pdo->query('SET NAMES gbk');

    Щоб ця атака спрацювала, нам потрібно кодування, яке сервер очікує на з'єднання як для кодування, 'так і в ASCII, тобто, 0x27 і мати якийсь символ, кінцевим байтом якого є ASCII, \тобто 0x5c. Як з'ясовується, є 5 таких кодувань , підтримуваних в MySQL 5.6 за замовчуванням: big5, cp932, gb2312, gbkі sjis. Ми виберемо gbkтут.

    Тепер дуже важливо відзначити використання SET NAMESтут. Це встановлює набір символів НА СЕРВЕРІ . Є ще один спосіб зробити це, але ми досить швидко потрапимо туди.

  2. Корисний вантаж

    Корисне навантаження, яке ми будемо використовувати для цього введення, починається з послідовності байтів 0xbf27. В gbk, це недійсний багатобайтовий символ; в latin1, це рядок ¿'. Зверніть увагу , що в latin1 і gbk , 0x27само по собі є буквальним 'характер.

    Ми вибрали це корисне навантаження, оскільки, якби ми зателефонували addslashes()на нього, ми вставимо ASCII, \тобто 0x5cперед 'символом. Таким чином, ми закінчилися 0xbf5c27, що gbkє послідовністю двох символів: 0xbf5cдалі 0x27. Або іншими словами, дійсний символ, за яким слідує непризначений '. Але ми не використовуємо addslashes(). Тож до наступного кроку ...

  3. $ stmt-> Execute ()

    Тут важливо усвідомити, що PDO за замовчуванням НЕ робить справжні підготовлені заяви. Він імітує їх (для MySQL). Отже, PDO внутрішньо будує рядок запиту, викликаючи mysql_real_escape_string()(функція API MySQL C) на кожне пов'язане значення рядка.

    Виклик API API mysql_real_escape_string()відрізняється від того, addslashes()що він знає набір символів з'єднання. Таким чином, він може виконати втечу належним чином для набору символів, якого очікує сервер. Однак до цього часу клієнт вважає, що ми все ще використовуємо latin1для з'єднання, оскільки ми ніколи не говорили про це інакше. Ми сказали серверу, який ми використовуємо gbk, але клієнт все ще вважає, що це latin1.

    Тому заклик mysql_real_escape_string()вставляти звороту косу рису, і ми маємо вільний висячий 'персонаж у нашому "униклому" вмісті! Справді, якби ми повинні були дивитися на $varв gbkнаборі символів, ми бачимо:

    縗 'АБО 1 = 1 / *

    А саме цього вимагає атака.

  4. Запит

    Ця частина є лише формальністю, але ось наданий запит:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

Вітаємо, щойно ви успішно атакували програму, використовуючи готові заяви PDO ...

Просте виправлення

Тепер варто зауважити, що ви можете запобігти цьому, відключивши емульовані підготовлені заяви:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Це зазвичай призводить до істинного підготовлену заяву (тобто дані, посланого через в окремому пакеті з запиту). Однак майте на увазі, що PDO буде мовчки відновлюватись до емуляції висловлювань, які MySQL не може самостійно підготувати: ті, які вони можуть бути перелічені в посібнику, але будьте обережні, щоб вибрати відповідну версію сервера).

Правильне виправлення

Проблема тут полягає в тому, що ми не викликали C API mysql_set_charset()замість SET NAMES. Якби ми це зробили, нам було б добре, якщо ми використовуємо реліз MySQL з 2006 року.

Якщо ви використовуєте більш ранню версію MySQL, потім помилку в mysql_real_escape_string()вигляді , що неприпустимі символи мультибайтних , такі як в наших корисних навантаженнях розглядалися як окремі байти для втечі цілей , навіть якщо клієнт був правильно поінформований про кодування з'єднання і тому ця атака буде все-таки досягти успіху. Помилка була виправлена ​​в MySQL 4.1.20 , 5.0.22 та 5.1.11 .

Але найгірше те, що PDOне виставляв API C mysql_set_charset()до 5.3.6, тому в попередніх версіях він не може запобігти цій атаці для кожної можливої ​​команди! Зараз це відкрито як параметр DSN , який слід використовувати замість SET NAMES ...

Збережуюча благодать

Як ми говорили на початку, для цієї атаки спрацьовує з'єднання з базою даних, використовуючи вразливий набір символів. неutf8mb4 є вразливим, але все ж може підтримувати кожен символ Unicode: ви можете використовувати це замість цього, але він доступний лише з MySQL 5.5.3. Альтернативою є utf8, яка також не є вразливою і може підтримувати всю багатомовну площину Unicode Basic .

Крім того, ви можете ввімкнути NO_BACKSLASH_ESCAPESрежим SQL, який (серед іншого) змінює роботу mysql_real_escape_string(). Якщо цей режим увімкнутий, 0x27він буде замінений, 0x2727а не, 0x5c27і, таким чином, процес виходу не може створити дійсні символи в жодному з уразливих кодувань, де вони не існували раніше (тобто 0xbf27є і 0xbf27т. Д.) - тому сервер все одно буде відкидати рядок як недійсний . Однак див. Відповідь @ eggyal щодо іншої вразливості, яка може виникнути при використанні цього режиму SQL (хоча і не з PDO).

Безпечні приклади

Наступні приклади безпечні:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Тому що сервер очікує utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Тому що ми правильно встановили набір символів таким чином, щоб клієнт та сервер відповідали.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Тому що ми вимкнули наслідувані підготовлені заяви.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Тому що ми правильно встановили набір символів.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Тому що MySQLi весь час робить дійсно підготовлені заяви.

Підведенню

Якщо ви:

  • Використовуйте сучасні версії MySQL (кінець 5.1, усі 5.5, 5.6 тощо) та параметр DSN-діаграми PDO (в PHP ≥ 5.3.6)

АБО

  • Не використовуйте вразливий набір символів для кодування з'єднання (ви використовуєте лише utf8/ latin1/ ascii/ тощо)

АБО

  • Увімкнути NO_BACKSLASH_ESCAPESрежим SQL

Ви на 100% безпечні.

В іншому випадку ви вразливі, навіть якщо використовуєте заявки, підготовлені PDO ...

Додаток

Я повільно працюю над патчем, щоб змінити типовий параметр, щоб не емулювати підготовку до майбутньої версії PHP. Проблема, з якою я стикаюся, полягає в тому, що багато тестів ламаються, коли я це роблю. Одна з проблем полягає в тому, що емульована підготовка видасть лише синтаксичні помилки при виконанні, але справжня підготовка викидає помилки під час підготовки. Тож це може спричинити проблеми (і це є причиною того, що тести нудні).


47
Це найкраща відповідь, яку я знайшов ..може ви надаєте посилання для отримання додаткової довідки?
StaticVariable

1
@nicogawenda: це була інша помилка. До 5.0.22 mysql_real_escape_stringне було б належним чином обробляти випадки, коли для з'єднання було встановлено належним чином BIG5 / GBK. Тому насправді навіть дзвінок mysql_set_charset()на mysql <5.0.22 буде вразливим для цієї помилки! Тож ні, ця публікація все ще застосовна до 5.0.22 (адже mysql_real_escape_string - це лише набір шаблонів на дзвінки mysql_set_charset(), через що ця публікація говорить про обхід) ...
ircmaxell

1
@progfa Незалежно від того, слід завжди перевіряти свої дані на сервері, перш ніж робити щось із даними користувача.
Текст

2
Зверніть увагу, що NO_BACKSLASH_ESCAPESтакож можна ввести нові вразливості: stackoverflow.com/a/23277864/1014813
lepix

2
@slevin "АБО 1 = 1" - це заповнювач будь-якого місця. Так, він шукає значення в імені, але уявіть, що "OR 1 = 1" частина була "UNION SELECT * FROM users". Тепер ви керуєте запитом, і як такий можете зловживати ним ...
ircmaxell

515

Підготовлені висловлювання / параметризовані запити, як правило, достатньо для запобігання введенню 1-го порядку в цей оператор * . Якщо ви використовуєте неперевірений динамічний sql в будь-якому іншому місці у вашій програмі, ви все ще вразливі до введення другого порядку .

Ін'єкція 2-го порядку означає, що дані були процизовані через базу даних один раз перед включенням у запит, і їх набагато важче зняти. AFAIK, ви майже ніколи не бачите справжніх інженерних атак 2-го порядку, оскільки нападникам, як правило, простіше, щоб соціальний інженер пробився, але іноді помилки 2-го порядку вистрибуються через зайві доброякісні 'символи чи подібні.

Ви можете здійснити атаку ін'єкції 2-го порядку, коли ви можете спричинити збереження значення в базі даних, яке згодом буде використано як літерал у запиті. Для прикладу, скажімо, ви вводите таку інформацію як своє нове ім’я користувача під час створення облікового запису на веб-сайті (якщо при цьому запитати MySQL DB):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

Якщо немає інших обмежень для імені користувача, підготовлена ​​операція все одно переконається, що вищевказаний вбудований запит не виконується під час вставки, і зберігає значення правильно в базі даних. Однак уявіть, що пізніше додаток отримує ваше ім'я користувача з бази даних і використовує рядкове з'єднання для включення цього значення в новий запит. Ви можете побачити чужий пароль. Оскільки перші кілька імен у таблиці користувачів, як правило, є адміністраторами, ви, можливо, також просто подарували ферму. (Також зауважте: це ще одна причина не зберігати паролі у простому тексті!)

Ми бачимо, що підготовлені заяви досить для одного запиту, але самі по собі вони НЕ достатні для захисту від SQL - ін'єкцій на протязі всього програми, тому що у них немає механізму для забезпечення все доступу до бази даних всередині програми використовує безпечні код. Однак, що використовується як частина гарного дизайну додатків - яка може включати такі практики, як огляд коду або статичний аналіз, або використання ORM, рівня даних або рівня обслуговування, що обмежує динамічний sql - підготовлені оператори є основним інструментом для вирішення інжекції Sql проблема.Якщо ви керуєтесь принципами належного дизайну додатків, таким чином, щоб ваш доступ до даних був відокремлений від решти програми, легко застосувати або перевірити, що кожен запит правильно використовує параметризацію. У цьому випадку ін'єкція sql (як першого, так і другого порядку) повністю запобігається.


* Виявляється, MySql / PHP (гаразд, були) просто нерозумні щодо обробки параметрів, коли задіяні широкі символи, і все ще є рідкісний випадок, викладений в іншій високоголосовій відповіді тут, який може дозволити ін'єкції проскочити через параметризований запит.


6
Це цікаво. Мені не було відомо 1-го проти 2-го порядку. Чи можете ви детальніше розібратися, як працює другий порядок?
Марк Бейк

193
Якщо ВСІ ваші запити параметризовані, ви також захищені від введення другого порядку. Ін'єкція 1-го порядку забуває, що дані користувача недостовірні. Інжекція 2-го порядку забуває, що дані бази даних недостовірні (адже вони надійшли від користувача спочатку).
CJM

6
Спасибі cjm. Я також вважаю цю статтю корисною для пояснення ін’єкцій 2-го порядку: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Марк Бік,

49
Ага, так. А як щодо ін'єкції третього порядку . Треба знати про це.
troelskn

81
@troelskn, де розробник є джерелом недостовірних даних
MikeMurko

45

Ні, вони не завжди.

Це залежить від того, чи дозволяєте ви вводити користувацькі дані в межах самого запиту. Наприклад:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

буде вразливим до ін'єкцій SQL і використання підготовлених операторів у цьому прикладі не буде працювати, оскільки введення користувача використовується як ідентифікатор, а не як дані. Правильною відповіддю тут буде використання якоїсь фільтрації / перевірки на зразок:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Примітка: ви не можете використовувати PDO для прив'язки даних, які виходять за межі DDL (Мова визначення даних), тобто це не працює:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

Причина , по якій вище робить роботу не тому , що DESCі ASCНЕ дані . PDO може отримати лише дані . По-друге, ви навіть не можете ставити 'цитати навколо цього. Єдиний спосіб дозволити сортування, обраному користувачем, - це вручну фільтрувати та перевіряти, чи це DESCабо ASC.


11
Я щось тут пропускаю, але чи не вся суть підготовлених заяв, щоб уникнути трактування sql як рядка? Не хотілося б щось на зразок $ dbh-> Priprav ('SELECT * FROM: tableToUse where username =: username'); обійти свою проблему?
Роб Форест

4
@RobForrest так, ви пропали :). Зв’язані дані працюють лише для DDL (Мова визначення даних). Вам потрібні ці цитати та правильне втеча. Розміщення лапок для інших частин запиту розбиває його з великою ймовірністю. Наприклад, SELECT * FROM 'table'може бути помилковим, як це має бути, SELECT * FROM `table`або без будь-яких зворотних факторів. Тоді деякі речі , як , ORDER BY DESCде DESCвідбувається від користувача не може бути просто втік. Отже, практичні сценарії досить необмежені.
Вежа

8
Цікаво, як 6 людей могли одержати коментар, пропонуючи явно неправильне використання підготовленої заяви. Якби вони навіть спробували це один раз, вони відразу зрозуміли б, що використання названого параметра замість імені таблиці не буде працювати.
Фелікс Ганьон-Греньє

Ось чудовий підручник з PDO, якщо ви хочете його вивчити. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
Р. Н. Кушваха

11
Ніколи не слід використовувати рядок запиту / тіло POST для вибору таблиці для використання. Якщо у вас немає моделей, принаймні використовуйте switchклавішу a для отримання назви таблиці.
ZiggyTheHamster

29

Так, достатньо. Те, як працюють атаки типу ін'єкцій, полягає в тому, щоб якось отримати інтерпретатора (Базу даних), щоб оцінити щось, що повинно було бути даними, як би кодом. Це можливо лише в тому випадку, якщо ви змішуєте код і дані в одному середовищі (наприклад, коли ви створюєте запит як рядок).

Параметризовані запити працюють, надсилаючи код та дані окремо, тому ніколи не вдасться знайти в цьому дірку.

Ви все ще можете бути вразливими до інших нападів ін'єкційного типу. Наприклад, якщо ви використовуєте дані на HTML-сторінці, ви можете зазнати атак типу XSS.


10
"Ніколи" це спосіб завищення, до того, що вводити в оману. Якщо ви використовуєте підготовлені заяви неправильно, це не набагато краще, ніж взагалі їх не використовувати. (Звичайно, "підготовлений вислів", який вводив в нього введення користувача, перемагає мету ... але я насправді бачив це зроблено. І підготовлені оператори не можуть обробляти ідентифікатори (назви таблиць тощо) як параметри.) Додати до цього деякі з драйверів PDO імітують підготовлені оператори, і є можливість зробити це неправильно (наприклад, шляхом напівсидячого розбору SQL). Коротка версія: ніколи не вважайте, що це так просто.
cHao

29

Ні цього недостатньо (у деяких конкретних випадках)! PDO за замовчуванням використовує емуляцію підготовлених операторів при використанні MySQL як драйвера бази даних. Під час використання MySQL та PDO завжди слід відключити емуляцію підготовлених операторів:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Інша річ, яку завжди слід зробити, це встановити правильне кодування бази даних:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

Дивіться також пов’язане з цим питання: Як я можу запобігти ін'єкції SQL у PHP?

Також зауважте, що мова йде лише про базу даних речей, за якими вам все-таки слід спостерігати під час відображення даних. Наприклад, htmlspecialchars()повторно використовуючи правильний стиль кодування та цитування.


14

Особисто я завжди запускав би якусь форму перевірки даних, оскільки ви ніколи не можете довіряти введенню користувача, однак при використанні заповнювачів / параметрів, що зв'язують введені дані, надсилаються на сервер окремо до заяви sql і потім пов'язуються разом. Ключовим тут є те, що це прив'язує надані дані до конкретного типу та конкретного використання та виключає будь-яку можливість змінити логіку оператора SQL.


1

Навіть якщо ви збираєтеся запобігти введенням sql переднього типу, використовуючи html або js-чеки, вам доведеться врахувати, що фронтальні перевірки є "обхідними".

Ви можете відключити js або відредагувати візерунок за допомогою інструмента розробки переднього інтернету (вбудованого в Firefox або Chrome сьогодні).

Отже, щоб запобігти введенню SQL, було б правильним провести санітарну дату введення дати всередині вашого контролера.

Я хотів би запропонувати вам використовувати натиснуту функцію PHP filter_input (), щоб очистити значення GET та INPUT.

Якщо ви хочете продовжувати безпеку, для розсудливих запитів до бази даних я б запропонував вам використовувати регулярне вираження для перевірки формату даних. preg_match () допоможе вам у цьому випадку! Але подбайте! Двигун Regex не такий вже й легкий. Використовуйте його лише у разі потреби, інакше продуктивність ваших програм зменшиться.

Безпека має витрати, але не витрачайте свої результати!

Простий приклад:

якщо ви хочете подвійно перевірити, чи є значення, отримане від GET, числом, меншим ніж 99, якщо (! preg_match ('/ [0-9] {1,2} /')) {...} важко

if (isset($value) && intval($value)) <99) {...}

Отже, остаточна відповідь: «Ні! Підготовлені заяви PDO не заважають всім типам sql введення»; Це не заважає несподіваним значенням, просто несподіваному конкатенації


5
Ви плутаєте ін'єкцію SQL з чимось іншим, що робить вашу відповідь абсолютно неважливою
ваше здорове почуття
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.