Избегать инъекции SQL без параметров

У нас есть еще одно обсуждение здесь, когда мы работаем над использованием параметризованных sql-запросов в нашем коде. У нас есть две стороны в обсуждении: Я и некоторые другие, которые говорят, что мы всегда должны использовать параметры для защиты от инъекций sql и других парней, которые не считают это необходимым. Вместо этого они хотят заменить одиночные апострофы двумя апострофами во всех строках, чтобы избежать инъекций sql. Наши базы данных работают под управлением Sql Server 2005 или 2008, а наша база кода работает в .NET framework 2.0.

Позвольте мне привести простой пример в С#:

Я хочу, чтобы мы использовали это:

string sql = "SELECT * FROM Users WHERE [email protected]";
SqlCommand getUser = new SqlCommand(sql, connection);
getUser.Parameters.AddWithValue("@name", userName);
//... blabla - do something here, this is safe

В то время как другие ребята хотят это сделать:

string sql = "SELECT * FROM Users WHERE Name=" + SafeDBString(name);
SqlCommand getUser = new SqlCommand(sql, connection);
//... blabla - are we safe now?

Если функция SafeDBString определяется следующим образом:

string SafeDBString(string inputValue) 
{
    return "'" + inputValue.Replace("'", "''") + "'";
}

Теперь, пока мы используем SafeDBString для всех строковых значений в наших запросах, мы должны быть в безопасности. Правильно?

Есть две причины использовать функцию SafeDBString. Во-первых, это так, как это было сделано с каменного века, а во-вторых, проще отлаживать SQL-команды, так как вы видите запрос excact, который запускается в базе данных.

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

Есть ли там кто-нибудь, кто может это сломать? Как бы вы это сделали?

EDIT: Подводя итог ответам:

  • Никто не нашел способ обойти SafeDBString на Sql Server 2005 или 2008 еще. Это хорошо, я думаю?
  • В нескольких ответах указывалось, что вы получаете выигрыш в производительности при использовании параметризованных запросов. Причина в том, что планы запросов могут быть повторно использованы.
  • Мы также согласны с тем, что использование параметризованных запросов дает более читаемый код, который проще поддерживать
  • Кроме того, легче всегда использовать параметры, чем использовать различные версии SafeDBString, преобразование строк в число и преобразование строк в дату.
  • Используя параметры, вы получаете автоматическое преобразование типов, что особенно полезно, когда мы работаем с датами или десятичными числами.
  • И, наконец, Не пытайтесь делать безопасность самостоятельно, как писал JulianR. Поставщики баз данных тратят много времени и денег на безопасность. Мы не можем сделать ничего лучше и не будем пытаться выполнять свою работу.

Так что, пока никто не мог нарушить простую безопасность функции SafeDBString, у меня появилось много других хороших аргументов. Спасибо!

Ответ 1

Думаю, правильный ответ:

Не пытайтесь самостоятельно выполнять безопасность. Используйте любую надежную, стандартную библиотеку, доступную для того, что вы пытаетесь сделать, а не пытаетесь сделать это сами. Какие бы предположения вы ни делали о безопасности, может быть неверным. Насколько это безопасно, как может выглядеть ваш собственный подход (и в лучшем случае он выглядит шатким), есть риск, что вы что-то упускаете, и действительно ли вы хотите воспользоваться этим шансом, когда дело касается безопасности?

Используйте параметры.

Ответ 2

И затем кто-то идет и использует "вместо". Параметры - это ИМО, единственный безопасный способ.

Он также избегает множества проблем i18n с датами/цифрами; какая дата 01/02/03? Сколько стоит 123 456? Поддерживают ли ваши серверы (сервер приложений и db-сервер) друг с другом?

Если фактор риска для них не убедителен, как насчет эффективности? RDBMS может повторно использовать план запроса, если вы используете параметры, помогая производительности. Он не может сделать это только с помощью строки.

