Как защититься от SQL-инъекций, когда предложение WHERE создается динамически из формы поиска?

Я знаю, что единственный действительно правильный способ защитить SQL-запросы от SQL-инъекций в Java — это использовать PreparedStatements.

Однако такой оператор требует, чтобы базовая структура (выбранные атрибуты, присоединяемые таблицы, структура условия WHERE) не менялась.

У меня есть JSP-приложение, содержащее форму поиска с дюжиной полей. Но пользователь не обязан заполнять их все — только тот, который ему нужен. Таким образом, мое условие WHERE каждый раз разное.

Что мне делать, чтобы по-прежнему предотвращать внедрение SQL?
Экранировать введенные пользователем значения? Написать класс-оболочку, который каждый раз создает PreparedStatement? Или что-то другое?

База данных - PostgreSQL 8.4, но я бы предпочел общее решение.

Заранее большое спасибо.


person Christoph Wurm    schedule 12.11.2010    source источник
comment
Этот вопрос должен быть дубликатом хотя бы одного из следующих: stackoverflow.com/questions/485023/ | stackoverflow.com/questions/1812891 / | stackoverflow .com/questions/350177/ | stackoverflow.com/questions/1115739 /   -  person T.J. Crowder    schedule 12.11.2010
comment
Это тот, с которым я пошел: stackoverflow.com/questions/1812891/ Ответ в основном: используйте PreparedStatement.   -  person T.J. Crowder    schedule 12.11.2010
comment
Я прочитал большинство из них, прежде чем опубликовать этот вопрос, но ни один из них, похоже, не имел дело с динамическими SQL-запросами.   -  person Christoph Wurm    schedule 12.11.2010
comment
@Т.Дж. Краудер: У них есть статический INSERT. У меня есть динамический SELECT. Я уже использую PreparedStatements для своих статических запросов.   -  person Christoph Wurm    schedule 12.11.2010
comment
Принцип тот же, независимо от того, имеете ли вы дело с insert или select: используйте PreparedStatement для всех пользовательских значений. Ваша проблема на самом деле не отличается от других, за исключением того, что вам нужно создать SQL для оператора (и отдельно параметры), но суть одна и та же.   -  person T.J. Crowder    schedule 12.11.2010


Ответы (5)


Вы видели JDBC ИменованныйПараметрJDBCTemplate ?

В класс NamedParameterJdbcTemplate добавлена ​​поддержка программирования операторов JDBC с использованием именованных параметров (в отличие от программирования операторов JDBC с использованием только классических аргументов-заполнителей ('?').

Вы можете делать такие вещи, как:

String sql = "select count(0) from T_ACTOR where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return namedParameterJdbcTemplate.queryForInt(sql, namedParameters);

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

person Brian Agnew    schedule 12.11.2010
comment
Спасибо Брайан, это выглядит очень интересно. Я обязательно посмотрю, в том числе и на его зависимости, так как я бы предпочел не добавлять всю библиотеку Spring в наше приложение. - person Christoph Wurm; 12.11.2010
comment
Я думаю, что Spring JDBC должен быть достаточно автономным. - person Brian Agnew; 12.11.2010

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

Это все же не то же самое, что использование пользовательских данных в SQL-запросе, для которого вы определенно хотите использовать PreparedStatement. На самом деле это очень похоже на стандартную проблему необходимости использования оператора in с PreparedStatement (например, where fieldName in (?, ?, ?), но вы не знаете заранее, сколько ? вам понадобится). Вам просто нужно создать запрос динамически и динамически добавлять параметры на основе информации, предоставленной пользователем (но не напрямую включая эту информацию в запрос).

Вот пример того, что я имею в виду:

// You'd have just the one instance of this map somewhere:
Map<String,String> fieldNameToColumnName = new HashMap<String,String>();
// You'd actually load these from configuration somewhere rather than hard-coding them
fieldNameToColumnName.put("title", "TITLE");
fieldNameToColumnName.put("firstname", "FNAME");
fieldNameToColumnName.put("lastname", "LNAME");
// ...etc.

// Then in a class somewhere that's used by the JSP, have the code that
// processes requests from users:
public AppropriateResultBean[] doSearch(Map<String,String> parameters)
throws SQLException, IllegalArgumentException
{
    StringBuilder           sql;
    String                  columnName;
    List<String>            paramValues;
    AppropriateResultBean[] rv;

    // Start the SQL statement; again you'd probably load the prefix SQL
    // from configuration somewhere rather than hard-coding it here.
    sql = new StringBuilder(2000);
    sql.append("select appropriate,fields from mytable where ");

    // Loop through the given parameters.
    // This loop assumes you don't need to preserve some sort of order
    // in the params, but is easily adjusted if you do.
    paramValues = new ArrayList<String>(parameters.size());
    for (Map.Entry<String,String> entry : parameters.entrySet())
    {
        // Only process fields that aren't blank.
        if (entry.getValue().length() > 0)
        {
            // Get the DB column name that corresponds to this form
            // field name.
            columnName = fieldNameToColumnName.get(entry.getKey());
                      // ^-- You'll probably need to prefix this with something, it's not likely to be part of this instance
            if (columnName == null)
            {
                // Somehow, the user got an unknown field into the request
                // and that got past the code calling us (perhaps the code
                // calling us just used `request.getParameterMap` directly).
                // We don't allow unknown fields.
                throw new IllegalArgumentException(/* ... */);
            }
            if (paramValues.size() > 0)
            {
                sql.append("and ");
            }
            sql.append(columnName);
            sql.append(" = ? ");
            paramValues.add(entry.getValue());
        }
    }

    // I'll assume no parameters is an invalid case, but you can adjust the
    // below if that's not correct.
    if (paramValues.size() == 0)
    {
        // My read of the problem being solved suggests this is not an
        // exceptional condition (users frequently forget to fill things
        // in), and so I'd use a flag value (null) for this case. But you
        // might go with an exception (you'd know best), either way.
        rv = null;
    }
    else
    {
        // Do the DB work (below)
        rv = this.buildBeansFor(sql.toString(), paramValues);
    }

    // Done
    return rv;
}

private AppropriateResultBean[] buildBeansFor(
    String sql,
    List<String> paramValues
)
throws SQLException
{
    PreparedStatement       ps      = null;
    Connection              con     = null;
    int                     index;
    AppropriateResultBean[] rv;

    assert sql != null && sql.length() > 0);
    assert paramValues != null && paramValues.size() > 0;

    try
    {
        // Get a connection
        con = /* ...however you get connections, whether it's JNDI or some conn pool or ... */;

        // Prepare the statement
        ps = con.prepareStatement(sql);

        // Fill in the values
        index = 0;
        for (String value : paramValues)
        {
            ps.setString(++index, value);
        }

        // Execute the query
        rs = ps.executeQuery();

        /* ...loop through results, creating AppropriateResultBean instances
         * and filling in your array/list/whatever...
         */
        rv = /* ...convert the result to what we'll return */;

        // Close the DB resources (you probably have utility code for this)
        rs.close();
        rs = null;
        ps.close();
        ps = null;
        con.close(); // ...assuming pool overrides `close` and expects it to mean "release back to pool", most good pools do
        con = null;

        // Done
        return rv;
    }
    finally
    {
        /* If `rs`, `ps`, or `con` is !null, we're processing an exception.
         * Clean up the DB resources *without* allowing any exception to be
         * thrown, as we don't want to hide the original exception.
         */
    }
}

