如何安全地使用 gawk 的 -i 选项或 @include 指令?

如何安全地使用 gawk 的 -i 选项或 @include 指令?

使用gawk -i inplace some-awk-code some-file(或@include "inplace"awk脚本中)就地编辑文件(或任何其他扩展名)是一个安全漏洞

为什么?

我该如何解决这个问题?

答案1

awk在指定要运行的代码方面,GNU对标准有一些扩展。

在 standard 中awk,您只能将代码作为一个或多个传递,-f filepath其中filepath被视为从中读取代码的文件路径 或作为第一个非选项参数(如awk -- 'literal code here'),gawk 还有更多选项:

  • -e 'literal code'(或--source 'literal code') 与 中一样sed,您可以将代码拆分为多个参数,并且可以将-f filepath这些参数散布其中。
  • -E filepath(或--exec filepath),与-f除了只能有一个之外相同,并且其后的任何内容不考虑选项或变量分配,仅考虑文件路径(或-标准输入)。
  • --file filepath: 的别名-f
  • -i filepath(或):与行为--include filepath相似但有一些变化-f如手册中所述

现在的问题是gawk文件路径上述所有内容并不总是被视为文件路径:

  1. 如果文件路径不存在,gawk将尝试打开添加了扩展名的同一文件.awk。这意味着它最终可能会解释您不希望的代码,但这在实践中不太可能成为问题,因为您希望它运行的文件并不存在才能发生这种情况。它不会用--traditionalor来做到这一点-W traditional,但是你不能用它来使用大多数 gawk 的扩展。
  2. 如果文件路径不包含/字符(并且不是-),则 awk 程序会在环境变量中查找,$AWKPATH方式与 shell 或execvp()在 中查找无斜杠命令类似,$PATH其中包括 with--posix和 with --traditional,对于所有-f// -i(-E以及不带或带.awk如上所述添加的扩展名的情况)。

第二点是这里问题的核心。

您可以通过以下方式找到默认的 AWKPATH:

$ (unset -v AWKPATH && gawk 'BEGIN{print ENVIRON["AWKPATH"]}')
.:/usr/share/awk

(即使在ment中没有这样的变量ENVIRON!)

它以当前工作目录 开头.,后面跟着一个系统位置,其中包含一些随 .NET 一起提供的扩展awk或其他第三方模块gawk。在这个系统上:

$ ls /usr/share/awk
断言.awk getlong.awk intdiv0.awk ord.awk rewind.awk
bits2str.awk getopt.awk isnumeric.awk passwd.awk round.awk
悬崖_rand.awk gettime.awk join.awk processarray.awk shellquote.awk
ctime.awk group.awk libintl.awk fastsort.awk strtonum.awk
dpkg-awk.awk have_mpfr.awk noassign.awk 可读.awk walkarray.awk
ftrans.awk      就地.awk    ns_passwd.awk 读文件.awk 零文件.awk

这意味着对于-f/ -E,如果您希望file加载当前工作目录中的 ,则需要gawk -f ./file,而不是如果当前工作目录中没有 ,gawk -f file则可以从其他地方加载 a file(或) 。就像您需要在 shell 中运行当前工作目录一样(除了出于安全原因通常不包含,并且它将尝试加载,如上所示)。file.awkfile./cmdcmd$PATH.gawkfile.awk

这也适用于-i,除了通常-i用来包括来自库的 gawk 扩展在这种情况下你希望在应该找到它们的目录中查找它们,并且您想要.awk添加扩展(因为那些库扩展通常有这样的扩展)。

在 中gawk -i inplace 'some code' some-file,您确实想要gawk找到/usr/share/awk/inplace.awk(或inplace.awk系统上安装的任何位置),但这里的问题是默认的 AWKPATH开始.,因此将首先在和gawk中查找它。./inplace./inplace.awk

如果您在/tmp任何可写或已被其他人可写或通常不可信的目录中运行该文件,您最终可能会运行恶意代码。

例如,运行:

echo 'BEGIN{system("reboot")}' > /tmp/inplace

您会发现任何awk -i inplace在当前工作目录下执行的脚本都会/tmp重新启动系统!

要解决这个问题:

  • inplace使用awk -i /usr/share/awk/inplace.awk而不是硬编码扩展的路径awk -i inplace,尽管您可能需要使路径适应每个系统或 gawk 部署。

  • 或从中删除.所有相对路径组件$AWKPATH

    export AWKPATH="$(LC_ALL=C gawk 'BEGIN {
      n = split(ENVIRON["AWKPATH"], dirs, ":")
      for (i = 1; i <= n; i++)
        if (substr(dirs[i], 1, 1) == "/") {
          newawkpath = newawkpath sep dirs[i]
          sep = ":"
        }
      if (newawkpath == "") newawkpath = "/dev/null"
      print newawkpath}')"
    

    请记住,您将需要使用gawk -f ./fileawk -E ./file加载当前工作目录中的文件(即使没有$AWKPATH如上所示的更改,您也可能已经这样做了)。另请注意,4.1.2 之前的 gawk 版本在查看$AWKPATH.

    该方法不能在#! /usr/bin/gawk -E使用@include尽管的脚本中使用,因为在启动$AWKPATH时必须已经在环境中。gawk因此,如果您有一个gawk使用的脚本,@include "some-extension"您需要告诉您的用户更改其$AWKPATH扩展程序的路径或按照上面的方式对扩展程序的路径进行硬编码。

  • 或者使用几十年来perl一直可以-i进行就地编辑的选项,并且可以做任何awk可以做的事情,并且以更明智的语法²和更少的可移植性问题来做更多的事情。但不要忘记--in perl -i -ne 'perl code' -- *.txt,否则您也会引入代码注入漏洞(或使用./*.txt; 请参阅运行 perl -ne '...' * 的安全隐患)!


¹ 除非当文件路径-在这种情况下,大多数实现awk将其解释为从标准输入读取代码。

²perl-M选项,可以看作是gawks的等价物,使用不包含也不包含任何其他相对路径的默认搜索路径(请参阅)在或中-i查找模块M$PERL5LIB$PERLLIB(unset -v PERL5LIB PERLLIB && perl -le 'print for @INC'.

答案2

首先,感谢@StephaneChazelas,因为他说了我多年来在我写的每个论坛上一直说的话:放开sed -iawk -i inplace

除了你已经说过的内容(这对我来说是新的,这比我想象的更糟糕):

  1. “-到位”?并不真地!

    sed -i两者awk -i inplace都假装“就地”编辑,但事实并非如此。事实上,他们创建一个(隐藏的)临时文件作为输出,并最终移动它,覆盖原始文件。基本上与使用 POSIX 确认变体所做的事情相同,但自动如此。这听起来是个好主意,但从“就地”来看,我希望保留索引节点号以及所有权和文件模式。不是这种情况!事实上,在满足正确的先决条件的情况下,所有三个属性都会发生更改(即,允许用户写入文件,但具有与文件不同的主要组,具有粘滞位的目录,...)。

    现在,不要误会我的意思:发生这种情况没有问题,如果我的进程写入临时文件然后自行复制,也会以同样的方式发生。但在这种情况下我会意识到这一点并确保文件模式等在更改后得到纠正。因为这假装有效到位用户很可能没有意识到这种影响。

  2. 不存在的临时文件

    下一个问题是:如果修改文件并在此过程中创建临时文件,我将采取预防措施:必须有足够的空间来保存临时文件,之后我将确保删除临时文件等。因为我不这样做不知道临时文件去了哪里(手册页中没有任何关于它的信息,据称一切都发生“就地”)我无法控制它,如果系统在脚本中崩溃(这些事情发生)我有不知道我什至留下了一些文物来占用磁盘空间。

答案3

此外,gawk 有一个 AWKLIBPATH 变量,如果在环境中找不到该变量,则有一个默认值。该变量控制在哪里@load "library"查找库文件: 加载共享库

默认值似乎不使用.目录(对于我安装的版本),但我认为这可能会改变。

答案4

就目前而言@include,有我的cppawk。它是 awk 的一个小型 shell 脚本包装器,可让您使用 C 预处理器及其#include宏和所有内容。

#include不查找当前目录。它做得更好:当标头名称用双引号引起来时,它会在与#include找到指令的文件相同的目录中查找它。这使得cppawk用多个文件创建一个程序变得很容易:主文件可以使用#include "..."指令中的相对路径轻松找到其他文件。

cppawk有一些自己的库头,但没有一个提供就地文件编辑的解决方案。该实用程序将使解决方案的重用变得容易。

这是一个低质量的原型:

$ cat file.bak
alpha
bravo
charlie
$ cp file.bak file
$ cppawk '
#include "inplace.h"
{ out(NR, $0) }
' file
$ cat file
1 alpha
2 bravo
3 charlie

内容inplace.h

BEGIN {
  __inplace_tmpfile = "xyz.tmp"
  __inplace_origfile = ARGV[1]
}

END {
  close(__inplace_tmpfile)
  system("mv " __inplace_tmpfile " " __inplace_origfile)
}

#define out(...) print __VA_ARGS__ >  __inplace_tmpfile

这里我们至少需要以下内容:更好的方法来获取临时文件,并对内容进行 shell 转义,ARGV[1]以便我们可以安全地将其插入mv命令中。

out我们可以在没有重定向的地方有一个默认实现。然后我们养成使用它而不是print在程序中的习惯,cppawk这样当inplace.h包含它时,代码就不必修改。

我们可以在不进行预处理的情况下实现其中一些目标,因为-f可以用于包含脚本材料。inplace.h我们准备一个包含以下内容的文件,而不是标头inplace.awk

BEGIN {
  inplace = "xyz.tmp"
  __inplace_origfile = ARGV[1]
}

END {
  close(inplace)
  system("mv " inplace " " __inplace_origfile)
}

我们已经对保存临时文件的变量名称进行了去匿名化处理;它现在是界面的一部分。

不幸的是,为了能够将-f包含内容与命令行内脚本材料混合在一起,我们需要 GNU 特定的-e选项:

$ mv file.bak file
$ awk -f inplace.awk -e '{ print NR, $0 > inplace }' file
$ cat file
1 alpha
2 bravo
3 charlie

还有一个如何引用的问题inplace.awk。我们把它放在哪里以及如何找到它?#include没有这个问题。如果我们将其与代码一起发送,它会发现它就在自己旁边。如果我们将它作为库头放入,cppawk它将是<inplace.h>;再次,没问题。我们还可以选择使用cppawk --prepro-only捕获整个“翻译单元”,然后可以在不需要预处理器的情况下运行它cpp

相关内容