Довіряй, але перевіряй: захист від SQL-ін'єкцій
Поза всякими сумнівами, SQL-ін'єкції є одним з найпоширеніших способів злому сайту. Чи не перше, що намагається провернути зломщик - тестування популярних ін'єкцій. У цьому невеликому пості ми коротко розглянемо історію питання, методи боротьби з ін'єкціями, а також напишемо невеликий PHP-клас обгортку для PDOStatement для безпечного підключення та взаємодії з MySQL-сервером (MySQL в даному випадку наводиться лише з причини найбільшої поширеності, при бажанні все наступне може бути адаптоване і на інші СУБД).
Що, по суті, із себе представляє ін'єкція? Початківець php-розробник, тільки що приступив до вивчення MySQL, найімовірніше, буде конструювати запити приблизно наступним чином:
У разі добропорядного користувача, скрипт спрацює саме так, як і було задумано. Що ж спробує зробити зловмисник? Він не стане відправляти на сервер числове значення. Замість цього він, спробує поставити після числового значення крапку з комою (символ завершення SQL-запиту) після чого напише свій, вже не має до спочатку задуманому нами запит. Щось на кшталт цього:
або ще страшніше:
В результаті, MySQL-сервер спочатку виконає наш запит, а слідом за ним - запит зловмисника. Таким чином, зломщик, в залежності від ситуації (і прав MySQL користувача, який використовується PHP) зробити майже все що завгодно: від отримання доступу до конфіденційної інформації до повного контролю над вашою базою даних.
Як же розробнику захистити свій сайт від ін'єкцій?
Ручна перевірка призначених для користувача даних. Зрозуміло, перевірка формату даних, що вводяться - перший найбільш очевидний варіант захисту від ін'єкцій. Замість того щоб довіряти вводиться даними ми будемо перевіряти їх на відповідність потрібного нам формату. Робити це можна десятком різних способів. наприклад:
В даному випадку, отримана з GET-а інформація перед відправкою перевіряється на числове значення. Будь-яке значення, відмінне від числового, введене зловмисники не буде відправлено на MySQL-сервер. Для валідації більш складних значень можна використовувати регулярні вирази.
Іншим варіантом примітивної захисту є використання нативної функції mysqli_real string_escape () для запобігання проникнення «корварних» запитів. Головним мінусом подібного підходу, є його крайня неавтоматізірованность: нам доводиться перевіряти призначене для користувача значення кожен раз коли ми робимо запит. Можна, звичайно, використовувати готовий SQL-билдер з вбудованим захистом від SQL-ін'єкцій або написати свій. Однак, в даній статті ми пропонуємо дещо інший підхід, заснований на використанні PDO.
Починаючи з версії 5.1 в PHP доступний вбудований клас PDO (PHP Data Objects). Даний клас містить багатий набір методів для роботи з широким спектром баз даних. Незважаючи на поважний вік даного інструменту, багато розробників їм нехтує, вважаючи за краще «по-старому» користуватися бібліотекою mysqli, в тому числі і ми в DLE, але у нас є ряд важливий причин, DLE це старий скрипт, який з'явився задовго до PDO, і у нас є зобов'язання по сумісності, як зі старими версіями скрипта, так і по максимальному спрощенню процесу оновлення, для тих, хто користується сторонніми модулями. Плюс ми дуже ретельно підходимо до питань фільтрації вхідних даних. Але ви на відміну від нас не такі старі «динозаври», тому головна думка, яку ми хочемо донести до вас полягає в наступному: пряме використання mysqli без будь-яких обгорток - прямий шлях до SQL-ін'єкцій, тому пишіть код відразу безпечним. У розробників, по суті, є тільки три виходи:
- використовувати ручну перевірку призначених для користувача даних
- написати власну бібліотеку (або взяти готову) на основі mysqli із захистом від SQL-ін'єкцій
- використовувати PDO
Наш клас буде містити всього 4 методу:
- метод для з'єднання з базою
- метод для перевірки наявності з'єднання
- метод для відправки безпечного запиту через PDOStatement
- метод для розриву з'єднання з базою
Перші 5 містять хост, ім'я бази, логін, пароль і кодування і не потребують пояснення. У 6-е властивість ж стане в нагоді нам для зберігання об'єкта pdo. Насамперед слід, звичайно, написати метод для з'єднання:
Перший рядок містить набір параметрів для з'єднання з сервером. Масив $ connopt задає різні режими роботи з PDO. Ці режими можуть бути використані для тонкої налагодження помилок і специфічних ситуацій. Ми не будемо вдаватися в подробиці використовуваних опцій в цьому посібнику. Всі зацікавлені можуть звернутися до більш докладної документації по PHP. Тут же відзначимо, що в останньому рядку ми створюємо об'єкт для роботи з PDO, передаючи конструктору задані параметри, і пишемо цей об'єкт в наше властивість $ pdo.
Отже, з базою ми з'єдналися. Напишемо же метод для перевірки наявності з'єднання. З цією метою ми будемо використовувати метод getAttribute (PDO :: ATTR_CONNECTION_STATUS). У разі нормального з'єднання він повертає рядок "hostname via TCP / IP", де hostname - ім'я нашого хоста.
Якщо властивість pdo порожньо - значить з'єднання не створено (або було зруйновано). Наступним етапом ми перевіряємо значення повертається getAttribute і порівнюємо його з нормальним. Якщо воно відрізняється - викидаємо в false. Якщо все пройшло гладко - повертаємо true. Якщо немає - false.
Тепер, власне, найцікавіше. Напишемо метод для відправки безпечного запиту до бази.
Наш метод буде мати три аргументи:
- тіло самого запиту
- асоціативний масив з набором значень
- бінарний аргумент, який визначає чи потрібно повернути дані з бази. Для виконання SELECT'ов даний аргумент буде дорівнює true, а для INSERT'ов \ UPDATE'ов - false.
Давайте розберемося, що ж тут відбувається. Перша умова необхідно для того, щоб намагатися виконувати запит тільки при наявності з'єднання з базою даних. У другому умови, ми перевіряємо наявність масиву значень (плейсхолдеров). Якщо масив не вказано - значить ми виконуємо запит «як є». Якщо ж масив плейсхолдеров має місце бути, ми передаємо його методу execute () як аргумент. Методи prepare () і execute () - і є те заради чого все створювалося. Як ви могли помітити, при роботі з базою через PDO тіло запиту і його значення передаються PDO окремо один від одного. При цьому сам запит пишеться в наступному вигляді:
Де: user_id - назва ключа в масиві, переданому в execute (). Тобто в явному вигляді відправка запиту з використанням PDOStatement виглядає приблизно так:
Для руйнування з'єднання досить лише привласнити null об'єкту pdo. Тому, метод для ліквідації з'єднання буде найкоротшим:
Отже, зберемо наш клас воєдино: