[Moscow.pm] Уязвимость при использовании dataTables и DBIx::Class

Grigory Batalov grisxa на gmail.com
Чт Дек 24 12:47:28 PST 2015


Всем привет!

Коллеги нашли в моём коде уязвимость, спешу поделиться, как не надо писать :)
Мне эта тема кажется интересной, может ещё кому-то пригодится.

В одном веб-проекте я использую фреймворк Mojolicious, плагин jQuery
dataTables.net и ORM DBIx::Class. DataTables (в режиме serverSide: true)
получает от сервера JSON и заполняет таблицы, которые можно сортировать
по колонкам или применить фильтр поиска. При сортировке или фильтрации
на сервер поступает новый запрос и приходит новый JSON-ответ, заполняющий
таблицу.

Параметры запроса примерно такие:
    columns[0][data]            "username",
    columns[0][name]            "",
    columns[0][orderable]       "true",
    columns[0][searchable]      "true",
    columns[0][search][regex]   "false",
    columns[0][search][value]   "",
    columns[1][data]            "filename",
    columns[1][name]            "",
    columns[1][orderable]       "true",
    columns[1][searchable]      "true",
    columns[1][search][regex]   "false",
    columns[1][search][value]   "",
    length                      10,
    order[0][column]            1,
    order[0][dir]               "asc",
    search[regex]               "false",
    search[value]               "",
    start                       0

Конечно, я не разбираю их вручную, а поручаю это модулю
JQuery::DataTables::Request. Как видите, в запросе указан номер колонки
для сортировки (order) и направление.

    my $req =
      JQuery::DataTables::Request->new(
        client_params => $self->req->body_params->to_hash );

    my $sort_column    = $req->column( $req->order(0)->{'column'} )->{'data'};
    my $sort_direction = $req->order(0)->{'dir'};

DBIx::Class же умеет сортировать выборку из таблицы в базе данных
(resultset) так: search( {...}, { order_by => { -desc => 'filename' } } ).
Заманчиво указать прямо здесь параметры, полученные от dataTables :)

    my $files = $schema->resultset('File')->search(
       { purge => 0 },
       { order_by => { "-$sort_direction" => $sort_column }
    );

DBIx::Class (посредством SQL::Abstract) обычно формирует запрос с подстановками
типа

SELECT filename FROM files WHERE SIZE > ? LIMIT ? : '1024', '10'

Но не в этом случае! Если вмешаться в запрос и в колонку вписать

columns[1][data] "id, (DELETE FROM files;)"

в SQL-запросе появится непосредственно

ORDER BY id, (DELETE FROM files;) ASC

К счастью, такие наивные попытки уязвить базу данных пресекаются проверкой
в SQL::Abstract:

SQL::Abstract::puke(): [SQL::Abstract::_assert_pass_injection_guard]
Fatal: Possible SQL injection attempt 'id, (DELETE FROM files;)'
If this is indeed a part of the desired SQL use literal SQL ( '...' or [ '...' ] )
or supply your own {injection_guard} attribute to DBIx::Class::SQLMaker->new()

Тем не менее, есть и более изощрённые методы. Что произойдёт, если вместо
названия колонки указать

columns[1][data] "id, (select 1 from (SELECT name, CASE name='john' WHEN FALSE THEN pg_sleep(0) ELSE pg_sleep(10) END from users) as t limit 1)"
	
? (это PostgreSQL) В хвосте SQL-запроса окажется

ORDER BY me.id, (select 1 from (SELECT name, CASE name='first' WHEN FALSE THEN pg_sleep(0) ELSE pg_sleep(10) END from users) as t limit 1) ASC

Т.е. помимо выполнения обычного запроса (он не испорчен), произойдёт поиск пользователя
john в таблице users. При неудаче ответ вернётся сразу, а в случае успеха - через 10 сек.
Таким образом, злоумышленник, генерируя запросы через веб-интерфейс, может перебором
определить содержимое таблицы! Такой "слепой" метод доступа к базе данных называется
Blind SQL Injection и широко используется нашими нечистоплотными коллегами. В сети
есть автоматические средства для перебора.

Подытоживая эту историю, посыпаю голову пеплом и напоминаю, что никаким входным данным
нельзя доверять! :) Любая мелочь может привести если не к порче информации, то к утечке.

Я же вынес сортировку в отдельный файл ResultSet/File.pm (там ещё кое-что есть),
добавив проверку:

# apply sorting order
sub order_by {
    my $self = shift;
    my ( $column, $direction ) = @_;

    # an unknown column name is given
    return $self if none { $_ eq $column } $self->result_source->columns;

    # sanitize order direction
    $direction = 'ASC' if uc $direction ne 'DESC';

    return $self->search( undef, { order_by => { "-$direction" => "me.$column" } } );
}

Используется так:

    my $files = $schema->resultset('File')->search( { purge => 0 } )
      ->another_filter
      ->order_by( $sort_column, $sort_direction );


Подробная информация о списке рассылки Moscow-pm