Основна помилка: конкатенація рядків
SQL-ін'єкції притаманні через одну повторювану помилку: побудову запиту до бази даних шляхом вставлення користувацького введення безпосередньо у рядок, що змушує базу даних не розрізняти дані, на яких має працювати запит, та код, який визначає цей запит. Типовий вразливий перевірка логіну виглядає так:
query = "SELECT * FROM users WHERE name = '" + username + "' AND pass = '" + password + "'"; // Атакувальник вводить ім'я користувача: ' OR '1'='1 // Рядок стає: // SELECT * FROM users WHERE name = '' OR '1'='1' AND pass = '...' Демонстрація · обійдений вразливий запит, потім заблоковано● ДЕМО КІЛЬКА Вразливість виникає через те, що користувацький ввід безпосередньо вставляється у SQL-запит. Це дозволяє атакувальнику змінювати логіку запиту та отримувати несанкціонований доступ до даних.
Оскільки '1'='1' завжди істинне, умова WHERE задовольняється для кожного рядка незалежно від пароля, і запит повертає першого користувача в таблиці — зазвичай адміністратора, оскільки він часто є першим зареєстрованим користувачем. Нічого не було зламано в криптографічному сенсі; база даних виконувала SQL-запит, який їй було надано, а введені атакуючим дані просто змінили цей запит.
query = "SELECT * FROM users WHERE name = '" + username +
"' AND pass = '" + password + "'";
// attacker enters username: ' OR '1'='1
// the string becomes:
// SELECT * FROM users WHERE name = '' OR '1'='1' AND pass = '...'
Параметризовані запити – це найкраще рішення
Вирішення полягає не в фільтрації чи обробці спеціальних символів після факту — цей підхід неодноразово зазнав невдачі, оскільки завжди можна знайти інше кодування або крайній випадок, який може використати нападник. Справжнє рішення – ніколи не будувати запит з рядка: параметризовані (підготовлені) запити надсилають структуру запиту та дані користувача в базу даних як два окремих канали, тому введення користувача ніколи не розглядається як частина граматики SQL.
stmt = db.prepare("SELECT * FROM users WHERE name = ? AND pass = ?");
stmt.execute([username, password]);
// username = " ' OR '1'='1 " is now just a literal string value to
// compare against the name column -- it can never become SQL syntax
XSS: одна ідея, на один рівень вище в браузері
Крос-сайтове скриптування структурно ідентичне, просто перенесено з парсера бази даних до HTML-аналізатора браузера: введені дані користувача вставляються у сторінку без екранування, а браузер інтерпретує частину цих даних як розмітку або скрипт замість тексту. Існує три стандартні варіанти: збережене XSS, де шкідливий ввід зберігається на стороні сервера (коментар, поле профілю) та подається кожному відвідувачу; відбите XSS, коли воно повертається безпосередньо у відповіді до одного створеного запиту, часто через URL-параметр; та DOM-based XSS, де вразливий код взагалі не торкається сервера — клієнтський JavaScript читає щось, контрольоване нападником (URL, наприклад), і пише це небезпечно у сторінку.
comment = "" page.innerHTML = "Latest comment: " + comment; // the browser parses and RUNS the script tag -- it doesn't know the // comment text was supposed to be inert data, same failure as SQLi
Рішення: екранування та Content-Security-Policy
Так само, як SQL-ін'єкції запобігають шляхом ніколи не дозволяючи користувальським даним інтерпретуватися як синтаксис SQL, XSS запобігається шляхом ніколи не дозволяючи користувальським даним інтерпретуватися як HTML або синтаксис скриптів. Екранування виводу (перетворення < на <, > на > і т.д.) кожного разу, коли ненадійні дані записуються на сторінку, означає, що браузер відображає їх як видимий текст замість виконання. Сучасні фреймворки автоматично виконують це для будь-яких даних, вставлених через їхній шар шаблонізації; вразливий шаблон вище майже завжди передбачає обхід цього шару шляхом прямого присвоєння innerHTML. Заголовок Content-Security-Policy додає додатковий рівень захисту, повідомляючи браузеру, з яких джерел дозволено виконувати скрипти, щоб навіть у разі успішного впровадження платної частини не було звідки завантажити її.
Часті запитання
Чому ' OR '1'='1' обходить перевірку входу?
Тому, що вразливий код будує SQL-запит шляхом конкатенації необробленого введення у рядок. Впроваджений текст закриває значене цитату та додає OR-умову, яка завжди істинна, тому пункт WHERE відповідає кожній рядку таблиці незалежно від фактичного пароля, а база даних повертає першого збігаючогося користувача.
Яке справжнє рішення для SQL-ін'єкцій, і чому не просто фільтрувати небезпечні символи?
Параметризовані (підготовлені) заяви, які надсилають структуру запиту та дані, надані користувачем, до бази даних окремими каналами, щоб вхідні дані ніколи не розглядалися як синтаксис SQL. Фільтрація або обробка символів є ненадійною, оскільки завжди можна використати інший код, хитрий трюк або особливість діалекту; параметризовані заяви повністю усувають клас вразливості замість того, щоб виправляти окремі обхідні шляхи.
Яка різниця між збереженим, відбитим та DOM-базовим XSS?
Збережений XSS зберігається на сервері та подається кожному відвідувачеві (наприклад, у коментарі). Відбитий XSS безпосередньо відображається в відповіді до одного створеного запиту, часто через параметр URL, і вимагає обману жертви для натискання посилання. DOM-базовий XSS взагалі не торкається сервера — вразливий JavaScript на стороні клієнта читає дані, контрольовані зловмисником (наприклад, URL), та безпечно записує їх у сторінку.
Спробуйте наживо
Усе, що вище, працює прямо у вашому браузері — відкрийте SQL Injection and XSS Demo і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.
▶ Відкрити симуляцію SQL Injection and XSS Demo