Обратите внимание, как мы используем информацию, предоставленную нам пользователем (поля, которые он заполнил), но мы никогда не помещали ничего, что они на самом деле предоставили напрямую в выполняемый нами SQL, мы всегда пропускали его через PreparedStatement.

person T.J. Crowder    schedule 12.11.2010
comment
Он похож на in(?, ?, ?, ...), но есть важное отличие, которое делает его решаемым другим способом — см. мой только что добавленный ответ. - person Andrew Spencer; 22.12.2015

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

Список имен столбцов может быть, но он конечен и исчисляем. Пусть JSP заботится о том, чтобы сделать выбор пользователя известным среднему уровню; пусть средний уровень связывается и проверяется перед отправкой в ​​​​базу данных.

person duffymo    schedule 12.11.2010
comment
В моем конкретном случае список имен столбцов на самом деле статичен. Но у меня может быть от одного до пятнадцати условий в предложении WHERE. - person Christoph Wurm; 12.11.2010
comment
Он всегда статичен, если только вы не планируете менять таблицу на лету. Проблема в количестве комбинаций? Даже при наличии 15 столбцов количество комбинаций быстро увеличивается. Сколько одновременно может запросить пользователь? - person duffymo; 12.11.2010

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

Будет ли ваш пользователь искать по названию?

select id, title, author from book where title = :title

Или по автору?

select id, title, author from book where author = :author

Или оба?

select id, title, author from book where title = :title and author = :author

Достаточно плохо только с 2 полями. Количество комбинаций (и, следовательно, различных PreparedStatements) растет экспоненциально с количеством условий. Верно, скорее всего, у вас достаточно места в пуле PreparedStatement для всех этих комбинаций, и для программного построения предложений в Java вам просто нужна одна ветвь if для каждого условия. Все равно не так красиво.

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

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

Вот:

select id, title, author
from book
where coalesce(:title, title) = title
and coalesce(:author, author) = author

Затем вы указываете NULL для каждого неиспользуемого условия. coalesce() — это функция, которая возвращает свой первый ненулевой аргумент. Таким образом, если вы передадите NULL вместо :title, первое предложение будет where coalesce(NULL, title) = title, которое оценивается как where title = title, которое, будучи всегда истинным, не влияет на результаты.

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

(Хотя эта проблема и похожа, она не аналогична проблеме с предложением IN (?, ?, ?), когда вы не знаете количество значений в списке, так как здесь у вас знает фиксированное количество возможных пунктов, и вам просто нужно активировать/деактивировать их по отдельности.)

person Andrew Spencer    schedule 22.12.2015

Я не уверен, что существует метод quote(), который широко использовался в PHP PDO. Это позволит вам использовать более гибкий подход к построению запросов.

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

person nefo_x    schedule 12.11.2010