Панацея от SQL-инъекций — запросы с параметрами

    Введение


    Этот топик — начало небольшого цикла о панацеях от различных уязвимостей Web-приложений.

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

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

    Реализация


    Реализация не сложнее идеи. Для примера возьмем запрос получения записи блога:
    SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`='bla-bla-bla' <br/>

    В PHP-коде это будет выглядеть примерно так:
    <?php<br/>    //...<br/>    $sql = 'SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`=\''.$_GET['url_title'].'\'';<br/>    DB::exec($sql);<br/>    //... <br/>

    Невооруженным глазом видна проблема в безопасности — параметр url_title адресной строки не фильтруется.
    Сей факт обычно обнаруживается тогда, когда его уже кто-нибудь из посетителей нашел. B тогда код преобразуется в более ужасный вид:
    <?php<br/>    //...<br/>    $sql = 'SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`=\''.mysql_real_escape_string($_GET['url_title']).'\'';<br/>    DB::exec($sql);<br/>    //... <br/>

    Некоторые запросы выглядят особо ужасно:
    <?php<br/>    //...<br/>    $sql = 'SELECT `firstname`,`lastname`,`nickname`,`avatar` FROM `users` WHERE `login`=\''.mysql_real_escape_string($_GET['username']).'\' AND `password`=\''.mysql_real_escape_string($_GET['password']).'\'';<br/>    DB::exec($sql);<br/>    //... <br/>

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

    Превратим теперь оба запроса в параметризованные.
    <?php<br/>    //...<br/>    $sql = 'SELECT `date`,`title`,`text`,`tags` FROM `posts` WHERE `url_title`=:1';<br/>    DB::exec($sql, $_GET['url_title']);<br/>    //... <br/>

    <?php<br/>    //...<br/>    $sql = 'SELECT `firstname`,`lastname`,`nickname`,`avatar` FROM `users` WHERE `login`=:1 AND `password`=:2';<br/>    DB::exec($sql, $_GET['username'], $_GET['password']);<br/>    //... <br/>

    По-моему, очень удобно. Вместо конструкций :N подставляется соответствующий аргумент. Их, конечно же, можно дублировать и все такое.
    А что происходит за кулисами (код взят из одного проекта, где я и придумал[хоть идея и не моя, но я об этом не знал] это)?

    public static function exec($query) {<br/>    global $args;<br/>    if(func_num_args() > 1) {<br/>        $args = func_get_args();<br/>        $query = preg_replace_callback(<br/>            '/:([0-9]+)/',<br/>            create_function(<br/>                '$matches',<br/>                'global $args; return "\'".str_replace("\'", "\\\\\'", @$args[$matches[1]])."\'";'<br/>            ),<br/>            $query<br/>        );<br/>    }<br/>    self::$result = sqlite_query($query, self::$handle);<br/>    return self::$result;<br/>} <br/>

    Конечно, регулярные выражения — не лучший выбор в плане производительности, но меня это устраивает. Можно приделать выборку из глобального контекста типа :glob:varname (например, :glob:_GET[id]), но для этого нужен парсер по-умней — в моих целях создание онного не числится. Или отдельное присвоение параметров как в Yii. Однако, это уже дело вкуса. По-моему то, что сделано Yii не решает вопрос читабельности, напротив:
    $username = mysql_real_escape_string($username); <br/>
    удобнее, чем
    $command->bindParam(":username",$username,PDO::PARAM_STR); <br/>

    Достоинства

    • Повышается читабельность
    • Не требуется отдельная функция, отвечающая за фильтрацию
    • Если принять такое обращение в БД за стиль, то про SQL-инъекции можно забыть.

    Недостатки

    • В начале придется помучаться, чтобы перейти на использование таких запросов
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 38

      0
      Все пробелы в коде убираются. Чем мне код отформатировать?
        +1
        Отформатировать и подсветить попробуйте этим или этим.

        По теме: давно заметил, что во многих классах абстрактакции БД есть такая полезная функция, как prepare. Например:
        1. $query = $db->prepare("SELECT * FROM table WHERE (id = ? AND user_id = ?)");
        2. $db->execute($query, array($_id, $_user_id));
        Но зачастую она не фильтровала переменные, приходилось дописывать самому :)
          0
          Мда… Время идет, а люди не меняются. Последнюю строчку предыдущего поста можете не читать :)
            0
            Спасибо, отформатировал.
            +1
            [pre]

            и отключить автоформатирование текста
            –8
            а почему бы не делать просто $username=htmlspecialchars($_GET['username']);?
              0
              Во-первых, htmlspecialchars() создан для совсем других целей и кавычки он не экранирует — можете проверить «kolpeex' or 1=1»==htmlspecialchars(«kolpeex' or 1=1»). У вас уже уязвимость :-)
              Во-вторых, я написал в начале, что многие забывают о фильтрации.
              В-третьих, из-за большого количества подобных строк код выглядит хуже. Слишком много в нем может быть $v = filter($v);. А тут две строки: запрос и его выполнение. Параметры прямо от пользователя.
                –1
                Что для других целей это да. А вообще, htmlspecialchars и одиночные кавычки заменяет на сущности, если его запускать в таком виде htmlspecialchars($argument, ENT_QUOTES).
                  +1
                  уважаемый, а потом вы собираетесь кавычки реплейсить обратно?
                    0
                    Не совсем понял вопроса, я просто напомнил человеку возможности функции. Зачем реплейсить обратно?

                    Лично для себя я htmlspecialchars использовал только при вставке данных, которые потом просто надо показать в браузере в виде, в котором их ввел пользователь (например какой то код).
                      0
                      А реплейсить их обратно потребуется, например, если Вы захотите отправить юзверю письмо в plain_text формате, или там сделать экспорт в CSV-формат, ну Вы поняли.

                      Приведенный пример очень плох, т.к. он смешивает данные с особенностями их представления в браузере. Как только Вам захочется получить данные в чистом виде, без html-сучностей, Вы имеете проблемы.
                        –1
                        С тем что данные смешал согласен. Тогда просто помещать в базу экранированные/сериализованные данные, а в браузер через htmlspecialchars.
                          +1
                          В базе должны лежать ЧИСТЫЕ данные. Экранировать при выводе в конкретный формат, в соответствии с его требованиями.
                            +1
                            Был неправ. Запомню на будущее как надо. Спасибо!
                              0
                              Должны… Млять — никто тебе ничего не должен. Если ты кончаешь от своих ЧИСТЫХ данных в базе — это лично твое дело.

                              Да и вообще все зависит от поставленной задачи. Если нужно чтобы хранились тэги- храним тэги. Не надо — не храним.

                              А лишний раз стр/прег реплайсе юзать — помоему нафиг не нужно.

                              На счет примера аффтора — он бесспорно имеет право существовать
                                0
                                Да пожалуйста пожалуйста.
                                Умудри тебя Господь (с)
                          0
                          например вам пришел xml документ который вы хотите положить в бд или любая другая структура свойства которой вам нужно сохранить не изменив сути (те не заменив ', <> и тд на спец символы)
                            0
                            Тогда документ/данные экранировать или сериализовать. Вы мне это объясняете будто я пропагандирую повальное использование htmlspecialchars. Все надо использовать в зависимости от ситуации.
                  0
                  А еще можно не придумывать велосипед, а использовать существующие ORM, например doctrine или propel
                    0
                    Велосипед у них. У меня target-specified.
                      0
                      К тому же эти продукты для более широкого применения. Использовать их для фильтрации не есть труЪ :-)
                      +5
                      Или подумать ещё и просто взять PDO. Эта статья предназначена для новичков, которые в 99% случаев имеют лишь несколько запросов во всей программе, и использовать ради этого пропел — есть битьё из пушки по воробьям. Со временем новичок сам к дойдёт до использования ORM, когда достигнет надлежащего уровня и соответственного размера и сложноcти приложений.
                        +1
                        + вам в карму за PDO (сам не использую, так что за дравую мысль)
                          +1
                          Все верно.
                        +3
                        Такой подход вроде как называют placeholder'ами, действительно действенно и удобно, и исключает саму возможность допустить SQL-инъекцию. Я лично в качестве варианта подстановки использую sprintf, где помимо всего прочего можно привести аргумент к конкретному типу. Кстати насчет готовых решений — такой подход вроде-бы присутствует в PDO.

                        Да и вообще, даже комментариях мануала по PHP советуют этот метод, в самом простом его варианте — mysql_query(sprintf($query, $arg1, $arg2)); — остается только один вопрос, почему этот подход упорно игнорируют разработчики популярных скриптов… не, ну на самом деле, какому-нибудь phpBB уже лет по десять, а как находили в нем SQL-инъекции так и находят по сей день.
                          +3
                          в pdo есть все что нужно для фильтрации, смотрим ru.php.net/manual/en/pdo.prepare.php
                          а если задать свой PDOStatement то можно получить:
                          Copy Source | Copy HTML
                          1.     public function bindValue($Parameter, $Value, $DataType = NULL)
                          2.     {
                          3.         if ($DataType === NULL)
                          4.         {
                          5.             $DataType = gettype($Value);
                          6.         }
                          7.         $DataType = strtolower($DataType);
                          8.         if ($DataType === 'boolean' || $DataType === 'bool' || $DataType === PDO::PARAM_BOOL)
                          9.         {
                          10.             $DataType = PDO::PARAM_BOOL;
                          11.         }
                          12.         elseif ($DataType === 'null' || $DataType === PDO::PARAM_NULL)
                          13.         {
                          14.             $DataType = PDO::PARAM_NULL;
                          15.         }
                          16.         elseif ($DataType === 'integer' || $DataType === 'int' || $DataType === PDO::PARAM_INT)
                          17.         {
                          18.             $DataType = PDO::PARAM_INT;
                          19.         }
                          20.         elseif ($DataType === 'string' || $DataType === 'str' || $DataType === PDO::PARAM_STR)
                          21.         {
                          22.             $DataType = PDO::PARAM_STR;
                          23.         }
                          24.         elseif ($DataType === 'lob' || $DataType === PDO::PARAM_LOB)
                          25.         {
                          26.             $DataType = PDO::PARAM_LOB;
                          27.         }
                          28.         elseif ($DataType === 'stmt' || $DataType === PDO::PARAM_STMT)
                          29.         {
                          30.             $DataType = PDO::PARAM_STMT;
                          31.         }
                          32.         else
                          33.         {
                          34.             $DataType = PDO::PARAM_STR;
                          35.         }
                          36.  
                          37.         return parent::bindValue($Parameter, $Value, $DataType);
                          38.     }
                          39.  
                          40. $stmt->bindValue(":val1", 'string'); //string
                          41. $stmt->bindValue(":val2", 123); //integer
                          42. $stmt->bindValue(":val3", TRUE, 'int'); //integer


                          sprintf кстати тоже не плохой вариант, тока не кидает эксепшен в случае ошибок %)
                            0
                            Про double забыли )
                          +1
                          Вряд-ли на хабре найдётся тот то напрямую подставляет значения массива GET в SQL-запрос, так как почти в любом мануале это твердят не один раз. А если и найдётся — то туда ему и дорога.
                            0
                            Рано или подно, но все, что предназначено для БД, попадает в SQL.
                            Использование дополнительной переменной от SQL-инъекций не избавляет :-)
                            +1
                            Для этого отлично подходит Dbsimple, удобные плэйсхолдеры и условные выражения
                              –1
                              Начальные примеры кода каие-то странные. Кто запрещает переменные с короткими названиями вставлять в код. Плейсхолдеры с валидацией по выбору пользователя реализованы очень приятно в goDB.
                                0
                                mysql_real_escape_string очень сильно увеличивает размер строки. Пример есть пример.
                              • UFO just landed and posted this here
                                  0
                                  блокирование чуваков которые шлют не то что надо

                                  Ни в коем случае этого делать нельзя. Я вот очень злюсь, когда меня при пен-тестинге блокируют, и низачто после этого (IP меняется довольно-таки просто) не стану писать отчет администратору. Плохой пример: video.xaknet.ru/

                                  А не вставлять проверки хрен нает где… что бы потом охреневать с того что все фильтры разбросаны по хреновой тучи файлов.

                                  Для этого и нужны параметризованные запросы — «спрятать» фильтрацию данных, чтобы глаза не мозолила.
                                  +1
                                  Меня удивляет то кол-во народа, которые либо придумывают или используют чей то велосипед, или отсылают к Doctrine, Propel и тому подобным.
                                  Один хабраюзер высказал здравую мысль о PDO, за что ему плюс в карму. Однако все поголовно забыли про mysqli и его prepared statments. Уж что-что, а эта библиотека давно в составе PHP и на ней работает phpMyAdmin. Стандартная mysql не поддерживает новые возможности MySQL версией 4.1 и выше. Один multi_query при правильном использовании чего стоит, а PDO в моём понимании лишняя абстракция, к тому же банальное использование LIMIT в запросах (кто-нить пишет запросы без LIMIT для постраничного вывода?) не будет работать нигде, кроме MySQL.
                                    0
                                    немного смущает то что данные передаются по ссылке см php.net/manual/en/mysqli-stmt.bind-param.php хотя наличие класса MySQLi_STMT дает возможность это исправить
                                    судя по бенчмаркам blogs.vinuthomas.com/2006/08/07/benchmark-mysql-mysqli-pdo-in-php/ разницы особой нет (хотя на такое стыдно ссылаться, ни методики тестирования, ни кода)
                                    про использование лимит давайте подробнее, мускул умеет LIMIT OFFSET если вы об этом
                                    пример правильного на ваш взгляд multi_query тоже стоит привести
                                    заранее спасибо
                                      0
                                      В моем проекте по веским причинам использовался SQLite, который, кажется, не предоставляет таких возможностей.
                                      0
                                      В Друпале например используется подстановка с помощью sprintf плюс некоторая дополнительная фильтрация.

                                      Only users with full accounts can post comments. Log in, please.