Август 2005


Профессионалу разработчику


Том Кайт

Том Кайт: об интоксикации и сравнении
(On Injecting and Comparing, By Tom Kyte)

Источник: журнал Oracle Magazine, January/February 2005
(http://www.oracle.com/technology/oramag/oracle/05-jan/o15asktom.html).

Наш эксперт рассказывает о безопасной интоксикации, сравнении таблиц, материализованных представлениях и выборочном предоставлении системных привилегий.

Вопрос. В середине вашей книги Effective Oracle by Design вы немного рассказываете об интоксикации SQL. Это заинтересовало меня и заставило "полезть" в Веб.

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

Ответ. Это – важная тема, но, я думаю, не так уж много людей осведомлены о ней. Перед тем, как начать отвечать на вопросы, давайте определим термин интоксикация SQL. (Английские варианты названия термина: SQL injection (его использует Том), SQL poisoning, intoxication. Прим. пер.)

Интоксикация SQL происходит, когда приложение принимает из ненадежного источника произвольный SQL-код, вслепую добавляет его к SQL-коду приложения, а затем выполняет его. Это как если бы операционная система получала от пользователя несколько строк C-кода, компилировала его "на проходе" и немедленно выполняла в ядре операционной системы. Вы никогда бы не согласились с этим, поскольку это может разрушить безопасность системы (никто не знает, какой код поступает). Но до сих пор многие разработчики приложений делают это в своих приложениях каждый день. Получая от конечных пользователей произвольный код, они немедленно присоединяют его к своим SQL-операторам, выполняют синтаксический разбор (компиляцию) и инициируют его выполнение.

Чтобы прояснить это, я собираюсь использовать отрывки из своей книги Effective Oracle by Design ("Проектирование эффективных приложений Oracle"). Отрывки взяты из главы о переменных связывания (представьте себе, что я на самом деле написал о переменных связывания!) и их важности для обеспечения масштабируемости и производительности. Чтобы дать дополнительные аргументы разработчикам и администраторам базы данных, желающим использовать переменные связывания, я предоставил этот понятный пример интоксикации SQL, используемой для обхода аутентификационных проверок:

Без переменных связывания ваш код становится менее защищенным

Другое беспокойство, когда вы не используете переменные связывания, связано с безопасностью, а именно с риском интоксикации SQL. Чтобы понять, как это работает, предположим, что мы имеем приложение, спрашивающее у пользователя его имя (username) и пароль (password). Мы выполняем запрос:

select count(*)
  from user_table
 where username = ЭТОТ_ПОЛЬЗОВАТЕЛЬ
   and password = ЭТОТ_ПАРОЛЬ;

Это кажется невинным, не так ли? Хорошо, попробуем в утилите SQL*Plus проверить это предположение и посмотреть, что может случится, если используются переменные связывания и без них.

SQL> create table user_table
  2  ( username varchar2(30),
  3   password varchar2(30) );
Table created.

SQL> insert into user_table
  2  values ( 'tom',
  3  'совершенно_секретный_пароль' );
1 row created.

SQL> commit;
Commit complete.

SQL> accept Uname -
   > prompt "Введите имя пользователя:"
Введите имя пользователя: tom

SQL > accept Pword -
    > prompt "Введите пароль: "
Введите пароль: я_не_знаю' or
'x' = 'x

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

SQL> select count(*)
  2    from user_table
  3   where username = '&Uname'
  4     and password = '&Pword'
  5  /
old  3:  where username = '&Uname'
new  3:  where username = 'tom'
old  4:    and password = '&Pword'
new  4:    and password =
        'я_не_знаю' or 'x' = 'x'

   COUNT(*)
 ----------
          1

Смотрите на это. Очевидно, пароль 'я_не_знаю' or 'x' = 'x' – наш пароль. Но если вместо этого мы будем использовать переменные связывания, и брать точные данные, вводимые пользователем, мы увидим следующее:

SQL> variable pword varchar2(30);
SQL> exec :uname := 'tom';
SQL> exec :pword := -
   >  'я_не_знаю'' or ''x'' = ''x';

PL/SQL procedure successfully
completed.

SQL> select count(*)
  2    from user_table
  3   where username = :uname
  4     and password = :pword
  5  /
   COUNT(*)
 ----------
          0

Мы получаем правильный ответ.

Подумайте об этом в следующий раз, когда вы будете размещать приложение в Интернете. Сколько скрытых уязвимостей вы "запрятали" бы в нем, если бы при разработке приложения вы использовали не переменные связывания, а сцепление строк? Думайте о побочных эффектах в вашей системе, которыми кто-то может воспользоваться. Рассмотрим этот пароль:

SQL> accept Pword -
   > prompt "Введите пароль: "

Введите пароль: hr_pkg.уволить_сотрудника( 1234)

Ой, этот человек, возможно, только что выполнил хранимую функцию как пользователь, который соединен с сервером базы данных. Он не зарегистрирован в системе, тем не менее был подсоединен к ней и уволил кого-то. Действительно ли это невероятно? Абсолютно нет. Поищите в http://www.google.com/ "SQL injection", и вы увидите приблизительно 15 800 результатов (сейчас, в 2005 г., их уже примерно 784 000 – прим. пер.). Только подумайте о последствиях.

Если вы не верите в аргументы в пользу переменных связывания, относящиеся к производительности, то, возможно, этот последний довод будет достаточным, чтобы убедить вас. Связанные переменные повышают безопасность. Интересно отметить, когда я делал вышеупомянутую выписку в 2003 г., Google имел приблизительно 16 000 индексированных страниц. Сейчас, в 2004 г., их приблизительно 250 000! Интоксикация SQL – усиливающаяся проблема. Эта проблема существует не только исключительно в активных серверных страницах (ASP, Active Server Pages), написанных на языке Visual Basic, или в JSP-страницах (JavaServer Pages) – столь же легко попасть в эту западню и с модулем mod_plsql и PL/SQL-кодом или, в сущности, с любым языком. Подумайте о "невинной" хранимой функции, показанной на листинге 1.

ЛИСТИНГ 1: "невинная" хранимая функция.
SQL> create or replace
  2  procedure remove_emp( p_schema in varchar2,
  3                      p_ename in varchar2 )
  4  is
  5    l_str long;
  6  begin
  7    l_str := '
  8    begin
  9      delete from ' || p_schema ||
 10       '.emp where ename = ''' || p_ename || ''';
 11      delete from ' || p_schema ||
 12       '.bonus where ename = ''' || p_ename || ''';
 13    end;';
 14    execute immediate l_str;
 15  end;
 16  /

Procedure created.

SQL> create table t ( x int );

Table created.

SQL> desc t

 Name        Null?    Type
 ---------   -----    ------------------
 X                    NUMBER(38)

SQL> begin
  2   remove_emp
  3   ( 'scott',
  4     'KING''; execute immediate ''drop table t''; --' );
  5  end;
  6  /
begin
*
ERROR at line 1:
ORA-00942: table or view does not exist
ORA-06512: at line 4
ORA-06512: at "OPS$TKYTE.REMOVE_EMP", line 11
ORA-06512: at line 2


SQL> desc t
ERROR:
ORA-04043: object t does not exist

Обратите внимание, как даже при том, что в этом блоке обобщенного кода (написанным так, чтобы быть независимым от имен схем) выдается ошибка, он достигает цели. Выполняется первый оператор удаления строки – DELETE FROM SCOTT.EMP WHERE ENAME = 'KING, а затем также успешно выполняется оператор удаления таблицы – drop table T, выполнение которого имело не совсем хороший побочный эффект преждевременной фиксации вашего удаления строки. Следующий оператор DELETE выполняется успешно, но при выполнении второго оператора drop table выдается ошибка. Итак, здесь вы поставили под удар непротиворечивость выполнения транзакций (потому что конечный пользователь, предоставляющий вам имя служащего, перехватил контроль над транзакцией), вы также дали ему возможность выполнять любой SQL-код (или, в этом случае, PL/SQL-код), используя привилегии владельца процедуры (который, по-видимому имеет большие права доступа, потому что это – "обобщенная" процедура, способная удалить большое количество служащих).

Итак, как сделать следующее:

  1. Обнаружить людей, которые в прошлом занимались интоксикацией SQL?
  2. Обнаружить людей, все еще занимающихся ею?
  3. Обнаружить код или приложения, чувствительные к интоксикации SQL (и решить, как исправить их)?
  4. Обнаружить уязвимости сервера базы данных, которые позволят злонамеренному пользователю заниматься интоксикацией SQL?

Хорошо, вопрос №1 – твердый орешек; если только вы не имели включенного аудита, а также знаете образец для поиска и имеете конкретный случай, который вы пробуете объяснить, в противном случае это, вероятно, будет невозможно. Например, если таблица загадочно "пропала", и я имел включенный аудит (или, возможно, использовал бы утилиту анализа журнальных файлов logminer, если этот случай был достаточно недавним), то я узнал бы использованную учетную запись и смог бы приступить к поиску возможностей для интоксикации SQL.

Для ответа на вопрос №2 вы можете воспользоваться представлением V$SQL, чтобы найти случаи интоксикации SQL. То, что вы ищете, – любой набор запросов в представлении V$SQL, которые отличаются только литеральным SQL, содержащимся в них. Это – верный признак, что приложение только что получило откуда-то строки, которые оно сцепило с SQL-кодом. Это плохо по многим причинам, но это – первое место для обнаружения потенциальных атак "интоксикация SQL".

К сожалению, если вы будете использовать установки параметра инициализации CURSOR_SHARING = FORCE/SIMILAR, то этот подход не будет хорошо работать, поскольку литералы из всех запросов будут удаляться. Вы просто будете не в состоянии локализовать в разделяемом пуле дубликаты запросов; вы должны будете смотреть на каждый оператор и видеть то, что видите. Но предположим, вы используете установку параметра инициализации CURSOR_SHARING = EXACT, то вы сможете воспользоваться методикой, набросанной на сайте asktom.oracle.com/~tkyte/literals.html. Она первоначально была разработана для поиска "плохого SQL-кода", который не связывался должным образом, но именно это мы и должны найти, поэтому и здесь она будет работать хорошо. Эти несвязанные SQL-операторы как раз те, которые можно считать подозрительными: они плохи для производительности вашей системы, но они также могут владеть "ключами к королевству".

Что вы должны сделать теперь – узнать, откуда поступают литералы в эти SQL-операторы. Если вы обнаруживаете, что они прибывают из поля ввода текста на каком-то экране, вы определенно подвергаетесь опасности. В любое время приложение принимает SQL-код из ненадежного источника (который может быть почти всем, что находится за пределами самого приложения), выполнение такого SQL-кода сопряжено с опасностью. Особенно опасно выполнение SQL-кода из промежуточного звена (с правами суперпользователя) или при передаче его в процедуру, выполняемую с правами ее владельца (хранимые процедуры по умолчанию выполняются с привилегиями их владельцев).

Ответ на вопрос №3 может быть получен только благодаря стандартам кодирования, экспертной оценке кода и строгому соблюдению стандартов. Любой код, который принимает строки из ненадежного источника и выполняет его(не связывает значения строк), открыт для интоксикации SQL. Этим утверждением все сказано. Если у вас есть приложение промежуточного звена, которое использует общую учетную запись (с большим количеством привилегий), если у вас есть клиент-серверное приложение, которое использует общую учетную запись (с большим количеством привилегий) или если вы используете динамический SQL в процедурах PL/SQL, выполняемых с правами их владельцев, вы в большой степени открыты для этого дефекта – если только ваши разработчики не используют переменные связывания. Вы можете также поинтересоваться у ваших поставщиков об их приложениях. Если их приложение принимает от вас вводимые данные, непосредственно помещает их в SQL-код и выполняет его, вы должны полагаться на поставщиков приложений и предполагать, что все будет в порядке, потому что вы не имеете доступа к коду приложения.

Для поиска ответов на вопрос №4 я рекомендую довольно новую книгу, написанную Дэвидом Ноксом (David Knox) Effective Oracle Database 10g Security by Design (Oracle Press, 2004) – проектирование эффективной безопасности сервера базы данных Oracle Database 10g. В ней нет контрольного списка вопросов типа "запустите этот скрипт и он скажет, все ли у вас в порядке" (такие контрольные списки вопросов безопасности встречаются), а скорее поддерживается концепция: "вы должны проектировать безопасность в вашей системе с самого начала всего проекта". Это аналогично тому, как я поддерживаю концепцию: "вы должны проектировать производительность, масштабируемость и сопровождаемость вашей системы с самого начала всего проекта".

Обычно при подготовке этой колонки я в значительной степени пользуюсь материалами сайта Ask Tom (Спрашивайте Тома) и редактирую их. Однако, чтобы сделать предыдущий вопрос и ответ на него более удобочитаемыми, я фактически написал все это более или менее с нуля, используя материалы обсуждения на своем сайте. Я предлагаю вам прочитать оригинальное обсуждение в теме asktom.oracle.com/~tkyte/sqlinj.html для дополнительного подтверждения, что интоксикация SQL реальна, существует, и использование переменных связывания – решение, которое в значительной степени полностью решает эту проблему.

Сравнение содержимого двух таблиц

Вопрос. У меня есть две таблицы с именами A и B. Они имеют идентичные столбцы и одинаковое количество строк (select count(*) from A и from B). Однако содержимое одной из строк отличается, как это показано в следующем запросе:

SQL> select * from A where C1=1;
     C1              C2          C3
 ------    ------------    --------
      1    AAAAAAAAAAAA         100

SQL> select * from B where C1=1;

     C1              C2          C3
 ------    ------------    --------
      1    AAAAAAAAAAAB         100

Единственное отличие – последний символ в столбце C2: "A" – в таблице A и "B" – в таблице B. Я хочу написать SQL-код для сравнения или чтобы увидеть, что таблицы A и B синхронны относительно их содержимого, а не только числа строк, но я не знаю, как это сделать.

Ответ. Хорошо, мы разрешим эту проблему для столбцов C1, C2 и C3, а затем посмотрим, как обобщить это решение для любого числа столбцов. Первый и непосредственный ответ, к которому я пришел, был следующим:

 (select 'A', a.* from a
 MINUS
 select 'A', b.* from b)
UNION ALL
(select 'B', b.* from b
 MINUS
 select 'B', a.* from a)

То есть, только выполните операцию над множествами A minus B (которая дает нам все, что находится в таблице A, но не находится в таблице B), и добавьте к полученному результату (UNION ALL) результат операции B minus A. По существу это правильно, но имеет ряд недостатков:

  • для выполнения запроса требуется четырех полных просмотра таблиц;
  • если строка дублируется в таблице A, то операция MINUS будет молча "де-дублировать" ее (то же самое делается и с таблицей B).

Итак, это решение кажется медленным, а также скрывает от нас информацию. Есть лучшее решение, которое использует только два полных просмотра и операцию группирования GROUP BY. Рассмотрим следующие значения в таблицах A и B:

SQL> select * from a;

         C1   C2   C3
 ----------   --   --
          1    x    y
          2   xx    y
          3    x    y

SQL> select * from b;
         C1    C2    C3
 ----------    --    --
          1     x     y
          2     x     y
          3     x    yy

Первые строки совпадают, а вторые и третьи отличаются. Вот как мы можем найти их:

SQL> select c1, c2, c3,
  2         count(src1) CNT1,
  3          count(src2) CNT2
  4     from
  5   ( select a.*,
  6          1 src1,
  7          to_number(null) src2
  8       from a
  9      union all
  10    select b.*,
  11         to_number(null) src1,
  12         2 src2
  13      from b
  14   )
  15  group by c1,c2,c3
  16 having count(src1) <> count(src2)
  17 /

  C1   C2    C3   CNT1    CNT2
 ---   --    --   ----    ----
   2    x     y      0       1
   2   xx     y      1       0
   3    x     y      1       0
   3    x    yy      0       1

Теперь, поскольку операция COUNT(<выражение>) возвращает число значений <выражения>, не равных NULL, мы предполагаем, что после группировки по всем столбцам таблицы, мы имеем два равных количества (потому что операция COUNT(src1) подсчитывает число записей в таблице A, которые имеют значения, а операция COUNT(src2) делает то же самое для таблицы B). Столбцы CNT1 и CNT2 могут показать нам, что, например, таблица A имеет какую-то строку дважды, а таблица B – трижды (приведенные выше операции MINUS и UNION ALL не в состоянии сделать это).

Чтобы убедиться в этом, вы можете прочитать оригинал обсуждения на сайте Ask Tom, которое привело нас к этому ответу: asktom.oracle.com/~tkyte/compare.html. Я считаю в этой теме интересным путь рассуждений "вперед и обратно", который мы должны были пройти, чтобы получить окончательный запрос. В конечном счете, комбинация способа Марко Стефанетти (Marco Stefanetti) с незначительным моим дополнением привела к этому запросу, но там вы сможете увидеть весь процесс становления довольно хорошей идеи.

Недостающий столбец в материализованном представлении

Вопрос. Существует ли способ создания столбца материализованного представления, который еще не содержит данных, но имеет нужный тип данных? У меня есть столбец типа даты, отсутствующий в главной таблице. Если в материализованном представлении не создается такой столбец, то любые сравнения с этим полем будут приводить к ошибке. Я пытался вместо такого столбца использовать 'sysdate * NULL', но это не работает.

Ответ. Для этого довольно хорошо подходит функция CAST; она работает и в операторе CREATE TABLE AS SELECT, и в операторе CREATE MATERIALIZED VIEW:

SQL> create materialized view mv
  2  as
  3  select empno, ename,
  4     cast ( null as date ) dt
  5    from emp
  6  /
Materialized view created.

SQL> desc mv

 Name    Null?     Type
 -----   --------  ------------
 EMPNO   NOT NULL  NUMBER(4)
 ENAME             VARCHAR2(10)
 DT                DATE

Выборочные системные привилегии

Следующие шаги

СПРАШИВАЙТЕ Тома
Том Кайт – вице-президент группы Oracle Government, Education, and Healthcare – отвечает на наиболее трудные вопросы, связанные с технологией баз данных Oracle. Наиболее яркие материалы этого форума публикуются в данной колонке.

ЧИТАЙТЕ еще о
проектировании эффективных приложений Oracle
более подробно о сравнении таблиц

Вопрос. Я хочу разрешить пользователям выполнять оператор "alter system set user_dump_dest". Я хочу дать им возможность включать расширенную трассировку, но писать трассировочные файлы не в каталог по умолчанию, а в другой каталог, такой, как /tmp. Можно ли это сделать легко?

Ответ. Для этого очень подходят хранимые процедуры!

Хранимая процедура по умолчанию выполняется с базовыми привилегиями владельца процедуры. Как пользователь, могущий выполнять операторы ALTER SYSTEM, вы должны сделать только это:

create or replace procedure set_udump (p_udump in varchar2)
as
begin
 if ( p_udump NOT LIKE '%=%' )
 then
      execute immediate 'alter system set
      user_dump_dest = '''||p_udump||''' scope=memory';
 else
    raise_application_error(-20000,'Извините, но по соображениям безопасности
    в параметре этой процедуры не разрешается использовать "="');
 end if;
end;
/

Примечание: исправленное содержание этой процедуры – предотвращение интоксикации SQL – предоставлено Роем Джордженсеном (Roy Jorgensen).

Владелец процедуры должен иметь привилегию ALTER SYSTEM, предоставленную непосредственно, а не через роль. Более подробно об этом нюансе см. asktom.oracle.com/~tkyte/Misc/RolesAndProcedures.html. Таким образом, мы выполняем:

SQL> grant alter system to ops$tkyte;
Grant succeeded.

SQL> grant execute on set_udump to scott;
Grant succeeded.

Теперь, соединившись как SCOTT, выполняем:

SQL> exec set_udump( '/tmp' );
PL/SQL procedure successfully completed.

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


Ведущий данной колонки Том Кайт (Tom Kyte, thomas.kyte@oracle.com) работает в Oracle с 1993 года. Кайт – вице-президент группы Oracle Government, Education, and Healthcare, он автор книг "Effective Oracle by Design" (издательство Oracle Press, 2003) – "Проектирование эффективных приложений Oracle" и "Expert One on One: Oracle" (издательство Apress, 2003) ( Прим. пер. Имеется русский перевод: Oracle для профессионалов. Книга 1. Архитектура и основные особенности. Книга 2. Расширение возможностей и защита. – ДиаСофт, 2003 г.).

E-mail this page