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

How to safely pass an arbitrary text as parameter to a program in a shell script?

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

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

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

echo '%t' >> history.txt

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

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

显然,这是个坏主意。

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

#!/bin/sh
echo "" >> history.txt

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

任何时候你运行命令,甚至可能有用户输入进入它们,你必须为shell上下文转义。

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

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

不要这样做。请参阅下面的 "Out-Of-Band" 部分。

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

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

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

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

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


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

数据只能从代码带外传递,这样它就永远不会运行通过解析器。调用 shell 时,有两种直接的方法(除了使用文件):环境变量和命令行参数。

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

使用环境变量

不包括错误处理(如果setenv() returns 非零结果,这应该被视为错误,并且应该使用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);

一些注意事项:

  • 虽然值可以是任意的 C 字符串,但重要的是要限制变量名称——如上硬编码常量,或以常量(小写 7 位 ASCII)字符串为前缀并经过测试仅包含以下字符:允许的 shell 变量名。 (建议使用小写前缀,因为符合 POSIX 的 shells 仅对修改其自身行为的变量使用全大写名称;参见 the POSIX spec on environment variables,特别是 "The name space of environment variable names containing lowercase letters is reserved for applications. Applications can define any environment variables with names from this name space without modifying the behavior of the standard utilities").
  • 环境space是一种有限的资源;在现代 Linux 上,环境变量和命令行参数的最大组合存储通常为 128kb;因此,设置大的环境变量将导致 execve()-family 调用大命令行失败。验证长度是否在合理的特定领域限制内是明智的。

使用命令行参数:

这个版本需要一个明确的 API,这样配置触发命令的用户就知道哪个值将在 </code> 中传递,哪个值将在 <code> 中传递,等等。

/* 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 [=11=] in the script */
  torrent_name_str,     /* this is  in the script */
  torrent_category_str, /* this is  in the script */
  path_str,             /* this is  in the script */
  NUL);