В bash безобидная с виду конструкция [[ $var -eq 42 ]] умеет выполнять и произвольный код

Знали ли вы о том, что следующий bash-скрипт способен выполнять произвольный код, который предоставит ему пользователь в ответ на запрос скрипта о вводе данных?

#!/bin/bash
read -rp "Enter guess: " num
if [[ $num -eq 42 ]]
then
  echo "Correct"
else
  echo "Wrong"
fi

Вот пример:

$ ./myscript
Enter guess: 42
Correct

$ ./myscript
Enter guess: a[$(date >&2)]+42
Sun Feb  4 19:06:19 PST 2018
Correct

Это — не некая новая находка, и не уязвимость, которая появилась в bash совсем недавно. Но это — одна из тех проблем разработки bash-скриптов, о которой говорят очень и очень мало. Да и ksh ведёт себя так же, если принимать во внимание отсутствие read -p.

Оболочка выполняет вычисление выражений в арифметическом контексте в нескольких синтаксических конструкциях, в которых она ожидает увидеть целое число. Сюда входят $((here))((here))${var:here:here}${var[here]}, var[here]= ..; то же самое оболочка ожидает и в правой или левой части конструкции [[, используемой для сравнения чисел с применением операций -eq-gt-le и прочих подобных.

В этом контексте можно использовать или передавать в команду строковые литералы, вроде «1+1», которые будут обрабатываться как математические выражения. Например, команда var="1+1"; echo $((var)) приведёт к выводу числа 2.

Но, как было показано, это — не просто система вычисления арифметических выражений, работающая в стиле «калькулятора» bc. Всё это более тесно интегрировано с оболочкой: команда a="b"; b="c+1"; c=41; echo $((a)) выведет 42. Тут же можно вспомнить и операции с индексами массивов. Как целые числа интерпретируются и замены команд.

Обычно эту проблему считают малозначительной или попросту игнорируют. На неё не указывают при проведении код-ревью или при анализе кода неких примеров. На неё не обращают внимания даже те, кто хорошо о ней осведомлён. Было бы довольно-таки утомительно использовать для выполнения всех операций сравнения ${var//[!0-9-]/}, да и, в любом случае, часто это не имеет отношения к нашему вопросу

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

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

А теперь сравните это всё с отношением к eval.

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

Часто можно слышать, что «eval — это зло». Но почему-то никто никогда не говорит, что «-eq — это зло», или что «конструкция $((foo+1)) считается опасной».

Почему существуют подобные двойные стандарты?

Я взял бы на себя смелость утверждать, что команда eval считается «плохой» не (только) потому, что она небезопасна, но и потому, что её считают «примитивной» и «низкопробной». «Безопасность» — это всего лишь самая простая причина советовать использовать что-то такое, что лучше eval.

Команду eval называют «примитивной» из-за того, что её, в основном, используют новички, которые выполняют с её помощью сложные операции, не имея при этом достаточно глубокого понимания того, как работает командная оболочка. Если есть возможность в общих чертах понять то, какой именно код надо выполнить, и сгенерировать соответствующие команды, то не нужно знать о том, как, в разных ситуациях, правильно пользоваться кавычками, или о том, как происходит раскрытие выражений.

А между тем — арифметические контексты bash — это удобные и полезные инструменты, которыми часто пользуются опытные разработчики скриптов, сознательно делая выбор в их пользу.

Вот — пример скрипта. Если запустить его в виде ./myscript 1 — он выдаст Foo, а если в виде ./myscript 2 — он выдаст Bar. Вы, возможно, увидите тут шаблон именования переменных, характерный для новичков, когда после имени идёт цифра (а может, вы и сами когда-то создавали такие переменные):

#!/bin/bash
var1="Foo"
var2="Bar"
eval echo "\$var$1"

Если где-нибудь опубликовать этот пример — к нему появится куча комментариев, авторы которых будут говорить о том, что это — небезопасно, что ./myscript '1; rm -rf /' может привести к серьёзным проблемам… В итоге автора кода призовут «подумать о детях» и переписать код с использованием массивов:

#!/bin/bash
var[1]="Foo"
var[2]="Bar"
echo "${var[$1]}"

Как уже было сказано, такая конструкция не намного безопаснее eval: команда ./myscript '1+a[$(rm -rf /)]' приведёт к столь же серьёзным проблемам, что и ./myscript '1; rm -rf /'. Правда, из-за того, что тут правильно используются подходящие возможности оболочки, большинство опытных программистов сочтёт этот код вполне приемлемым.

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

Просто стоит помнить о том, что существуют двойные стандарты.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *