Что такое SQL-инъекция и как ее предотвратить в PHP-приложениях?

Итак, вы думаете, что ваша база данных SQL эффективна и защищена от мгновенного уничтожения? Ну, SQL Injection не согласен!

Да, речь идет именно о мгновенном уничтожении, потому что я не хочу начинать эту статью с привычной хромой терминологии «усиление безопасности» и «предотвращение злонамеренного доступа». SQL-инъекция — это такой старый прием, о котором знает каждый разработчик и прекрасно знает, как его предотвратить. За исключением того странного случая, когда они ошибаются, и результаты могут быть не чем иным, как катастрофическими.

Если вы уже знаете, что такое SQL Injection, смело переходите ко второй половине статьи. Но для тех, кто только начинает заниматься веб-разработкой и мечтает занять более высокие должности, необходимо ввести некоторые сведения.

Что такое SQL-инъекция?

Ключ к пониманию SQL Injection кроется в его названии: SQL + Injection. Слово «инъекция» здесь не имеет никакого медицинского значения, а скорее является употреблением глагола «вводить». Вместе эти два слова передают идею использования SQL в веб-приложении.

Внедрение SQL в веб-приложение. . . хм . . . Разве это не то, что мы делаем в любом случае? Да, но мы не хотим, чтобы злоумышленник управлял нашей базой данных. Давайте разберемся в этом на примере.

Допустим, вы создаете типичный веб-сайт PHP для местного интернет-магазина, поэтому вы решили добавить контактную форму, подобную этой:

<form action="record_message.php" method="POST">
  <label>Your name</label>
  <input type="text" name="name">
  
  <label>Your message</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Send">
</form>

И давайте предположим, что файл send_message.php хранит все в базе данных, чтобы владельцы магазина могли позже прочитать пользовательские сообщения. У него может быть такой код:

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// check if this user already has a message
mysqli_query($conn, "SELECT * from messages where name = $name");

// Other code here

Итак, сначала вы пытаетесь узнать, есть ли у этого пользователя непрочитанное сообщение. Запрос SELECT * from messages where name = $name кажется достаточно простым, не так ли?

НЕПРАВИЛЬНЫЙ!

В своей невинности мы открыли двери для мгновенного уничтожения нашей базы данных. Для этого злоумышленнику необходимо выполнить следующие условия:

  • Приложение работает на базе данных SQL (сегодня почти каждое приложение)
  • Текущее подключение к базе данных имеет права «редактировать» и «удалять» в базе данных.
  • Названия важных таблиц можно угадать

Третий пункт означает, что теперь, когда злоумышленник знает, что вы управляете интернет-магазином, вы, скорее всего, храните данные о заказах в таблице заказов. Вооружившись всем этим, все, что нужно сделать злоумышленнику, это указать это как свое имя:

Джо; урезать заказы;? Да сэр! Давайте посмотрим, во что превратится запрос, когда он будет выполнен PHP-скриптом:

SELECT * FROM messages WHERE name = Joe; урезать заказы;

Хорошо, в первой части запроса есть синтаксическая ошибка (без кавычек вокруг «Джо»), но точка с запятой заставляет движок MySQL начать интерпретацию новой: обрезать заказы. Вот так, одним махом вся история заказов пропала!

