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

dan*_*pla 3 c bash posix sh

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

我的第一个想法是让它像许多其他程序一样,用户可以在文本条目中键入命令,然后printf()命令中的特殊字符串(例如 in )被适当的数据替换(在我的例子中,可能是%t)。然后整个字符串被传递到execvp(). 例如,这是 qBittorrent 的屏幕截图: 在此输入图像描述

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

echo '%t' >> history.txt
Run Code Online (Sandbox Code Playgroud)

所以要执行的完整命令是:

echo ''&& rm -rf some_dir'' >> history.txt
Run Code Online (Sandbox Code Playgroud)

显然,这是一个坏主意。

第二选项只让用户选择一个可执行文件(带有文件选择对话框),因此我可以手动将 Tesseract 中的文本argv[1]放入execvp(). 这个想法是,可执行文件可以是一个脚本,用户可以在其中放置他们想要的任何内容并使用"$1". 这样,命令注入是不可能的(我认为)。以下是用户可以创建的示例脚本:

#!/bin/sh
echo "$1" >> history.txt
Run Code Online (Sandbox Code Playgroud)

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

Cha*_*ffy 5

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

不要这样做。请参阅下面的“带外”部分。

要使任意 C 字符串(不包含 NUL)在严格符合 POSIX 的 shell 中的不带引号的上下文中使用时计算其自身,可以使用以下步骤:

  • 前置 a ' (从所需的初始无引号上下文移动到单引号上下文)。
  • 将数据中的每个文字替换'为字符串'"'"'。这些字符的工作方式如下:
    1. '关闭初始单引号上下文。
    2. "输入双引号上下文。
    3. '在双引号上下文中,是字面意思。
    4. "关闭双引号上下文。
    5. '重新进入单引号上下文。
  • 追加 a ' (返回到所需的初始单引号上下文)。

这在符合 POSIX 标准的 shell 中可以正常工作,因为单引号上下文中唯一非文字的字符是'; 在该上下文中,甚至反斜杠也会被解析为文字。

然而,只有当 sigils 仅在未加引号的上下文中使用时(从而让用户有责任让事情正确)并且 shell 严格符合 POSIX 时,这才可以正常工作。此外,在最坏的情况下,此转换生成的字符串最多可以比原始字符串长 5 倍;因此,需要谨慎对待用于转换的内存的分配方式。

(有人可能会问为什么'"'"'建议而不是'\'';这是因为反斜杠改变了传统反引号命令替换语法中使用的含义,因此较长的形式更强大)。


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

数据只能在带外传递传递,这样它就根本不会通过解析器运行。调用 shell 时,有两种简单的方法可以执行此操作(除了使用文件之外):环境变量和命令行参数。

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

使用环境变量

排除错误处理(如果setenv()返回非零结果,则应将其视为错误,并且perror()或应使用类似的方法向用户报告),这将如下所示:

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);
Run Code Online (Sandbox Code Playgroud)

一些注意事项:

  • 虽然值可以是任意 C 字符串,但重要的是要限制变量名称 - 要么是如上所述的硬编码常量,要么是用常量(小写 7 位 ASCII)字符串作为前缀,并经过测试以仅包含允许的 shell 变量名称字符。(建议使用小写前缀,因为 POSIX 兼容的 shell 仅对修改其自身行为的变量使用全大写名称;请参阅有关环境变量的 POSIX 规范,特别是“包含小写字母的环境变量名称的名称空间”的注释为应用程序保留。应用程序可以使用此名称空间中的名称定义任何环境变量,而无需修改标准实用程序的行为”)。
  • 环境空间是有限的资源;在现代 Linux 上,环境变量和命令行参数的最大组合存储通常为 128kb;因此,设置较大的环境变量将导致execve()使用较大命令行的 -family 调用失败。验证长度是否在合理的特定领域限制内是明智的。

使用命令行参数:

此版本需要显式 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);
Run Code Online (Sandbox Code Playgroud)