SQL Injection (вставка SQL) - це одна з найпоширеніших на ряду із XSS (міжсайтовим скриптингом, про який буде мова у наступній статті) уразливостей, яка є наслідком недостаньої (чи навіть відсутньої) перевірки вводу. Ціль цього типу атак є безпосередньо база даних вашого сайту. Суть - вставка сторонніх даних (найчастіше - запиту до БД) у строку, яка в результаті буде виконана базою даних. Такий запит може призвести до великої кількості небажаних дій: починаючи від отримання зловмиником будь-яких даних до їх зміни чи видалення. Тож важливо знати, як захистити свою базу даних від таких уразливостей.
Для оцінки маштабів цієї проблеми приведемо приклад:
<?php $userId = "4'; DELETE FROM users;"; //можливий ввід mysql_query("SELECT * FROM users WHERE id='$userId'"); ?>
Цей шмат коду має на меті вибрати з бази даних дані користувача за переданим у скрипт ідентифікатором. І, якщо ви використовуєте MySQL, то власне так і зробить
Адже ця БД не підтримує декілька запросів у одному виклику. А якщо ж ваша БД - SQLite чи PostgreSQL, то вони, не довго думаючи, спочатку виберуть потрібні дані, а потім видалять всі дані з таблиці “users”.
Екранування вводу
Проблемою у даному випадку є недостатнє екранування і перевірка вводу. Про це вже було достатньо сказано у статті про перевірку вхідних даних. Давайте розглянемо це у контексті SQL Injection. Найпростіший захист від цього надає вбудований у PHP механізм magic_quotes_gpc. Коли він увімкнений, перед лапками (одинарними чи подвійними) та іншими загрозливими символами автоматично буде вставлено зворотній слеш (”\”).
Однак це недостатнє рішення, оскільки не екранує деякі спеціальні до SQL символи та й не завжди цей механізм увімкнено. Тож різні розширення для роботи із БД мають свої спеціалізовані механізми екранування. У MySQL це функція mysql_real_escape_string та mysql_escape_string. Різниця між ними лиш у тому, що перша екранує відносно указаного їй у параметрах кодування, а друга до цього не чутлива. Приклад використання:
<?php if (get_magic_quotes_gpc()) { $userId = stripslashes($userId); } $userId = mysql_real_escape_string($userId); mysql_query("SELECT * FROM users WHERE id='$userId'"); ?>
У цьому коді спочатку перевіряється наявність розширення magic_quotes_gpc, в разі чого його дія реверсується, щоб не виникло ситуації з подвійним екрануванням. Також специфічні до БД функції є незамінними при зберіганні двійкових даних. Якщо залишити їх неекранованими - вони можуть конфліктувати із форматом збереження даних самої БД. Функції екранування про це знають і не допускають можливої втрати даних.
У деяких БД, наприклад, PostgreSQL - окремі функції відповідають за екранування двійкових даних. Наприклад, функція pg_escape_bytea застосовує Base64-подібне кодування до вхідних даних. Щоправда, таке кодування накладає обмеження на пошук - до них неможна застосовувати такі запити як “LIKE ’str%’”, оскільки значення, збережене у БД, не обов’язково буде сходитись із тим, яке шукається за запитом. Звичайно, це не проблема для більшості веб-додатків, адже збереження у двійковому вигляді зазвичай необхідно для таких даних як картинки та зтиснені файли, а вони не є предметом глибинного пошуку.
Підготовлені вирази
На жаль, не для всіх БД існують специфічні функції екранування. Фактично, вони є лиш для: MySQL, PortgreSQL, SQLite, Sybase та MaxDB. Для інших популярних баз даних як Oracle, MS SQL Server та інших необхідно знайти альтернативу.
На перший погляд може підійти кодування даних у Base64, що ефективно екранує всі спеціальні символи. Але це створює кілька проблем: розмір даних, закодованих Base64 зростає рівно на 33% та унеможливлює вибірку даних за допомогою LIKE. Тож потрібен кращий метод.
Цей метод називається підготовленими виразами (або запитами). Такі запити фактично є шаблонами з визначеною структурою та місцями для реальних даних. Часто ці місця суворо типізовані для чисельних та текстових даних. Якщо тип даних не збігається із визначеним типом, то буде видана помилка, що додає ще один ступінь перевірки даних. Також підготовлені вирази додають швидкодію, адже вони компілюються лиш один раз і можуть використовуватись після цього із будь-якими даними. Наприклад, для PostgreSQL це може виглядати так:
<?php pg_query($db, 'PREPARE stmt_id (int) AS SELECT * FROM users WHERE id=$1'); pg_query($db, "EXECUTE stmt_id ($userId)"); pg_query($db, 'DEALLOCATE stmt_id'); ?>
Якщо ви працюєте із MySQL, то підготовлені запити можна використовувати лише із розширенням mysqli чи PDO. Приклад:
<?php $stmt = $mysqli->stmt_init(); if ($stmt>prepare('SELECT * FROM users WHERE id=?')) { $stmt->bind_param('i', $userId); //"i" означає integer $stmt->execute(); $stmt->bind_result($user); $stmt->fetch(); } $stmt->close(); ?>
Використання трохи різне, але принцип однаковий. Спочатку створюється шаблон запиту, потім приєднуються реальні дані за їх типом та виконується сам запит. Підготовлені вирази - дуже класний інструмент, але дійсний не для всіх БД. У цих випадках слід використовувати екранування.
Коли екранування не допомагає
Існують випадки, коли екранування даних не убезпечить від зловмисних дій. Уявімо таку ситуацію:
<?php $userId = '0; DELETE FROM users'; $userId = pg_escape_string($userId); pg_query($db, "SELECT * FROM users WHERE id=$id"); ?>
Запит у даному випадку цілком дійсний, адже ми очікуємо чисельні дані і їх не обов’язково брати у лапки. Цим і наражаємо себе на небезпеку у вигляді SQL Injection. Перший вихід з цієї ситуації - абсолютно всі вхідні дані брати у лапки при використанні у запиті, незалежно від їх типу. Але як ми зараз побачимо, це також не дуже добре:
<?php $userId = "0'; DELETE FROM users"; $userId = pg_escape_string($userId); pg_query($db, "SELECT * FROM users WHERE id='$id'") or die(pg_last_error($db)); ?>
У цьому випадку звичайно запит не буде виконано, натомість буде видана помилка і це призведе до краху вашої програми. Але ми можемо цього легко і просто уникнути звичайним приведенням типу. Таким чином запит буде вдало виконано і нічого не зупиниться:
<?php $userId = '22; DELETE FROM users'; $userId = (int)$userId; // 22 pg_query($db, "SELECT * FROM users WHERE id=$id"); ?>
Це буде абсолютно безпечно, адже до запиту прийдуть лише чисельні дані. До того ж це підвищить швидкодію, оскільки приведення типу - дуже швидка операція, яка убирає необхідність виклику функції екранування.
Оператор LIKE
Оператор LIKE у SQL запитах є дуже корисним. Він дозволяє робити вибірку в залежності від знайдених у полі підстрок. Це робиться за допомогою його складових “%” та “_“, які дозволяють відповідно знаходити строки із 0 чи більшою кількістю будь-яких символів та один будь-який символ. Проблема ж у тому, що ані функції екранування, ані magic quotes не впливають на ці символи. Тож із деякими комбінаціями можна змінити запит, ускладнити вибірку та у багатьох випадках не дозволити використання індексів, що значно вповільнить запит і надасть ґрунт до запуску DoS атаки на ваш сервер БД. Ось надзвичайно простий приклад:
<?php $string = mysql_real_escape_string('%substring'); // так само %substring mysql_query("SELECT * FROM posts WHERE title LIKE '{$string}%'"); ?>
Суть запиту у тому, щоб вибрати ті записи, у яких назва починається з деякої строки. Такий запит мав би виконатись досить швидко за наявності індексації колонки “title”. Але, якщо у строку буде вставлено ще один символ “%” на початок, то це унеможливить використання індекса і значно сповільнить запит, при чому, чим більше записів у таблиці, тим повльніше буде здійснюватись пошук.
Символ “_” представляє схожу але дещо іншу проблему. Використання цього символу перед закінченням строки пошуку унеможливить використання індекса, а наявність його у кінці - дасть інший результат. Окрім того, символ “_” є досить часто вживаним, тож може стати проблемним зовсім випадково без злих намірів. Тож нам потрібно розширити множину екранованих символів. Для цього існує вбудована функція addcslashes, яка приймає строку із символами для екранування другим параметром:
<?php $str = addcslashes(mysql_real_escape_string('%something_'), '%_'); //$str = '\%something\_' mysql_query("SELECT * FROM posts WHERE title LIKE '{$str}%'"); ?>
Тож функція addcslashes є таким собі нестандартним addslashes і є досить ефективною - значно швидша за str_replace чи еквівалентний регулярний вираз.
Слідкуємо за помилками
Один із звичайних шляхів для зловмисника при пошуку уразливого до SQL Injection коду - використання інструментів розробників проти них самих. Наприклад, для полегшення відлагодження SQL-запитів, розробники часто виводять запит, що призвів до помилки і власне саму помилку бази даних:
<?php mysql_query($query) or die("Помилка запиту: $query<br />".mysql_error()); ?>
Часто після процесу розробки такий код потрапляє на робочий варіант сайту і тоді, окрім того, що звичайні користувачі бачать ваш сайт в агонії, ще й зловмисники отримують купу інформації про вашу програму. Наприклад, можливо проаналізувати SQL-запит на предмет колонок використаних таблиць та змайструвати GET чи POST запити, які можуть призвести до ін’єкції SQL. Більше того, помилка може стати результатом спроби SQL-вставки і послужить зловмиснику інструкцією зі створення більш хитрих запитів.
Найпростіший шлях уникнути цього - написати власний обробник помилок SQL:
<?php function sql_failure_handler($query, $error) { $msg = htmlspecialchars("Невдалий запит: $query<br />Помилка SQL: $error"); error_log($msg, 3, '/path/to/site/logs/sql_error.log'); if (defined('debug')) { return $msg; } return 'Ця сторінка тимчасово недоступна. Будь ласка, повторіть спробу пізніше.'; } mysql_query($query) or die(sql_failure_handler($query, mysql_error())); ?>
Ось і все. Обробник приймає строки із запитом і помилкою, виводить інформацію про збій у файл журналу, та, якщо увімкнено режим відлагодження - виводить повідомлення на екран. У робочому ж середовищі, користувач нічого, окрім стандартного повідомлення про недоступність ресурсу, не побачить.
Збігання даних про доступ до БД
Дуже важливим у створенні безпечного середовища є те, де ви зберігаєте дані про доступ до БД, тобто ваші логін та пароль. Більшість веб-програм використовують маленький PHP-файл, у якому логін та пароль присвоюються деяким змінним чи константам. На цей файл часто встановлюються права на читання для будь-кого, щоб веб-сервер міг отримати до нього доступ. Але це означає, що будь-хто на цій системі чи експлоіт може прочитати цей файл та вкрасти ваші дані про доступ до БД. Більше того, іноді цей файл кладуть у директорії, доступній для будь-кого по ту сторону сервера та дають йому яке-небудь розширення, не асоційоване із PHP. Наприклад, популярним вибором є “.inc“. Такі розширення типово не налаштовані на інтерпретацію як PHP-скрипти і веб-сервер просто віддає їх як звичайний текст, який будь-хто може побачити.
Вирішень цієї проблеми декілька з різною ступінню безпечності. По-перше, можна використати можливості веб-сервера, такі як файл налаштування .htaccess у Apache для того, щоб заборонити доступ до деяких файлів. Наприклад ось директива, яка забороняє доступ до будь-яких файлів із розширенням .inc:
<Files ~ "\.inc$"> Order allow,deny Deny from all </Files>
Або ж, наприклад, можна змусити PHP інтерпретувати .inc файли, як скрипти або змінити розширення на .php або .inc.php. Щоправда, перейменування файлів - не завжди найбезпечніший варіант. Найкращий і найпростіший спосіб - не зберігати такі файли у директоріях, доступних для веб-сервера. Але це все-одно не убезпечує від використання експоітів локальними користувачами (актуально, якщо ви на віртуальному хостингу).
Одне, на перший погляд, гарне вирішення проблеми - шифрування важливих даних. Але це робить крадіжку лиш дещо складнішою і не вирішує проблеми, адже ключ до шифру все-одно має бути доступним для PHP-скрипта, який запускається із користувачем веб-сервера, що означає те, що ключ все-одно є доступним для читання будь-ким. Повертаємось до того, з чого і почали.
Правильне вирішення має давати гарантію, що інші користувачі системи не мають жодного шансу побачити дані доступу до БД. На щастя, Apache надає такий інструмент. Файл налаштування Apache, httpd.conf, може включати в себе сторонні файли налаштування у момент запуску процесу веб-сервера, тобто доки він ще працює як root. Оскільки root має доступ до будь-яких файлів, ви можете покласти важливу інформацію у файлі в своїй домашній директорії (/home/user/) і надати права на читання лиш для себе (0600). Таким чином тільки ви та root зможуть читати цей файл. Робиться це таким чином:
<VirtualHost webdeveloping.com.ua>
Include /home/webdeveloping/sql.conf
</VirtualHost>
Сам же sql.conf встановлює наступні змінні середовища:
SetEnv DB_LOGIN "login" SetEnv DB_PASS "password" SetEnv DB_NAME "my_database" SetEnv DB_HOST "127.0.0.1"
Після запуску Apache, ці змінні будуть доступні PHP через суперглобальний масив $_SERVER або функцію getenv:
<?php echo $_SERVER['DB_LOGIN']; //login echo getenv('DB_LOGIN'); //login ?>
А ще краще заховати ці дані навіть від скрипта, якому вони потрібні
Можна використати директиви php.ini для визначення типових даних для зв’язку із БД. Це також можна встановити у захищеному файлі налаштувань Apache:
php_admin_value mysql.default.host "127.0.0.1" php_admin_value mysql.default.user "login" php_admin_value mysql.default.password "password"
Тепер, функція mysql_connect може працювати без параметрів, адже вони будуть взяті автоматично з налаштувань. Єдине, що потрібно буде встановити - це назва бази. Але не слід використовувати цей метод, якщо ваша версія PHP < 4.3.5. У цих старих версіях був глюк, який дозволяв протікати налаштуванням PHP від одного віртуального хоста до іншого, і таким чином, користувачі інших хостів могли побачити чужі налаштування.
Вказівки до швидкодії
На останок хочу надати кілька вказівок, які можуть поліпшити швидкодію ваших веб-проектів. Це має безпосереднє відношення і до безпеки, адже повільні запити можуть призвести до інших типів атак, як це було показано із оператором LIKE. Ось кілька простих правил, які слід пам’ятати:
- Отримуйте лише ті дані, які вам необхідні. Часто розробники використовують символ “*” у запитах для того, щоб дістати всі колонки результату, що може бути величезною кількістю даних, особливо, якщо запит складається з багатьох приєднаних таблиць. Це означає більше використання пам’яті для сортування у БД, більше часу для передачі до PHP, більше часу і пам’яті для обробки даних.
- Спробуйте використовувати небуферизовані запити, які отримують дані невеличкими порціями. Але їх треба використовувати обережно, адже одночасно ви можете працювати лише із одним запитом. У випадку MySQL, ви не зможете навіть виконати INSERT чи UPDATE, доки всі дані поточного запиту не будуть отримані.
- Створення з’єднання із БД - повільна операція, особливо у випадку “тяжких” систем, як Oracle, PostgreSQL чи MSSQL. Пришвидшити це можна використовуючи постійне з’єднання, яке дозволяє ресурсу залишатись дійсним навіть після припинення роботи скрипта, що дозволяє використовувати одне і те саме з’єднання для багатьох процесів веб-сервера. Для цього існує функція mysql_pconnect (у випадку MySQL, інші БД мають схожі функції). Але це має свої мінуси. Пул з’єднань у PHP працює на основі процесів, а не веб-серверів, тому кожен процес веб-сервера має свій пул з’єднань. Це означає, що 50 запущених процесів Apache призведуть до 50 відкритих з’єднань із БД. Якщо БД не налаштована на те, щоб приймати одночасно стільки з’єднань, то зайві будуть відкинуті, що призведе до збою вашої програми.
- Часто веб-сервер та сервер БД працюють на одній і тій самій машині, що дозволяє оптимізувати передачу даних. Навіщо використовувати повільний і громіздкий TCP/IP, коли ви можете використати Unix Domain Sockets (UDG) - найвшидший засіб передачі даних після Inter Process Communication (IPC). Цим ви можете значно пришвидшити зв’язок між двома серверами. Для цього потрібно змінити хост у налаштуваннях з’єднання: <?php mysql_connect(’:/tmp/mysql.sock’, ‘login’, ‘password’); ?>
- Використовуйте кешування запитів. Це збереже результат запиту на деякий час, продовж якого можна не повторювати запит, а натомість - брати готові дані із кешу. Після того, як кеш стане недійсним - повторити запит і зберегти у кеш.
Сподіваюсь, стаття була вам цікава і допоможе зробити вашу роботу із базами даних швидшою і безпечнішою.




















