Коротка відповідь - НІ , підготовка 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 ряду. Давайте розберемо, що тут відбувається:
Вибір набору символів
$pdo->query('SET NAMES gbk');
Щоб ця атака спрацювала, нам потрібно кодування, яке сервер очікує на з'єднання як для кодування, '
так і в ASCII, тобто, 0x27
і мати якийсь символ, кінцевим байтом якого є ASCII, \
тобто 0x5c
. Як з'ясовується, є 5 таких кодувань , підтримуваних в MySQL 5.6 за замовчуванням: big5
, cp932
, gb2312
, gbk
і sjis
. Ми виберемо gbk
тут.
Тепер дуже важливо відзначити використання SET NAMES
тут. Це встановлює набір символів НА СЕРВЕРІ . Є ще один спосіб зробити це, але ми досить швидко потрапимо туди.
Корисний вантаж
Корисне навантаження, яке ми будемо використовувати для цього введення, починається з послідовності байтів 0xbf27
. В gbk
, це недійсний багатобайтовий символ; в latin1
, це рядок ¿'
. Зверніть увагу , що в latin1
і gbk
, 0x27
само по собі є буквальним '
характер.
Ми вибрали це корисне навантаження, оскільки, якби ми зателефонували addslashes()
на нього, ми вставимо ASCII, \
тобто 0x5c
перед '
символом. Таким чином, ми закінчилися 0xbf5c27
, що gbk
є послідовністю двох символів: 0xbf5c
далі 0x27
. Або іншими словами, дійсний символ, за яким слідує непризначений '
. Але ми не використовуємо addslashes()
. Тож до наступного кроку ...
$ 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 / *
А саме цього вимагає атака.
Запит
Ця частина є лише формальністю, але ось наданий запит:
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. Проблема, з якою я стикаюся, полягає в тому, що багато тестів ламаються, коли я це роблю. Одна з проблем полягає в тому, що емульована підготовка видасть лише синтаксичні помилки при виконанні, але справжня підготовка викидає помилки під час підготовки. Тож це може спричинити проблеми (і це є причиною того, що тести нудні).