Ответ 3

Аргумент беспроигрышный. Если вам удастся найти уязвимость, ваши сотрудники просто изменят функцию SafeDBString для ее учета, а затем попросят вас снова доказать, что она небезопасна.

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

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

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

Ответ 4

Прежде всего, ваш образец для версии "Заменить" неверен. Вам нужно поставить апострофы вокруг текста:

string sql = "SELECT * FROM Users WHERE Name='" + SafeDBString(name) & "'";
SqlCommand getUser = new SqlCommand(sql, connection);

Итак, для вас нужны другие параметры: вам не нужно беспокоиться о том, нужно ли включать значение в кавычки. Конечно, вы можете создать это в функции, но тогда вам нужно добавить много сложностей в функцию: как узнать разницу между "NULL" как "null" и "NULL" как просто строку или между числом и строка, которая просто содержит много цифр. Это просто еще один источник ошибок.

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

Кроме того, экранирование одиночных кавычек недостаточно.. Многие продукты БД позволяют использовать альтернативные методы для экранирования символов, которые может использовать злоумышленник. В MySQL, например, вы также можете избежать одиночной кавычки с обратной косой чертой. И поэтому следующее значение "name" взорвало бы MySQL только с помощью функции SafeDBString(), потому что, когда вы удваиваете одиночную кавычку, первая из них все же экранируется обратной косой чертой, оставив вторую "активной":

x\'OR 1 = 1; -


Кроме того, JulianR поднимает хороший момент ниже: НИКОГДА не пытайтесь самостоятельно выполнять работу по обеспечению безопасности. Это так просто, чтобы заставить программирование безопасности ошибочно тонкими способами, которые, кажется, работают, даже при тщательном тестировании. Затем проходит время, и через год вы узнаете, что ваша система была взломана шесть месяцев назад, и вы даже не знали об этом до этого момента.

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

Ответ 5

Итак, я бы сказал:

1) Почему вы пытаетесь повторно внедрить что-то, что было встроено? он там, легко доступен, прост в использовании и уже отлажен в глобальном масштабе. Если в нем обнаружены будущие ошибки, они будут исправлены и доступны для всех очень быстро, если вы ничего не сделаете.

2) Какие процессы существуют, чтобы гарантировать, что вы никогда не пропустите вызов SafeDBString? Отсутствие всего лишь одного места может открыть целый ряд проблем. Сколько вы собираетесь наблюдать за этими вещами и подумайте о том, сколько усилий потрачено на то, когда приемлемый правильный ответ так легко достичь.

3) Насколько вы уверены, что вы закрыли каждый вектор атаки, который Microsoft (автор базы данных и библиотека доступа) знает о вашей реализации SafeDBString...

4) Насколько легко читать структуру sql? В примере используется + конкатенация, параметры очень похожи на string.Format, что более читаемо.

Кроме того, есть два способа разработки того, что было на самом деле запущено, - сворачивайте свою собственную функцию LogCommand, простую функцию без каких-либо проблем с безопасностью или даже просматривайте трассировку sql, чтобы понять, что думает база данных.

Наша функция LogCommand проста:

    string LogCommand(SqlCommand cmd)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine(cmd.CommandText);
        foreach (SqlParameter param in cmd.Parameters)
        {
            sb.Append(param.ToString());
            sb.Append(" = \"");
            sb.Append(param.Value.ToString());
            sb.AppendLine("\"");
        }
        return sb.ToString();
    }

Правильно или неправильно, это дает нам информацию, которая нам нужна без проблем безопасности.

Ответ 6

С параметризованными запросами вы получаете больше защиты от SQL-инъекции. Вы также получаете лучший потенциал кэширования плана выполнения. Если вы используете профилировщик запросов sql server, вы все равно можете увидеть "точный sql, который запускается в базе данных", поэтому вы не теряете ничего с точки зрения отладки своих операторов sql.

Ответ 7

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

Ответ 8

Вы не можете легко выполнять проверку типов ввода пользователя без использования параметров.

Если вы используете классы SQLCommand и SQLParameter, чтобы вы делали вызовы БД, вы все равно можете увидеть выполняемый SQL-запрос. Посмотрите на свойство SQLCommand CommandText.

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

Ответ 9

Это безопасно, если вам гарантировано, что вы собираетесь передать строку.

Что делать, если в какой-то момент вы не передаете строку? Что делать, если вы передаете только число?

http://www.mywebsite.com/profile/?id=7;DROP DATABASE DB

В итоге получится:

SELECT * FROM DB WHERE Id = 7;DROP DATABASE DB

Ответ 10

Я бы использовал хранимые процедуры или функции для всего, поэтому вопрос не возник бы.

Где я должен поместить SQL в код, я использую параметры, что единственное, что имеет смысл. Напомните несогласным, что есть хакеры умнее, чем есть, и с лучшим стимулом нарушить код, который пытается перехитрить их. Используя параметры, это просто невозможно, и это не так сложно.

Ответ 11

Внимательно соглашайтесь с вопросами безопасности.
Другая причина использования параметров - эффективность.

Базы данных всегда будут компилировать ваш запрос и кэшировать его, а затем повторно использовать кешированный запрос (что, очевидно, быстрее для последующих запросов). Если вы используете параметры, то даже если вы используете разные параметры, база данных будет повторно использовать ваш кешированный запрос, поскольку он соответствует на основе строки SQL, прежде чем связывать параметры.

Если вы не привязываете параметры, то строка SQL изменяется на каждый запрос (который имеет разные параметры), и он никогда не будет соответствовать тому, что в вашем кеше.

Ответ 12

С самого короткого времени, которое мне пришлось исследовать при проблемах с SQL-инъекциями, я вижу, что создание ценности "безопасно" также означает, что вы закрываете дверь в ситуации, когда вам действительно нужны апострофы в ваших данных - что о имени кого-то, например, О'Рейли.

Это оставляет параметры и хранимые процедуры.

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

Ответ 13

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

http://www.sommarskog.se/dynamic_sql.html

http://unixwiz.net/techtips/sql-injection.html

Лично я предпочитаю никогда не позволять динамическому коду касаться моей базы данных, требуя, чтобы все контакты проходили через sps (а не тот, который использует динамический SQl). Это означает, что ничего из того, что я дал пользователям разрешить делать, может быть сделано, и что внутренние пользователи (за исключением немногих с производственным доступом для целей администратора) не могут напрямую обращаться к моим таблицам и создавать хаос, красть данные или совершать мошенничество. Если вы запустите финансовое приложение, это самый безопасный способ.

Ответ 14

Я не видел, чтобы какие-либо другие ответчики обращались к этой стороне "зачем делать это сами плохо", но рассмотрите атаку SQL Truncation,

Существует также функция QUOTENAME T-SQL, которая может быть полезна, если вы не можете убедить их использовать параметры. Он ловит много (все?) Проблем, связанных с qoute.

Ответ 15

Он может быть сломан, однако средства зависят от точных версий/патчей и т.д.

Тот, который уже был поднят, является ошибкой переполнения/усечения, которая может быть использована.

Другим будущим средством было бы найти ошибки, похожие на другие базы данных - например, в стеке MySQL/PHP возникла проблема экранирования, потому что некоторые определенные последовательности UTF8 могли использоваться для управления функцией замены - функция замены была бы обманута введением символов инъекции.

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

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

Ответ 16

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

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

var bldr = new SqlBuilder( myCommand );
bldr.Append("SELECT * FROM CUSTOMERS WHERE ID = ").Value(myId, SqlDbType.Int);
//or
bldr.Append("SELECT * FROM CUSTOMERS WHERE NAME LIKE ").FuzzyValue(myName, SqlDbType.NVarChar);
myCommand.CommandText = bldr.ToString();

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

Класс выглядит следующим образом:

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace myNamespace
{
    /// <summary>
    /// Pour le confort et le bonheur, cette classe remplace StringBuilder pour la construction
    /// des requêtes SQL, avec l'avantage qu'elle gère la création des paramètres via la méthode
    /// Value().
    /// </summary>
    public class SqlBuilder
    {
        private StringBuilder _rq;
        private SqlCommand _cmd;
        private int _seq;
        public SqlBuilder(SqlCommand cmd)
        {
            _rq = new StringBuilder();
            _cmd = cmd;
            _seq = 0;
        }
        //Les autres surcharges de StringBuilder peuvent être implémenté ici de la même façon, au besoin.
        public SqlBuilder Append(String str)
        {
            _rq.Append(str);
            return this;
        }
        /// <summary>
        /// Ajoute une valeur runtime à la requête, via un paramètre.
        /// </summary>
        /// <param name="value">La valeur à renseigner dans la requête</param>
        /// <param name="type">Le DBType à utiliser pour la création du paramètre. Se référer au type de la colonne cible.</param>
        public SqlBuilder Value(Object value, SqlDbType type)
        {
            //get param name
            string paramName = "@SqlBuilderParam" + _seq++;
            //append condition to query
            _rq.Append(paramName);
            _cmd.Parameters.Add(paramName, type).Value = value;
            return this;
        }
        public SqlBuilder FuzzyValue(Object value, SqlDbType type)
        {
            //get param name
            string paramName = "@SqlBuilderParam" + _seq++;
            //append condition to query
            _rq.Append("'%' + " + paramName + " + '%'");
            _cmd.Parameters.Add(paramName, type).Value = value;
            return this; 
        }

        public override string ToString()
        {
            return _rq.ToString();
        }
    }
}

Ответ 17

Вот несколько причин для использования параметризованных запросов:

  • Безопасность. Уровень доступа к базе данных знает, как удалить или удалить элементы, которые не разрешены в данных.
  • Разделение проблем. Мой код не отвечает за преобразование данных в формат, который нравится базе данных.
  • Нет избыточности - мне не нужно включать сборку или класс в каждый проект, который форматирует/экранирует эту базу данных; он встроен в библиотеку классов.

Ответ 18

Было мало уязвимостей (я не помню, какая именно база данных) была связана с переполнением буфера SQL-запроса.

Я хочу сказать, что SQL-Injection - это больше, чем просто "избежать цитаты", и вы не знаете, что будет дальше.

Ответ 19

Еще одно важное соображение - отслеживание экранированных и неэкранированных данных. Есть тонны и тонны приложений, Web и других, которые, похоже, не отслеживают, когда данные являются необработанными - Unicode, & -encoded, форматированный HTML и т.д. Очевидно, что будет сложно отслеживать, какие строки '' -кодированы, а какие нет.

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

Ответ 20

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

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

Ответ 21

Через 2 года, я переработал... Любой, кто находит параметры боли, может попробовать мое расширение VS, QueryFirst. Вы редактируете свой запрос в реальном файле .sql(Validation, Intellisense). Чтобы добавить параметр, просто введите его прямо в свой SQL, начиная с "@". Когда вы сохраняете файл, QueryFirst будет генерировать классы-оболочки, чтобы вы могли запускать запрос и получать доступ к результатам. Он будет искать тип DB вашего параметра и сопоставить его с типом .net, который вы найдете в качестве входных данных для сгенерированных методов Execute(). Не может быть проще. Выполнение этого правильного пути радикально быстрее и проще, чем в любом другом случае, и создание уязвимости в sql-инъекции становится невозможным или, по крайней мере, крайне сложно. Существуют и другие преимущества убийцы, такие как возможность удалять столбцы в вашей БД и сразу видеть ошибки компиляции в вашем приложении.

Правовая оговорка: я написал QueryFirst