Теперь, когда вы знаете, как работает SQL-инъекция, пришло время посмотреть, как ее остановить. Два условия, которые необходимо выполнить для успешного внедрения SQL:

  • PHP-скрипт должен иметь права на изменение/удаление в базе данных. Я думаю, что это верно для всех приложений, и вы не сможете сделать свои приложения доступными только для чтения. 🙂 И угадайте, что, даже если мы удалим все права на изменение, SQL-инъекция все еще может позволить кому-то выполнять запросы SELECT и просматривать всю базу данных, включая конфиденциальные данные. Другими словами, снижение уровня доступа к базе данных не работает, и в любом случае это необходимо вашему приложению.
  • Пользовательский ввод обрабатывается. SQL-инъекция может работать только тогда, когда вы принимаете данные от пользователей. Опять же, нецелесообразно останавливать все входные данные для вашего приложения только потому, что вы беспокоитесь о внедрении SQL.
  • Предотвращение внедрения SQL в PHP

    Теперь, учитывая, что подключения к базе данных, запросы и пользовательский ввод являются частью жизни, как мы можем предотвратить внедрение SQL? К счастью, это довольно просто, и есть два способа сделать это: 1) очистить пользовательский ввод и 2) использовать подготовленные операторы.

    Дезинфекция пользовательского ввода

    Если вы используете более старую версию PHP (5.5 или ниже, а это часто происходит на виртуальном хостинге), целесообразно запускать весь пользовательский ввод через функцию mysql_real_escape_string(). По сути, что он делает, так это удаляет все специальные символы в строке, чтобы они теряли свое значение при использовании базой данных.

    Например, если у вас есть строка типа I’m a string, злоумышленник может использовать символ одинарной кавычки (‘) для управления создаваемым запросом к базе данных и выполнения SQL-инъекции. Запустив его через mysql_real_escape_string(), я получаю строку, которая добавляет обратную косую черту к одинарной кавычке, экранируя ее. В результате вся строка теперь передается в базу данных как безвредная строка, вместо того, чтобы участвовать в манипулировании запросом.

    У этого подхода есть один недостаток: это очень, очень старый метод, который сочетается со старыми формами доступа к базе данных в PHP. Начиная с PHP 7, этой функции больше не существует, что подводит нас к следующему решению.

    Используйте подготовленные операторы

    Подготовленные операторы — это способ сделать запросы к базе данных более безопасными и надежными. Идея состоит в том, что вместо того, чтобы отправлять необработанный запрос в базу данных, мы сначала сообщаем базе данных структуру запроса, который мы будем отправлять. Вот что мы подразумеваем под «подготовкой» заявления. Как только оператор подготовлен, мы передаем информацию в виде параметризованных входных данных, чтобы база данных могла «заполнить пробелы», вставив входные данные в структуру запроса, которую мы отправили ранее. Это лишает входные данные какой-либо особой силы, заставляя их рассматриваться как простые переменные (или полезные нагрузки, если хотите) во всем процессе. Вот как выглядят подготовленные заявления:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Create connection
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Check connection
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // prepare and bind
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // set parameters and execute
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "New records created successfully";
    
    $stmt->close();
    $conn->close();
    ?>

    Я знаю, что процесс кажется излишне сложным, если вы новичок в подготовленных заявлениях, но концепция стоит затраченных усилий. Вот хорошее введение в него.

    Для тех, кто уже знаком с расширением PHP PDO и использует его для создания готовых операторов, у меня есть небольшой совет.

    Предупреждение: будьте осторожны при настройке PDO

    При использовании PDO для доступа к базе данных у нас может возникнуть ложное чувство безопасности. «Ну, я использую PDO. Теперь мне не нужно думать ни о чем другом» — так обычно идет наше мышление. Это правда, что PDO (или подготовленных операторов MySQLi) достаточно, чтобы предотвратить все виды атак SQL-инъекций, но вы должны быть осторожны при его настройке. Обычно просто копируют и вставляют код из руководств или из ваших предыдущих проектов и идут дальше, но этот параметр может отменить все:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Этот параметр указывает PDO эмулировать подготовленные операторы, а не фактически использовать функцию подготовленных операторов базы данных. Следовательно, PHP отправляет простые строки запроса в базу данных, даже если ваш код выглядит так, как будто он создает подготовленные операторы, устанавливает параметры и все такое. Другими словами, вы так же уязвимы для SQL-инъекций, как и раньше. 🙂

    Решение простое: убедитесь, что для этой эмуляции установлено значение false.

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

    Теперь скрипт PHP вынужден использовать подготовленные операторы на уровне базы данных, предотвращая все виды SQL-инъекций.

    Предотвращение использования WAF

    Знаете ли вы, что вы также можете защитить веб-приложения от SQL-инъекций с помощью WAF (брандмауэра веб-приложений)?

    Ну, не только SQL-инъекция, но и многие другие уязвимости уровня 7, такие как межсайтовый скриптинг, нарушенная аутентификация, межсайтовая подделка, раскрытие данных и т. д. Либо вы можете использовать самостоятельный хостинг, такой как Mod Security, либо облачный, как показано ниже.

    SQL-инъекция и современные PHP-фреймворки

    SQL-инъекция настолько распространена, настолько проста, настолько разочаровывает и настолько опасна, что все современные веб-фреймворки PHP имеют встроенные меры противодействия. В WordPress, например, у нас есть функция $wpdb->prepare(), тогда как, если вы используете инфраструктуру MVC, она сделает всю грязную работу за вас, и вам даже не придется думать о предотвращении SQL-инъекций. Немного раздражает то, что в WordPress вам приходится явно подготавливать операторы, но мы же говорим о WordPress. 🙂

    В любом случае, я хочу сказать, что современному поколению веб-разработчиков не нужно думать о SQL-инъекциях, и в результате они даже не подозревают о такой возможности. Таким образом, даже если они оставят открытым один бэкдор в своем приложении (возможно, это параметр запроса $_GET и старая привычка запускать грязный запрос), результаты могут быть катастрофическими. Поэтому всегда лучше потратить время на то, чтобы глубже погрузиться в основы.

    Вывод

    SQL-инъекция — очень неприятная атака на веб-приложение, но ее легко избежать. Как мы видели в этой статье, нужно быть осторожным при обработке пользовательского ввода (кстати, SQL Injection — не единственная угроза, связанная с обработкой пользовательского ввода) и выполнять запросы к базе данных — это все, что нужно. Тем не менее, мы не всегда работаем с безопасностью веб-фреймворка, поэтому лучше знать об этом типе атак и не поддаваться на них.