如何安全地将任意文本作为参数传递给Shell脚本中的程序?

时间:2018-11-18 19:58:54

标签: c bash sh posix

我正在编写一个使用Tesseract的用于字符识别的GUI应用程序。我想允许用户指定文本准备好后将使用/bin/sh -c执行的自定义Shell命令。 问题在于,可识别的文本实际上可以包含任何内容,例如&& rm -rf some_dir

我的第一想法是使它像其他许多程序一样 用户可以在文本输入中键入命令,然后将命令中的特殊字符串(如printf())替换为适当的数据(在我的情况下,可能是%t)。然后,整个字符串将传递到execvp()。例如,这是qBittorrent的屏幕截图: enter image description here

问题在于,即使我在替换%t之前正确地转义了文本,也没有什么能阻止用户在说明符周围添加额外的引号:

echo '%t' >> history.txt

因此要执行的完整命令是:

echo ''&& rm -rf some_dir'' >> history.txt

显然,这是个坏主意。

第二选项仅允许用户选择一个可执行文件(带有文件选择对话框),因此我可以手动将Tesseract中的文本作为{{1}的argv[1] }。这个想法是可执行文件可以是一个脚本,用户可以在其中放置所需的任何内容,并使用execvp()访问文本。这样,命令注入是不可能的(我认为)。这是用户可以创建的示例脚本:

"$1"

这种方法有什么陷阱吗?也许有更好的方法可以安全地将任意文本作为参数传递给Shell脚本中的程序?

2 个答案:

答案 0 :(得分:4)

带内:在无引号的上下文中转义任意数据

请勿执行此操作。请参阅下面的“带外”部分。

要在严格符合POSIX的外壳中的未加引号的上下文中使用时,使任意C字符串(不包含NUL)对其自身求值,可以使用以下步骤:

  • 添加一个' (从所需的初始未引用上下文移至单引号上下文)。
  • 用字符串'替换数据中的每个文字'"'"'。这些字符的工作方式如下:
    1. '关闭初始的单引号上下文。
    2. "输入一个双引号上下文。
    3. 在双引号中,
    4. '是文字。
    5. "关闭双引号上下文。
    6. '重新输入单引号上下文。
  • 附加' (返回所需的初始单引号上下文)。

这在POSIX兼容的外壳程序中可以正常工作,因为在单引号上下文中唯一不是文字的字符是';在这种情况下,甚至将反斜杠也解析为文字。

但是,这仅在仅在无引号的上下文中使用sigils时才有效(因此,请用户承担责任以使事情正确),并且外壳严格符合POSIX。同样,在最坏的情况下,您可以使此转换生成的字符串的长度比原始字符串长5倍;因此,需要注意用于转换的内存的分配方式。

(可能会问为什么建议使用'"'"'而不是'\'';这是因为反斜杠会更改其在旧版反引号命令替换语法中使用的含义,因此较长的形式会更健壮)。


带外:环境变量或命令行参数

数据仅应从代码中进行带外传输,以使它永远不会通过解析器运行。调用Shell时,有两种简单的方法(使用文件除外):环境变量和命令行参数。

在以下两种机制中,仅需信任user_provided_shell_script(尽管这还要求不要引入新的或附加的漏洞;调用eval或任何与之等效的道德使所有信任无效)保证,但这是用户的问题,而不是您的问题。

使用环境变量

不包括错误处理(如果setenv()返回非零结果,则应将其视为错误,并且应使用perror()或类似内容向用户报告),如下所示:< / p>

setenv("torrent_name", torrent_name_str, 1);
setenv("torrent_category", torrent_category_str, 1);
setenv("save_path", path_str, 1);

# shell script should use "$torrent_name", etc
system(user_provided_shell_script);

一些注意事项:

  • 虽然值可以是任意的C字符串,但限制变量名称很重要-可以是上面的硬编码常量,也可以是常量字符串(小写的7位ASCII)前缀,并经过测试只能包含允许的外壳字符变量名。 (建议使用小写字母前缀,因为兼容POSIX的外壳程序仅使用全大写字母的名称来修改其自身行为的变量;请参见the POSIX spec on environment variables,尤其是注意“环境变量名称的名称空间包含小写字母留给应用程序使用。应用程序可以使用此名称空间中的名称定义任何环境变量,而无需修改标准实用程序的行为”。
  • 环境空间是有限的资源;在现代Linux上,环境变量和命令行参数的最大组合存储量通常为128kb;因此,设置较大的环境变量将导致具有较大命令行的execve()系列调用失败。验证长度是否在特定于域的合理范围内是明智的。

使用命令行参数:

此版本需要一个显式的API,以便配置触发命令的用户知道哪个值将在$1中传递,哪个值将在$2中传递,等等。

/* You'll need to do the usual fork() before this, and the usual waitpid() after
 * if you want to let it complete before proceeding.
 * Lots of Q&A entries on the site already showing the context.
 */
execl("/bin/sh", "-c", user_provided_shell_script,
  "sh",                 /* this is $0 in the script */
  torrent_name_str,     /* this is $1 in the script */
  torrent_category_str, /* this is $2 in the script */
  path_str,             /* this is $3 in the script */
  NUL);

答案 1 :(得分:0)

每次运行命令时,甚至可能会有用户输入进入命令中,都必须转义以获取shell上下文。

C中没有内置函数可以执行此操作,因此您可以自己操作,但是基本思想是将用户参数呈现为正确转义的字符串或某种执行函数的单独参数(例如{ {3}}。