你如何使用纯的unset shell内置命令?你能编写抵御篡改的shell脚本吗?

7
我想使用 unset,而不是一个 shell 函数。如果我能做到这一点,就可以通过运行以下命令来确保 command 是纯的。
#!/bin/sh
{ \unset -f unalias command [; \unalias unset command [ } 2>/dev/null;
# make zsh find *builtins* with `command` too:
[ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on

如果我使用Debian Almquist shell(dash),我认为可以相信\unset是纯净的。至少我无法在dash中定义名为unset的shell函数。而在bashzsh中,我可以定义unset() { echo fake unset; },此后我无法取消该函数:\unset -f unset输出"fake unset"。
关于这一点,在脚本中,可以通过export -f <function name>导出函数,以便在由脚本调用的脚本中使用。然而,在dash脚本中不起作用。我想知道,如果我使用dash,是否要担心命令被定义为脚本文件外的shell函数?其他符合POSIX标准的shell又如何?

1
作为部分答案,POSIXLY_CORRECT= 确保在 Bash 中调用 unset 时不使用 shell 函数。对于任何 POSIX 特殊内置实用程序也适用。但是这在 Zsh shell 中不起作用。 - jarno
1
实际上,对于Bash来说,如果脚本是以'sh'的形式调用的话,就不需要使用POSIXLY_CORRECT=。例如,如果有一个哈希标记#!/bin/sh,并且通过其名称运行脚本。请参见从bash手册页面链接的文档 - jarno
3个回答

16
注意:以下内容适用于所有主要的 POSIX 兼容 shell,除非另有说明:bashdashkshzsh。(dash,即 Debian Almquist Shell,在基于 Debian 的 Linux 发行版(如 Ubuntu)上是默认的 shell(sh))。
  • unset having its original meaning - 使用其-f选项可以取消定义shell 函数的内置命令 - 是确保任何其他shell关键字、命令或内置命令具有其原始含义的关键

    • 从未修改的unset开始,您可以确保未修改的shopt和/或command,并且它们可以一起用于绕过或取消定义可能遮蔽shell关键字、内置命令和外部实用程序的任何别名或shell函数。
    • 作为取消定义函数的替代方法,可以使用command绕过它们,包括那些可能已经通过环境在代码外部定义的函数;只有bash支持导出函数,这仅是这些机制中的一个;不同的shell有不同的机制,并且可能支持多个-请参见下面。
  • 只有dashkshbash在POSIX兼容模式下时才能保证unset没有被重新定义:

    • dashksh是安全的,因为它们不允许定义名为unset函数,正如您发现的那样,任何别名形式都可以通过调用\unset来绕过。

    • bash在POSIX兼容模式下时允许您定义名为unset的函数,但在调用unset忽略它,并始终执行内置命令,正如您后来发现的那样。

      • 鉴于POSIX兼容模式限制了Bash的功能集并修改了其行为,通常不希望在其中运行Bash代码。在本文底部是您建议的解决方法的实现,它在临时激活POSIX兼容模式以确保未定义任何unset函数
  • 遗憾的是,据我所知,在zsh中 - 以及在bash的默认模式中 - 没有办法保证unset本身没有被重新定义,可能有其他类似POSIX的shell表现相似。

    • 将其称为\unset(引用名称的任何部分)将绕过别名重新定义,但不会绕过函数重新定义 - 要撤消这种情况,您需要原始的unset本身:陷入僵局。
  • 因此,在没有对执行环境的控制的情况下,您无法编写完全免受篡改的shell脚本除非您知道您的代码将由dashkshbash(采用解决方法)执行

    • 附加信息:

      • 根据 POSIX,引用命令名的任何部分(例如\unset)将绕过该名称的任何alias形式或keyword form(POSIX和zsh术语中的reserved word),但不会绕过shell函数。

      • 根据 POSIX,unalias -a会取消定义所有alias。没有等价的、符合POSIX标准的命令可以取消定义所有函数。

        • 注意:旧版本的zsh不支持-a;然而,至少从v5.0.8开始,它们支持这个选项。
      • 内置命令command可用于绕过bashdashksh中的关键字、alias和函数,换句话说:command只执行内置和外部工具。相比之下,默认情况下zsh也绕过内置工具;为了使zsh也执行内置工具,使用options[POSIX_BUILTINS]=on

      • 以下代码可以在所有shell中仅执行名为<name>的外部工具:
        "$(command which <name>)" ...
        请注意,虽然which不是POSIX实用程序,在现代类Unix平台上广泛可用。

      • 命令形式的优先级:

        • bashzsh:alias > shell关键字 > shell函数 > 内置 > 外部工具
        • kshdash:shell关键字 > alias > shell函数 > 内置 >外部工具
        • 即:在bashzsh中,alias可以覆盖shell关键字,而在kshdash中则不能。
      • bashkshzsh(但不包括dash)都支持非标准函数签名:function <name> { ...,作为符合POSIX标准的<name>() { ...形式的替代方案。

        • function语法是:
          • 确保在定义函数之前不对<name>进行别名扩展。
          • 能够选择一个也是shell关键字的<name>;注意,这样的函数只能以引号形式调用;例如:\while
          • (在ksh的情况下,使用function语法还意味着typeset语句创建本地变量。)
        • dashkshbash在POSIX模式下还防止为特殊内置命令(例如unsetbreaksetshift)命名函数;POSIX定义的特殊内置命令列表可以在here找到;dashksh

          针对 bash 的解决方法:确保 unset 具有其原始含义:

          如果您知道 bash 将执行您的脚本,则此解决方法是安全的,但不幸的是无法保证。

          此外,由于它修改了 shell 环境(删除别名和函数),因此不适用于旨在被引用的脚本。

          如上所述,通常不希望在 Bash 的 POSIX 兼容模式下运行代码,但您可以临时激活它以确保 unset 不会被函数遮蔽:

          #!/bin/bash
          
          # *Temporarily* force Bash into POSIX compatibility mode, where `unset` cannot 
          # be shadowed, which allows us to undefine any `unset` *function* as well
          # as other functions that may shadow crucial commands.
          # Note: Fortunately, POSIXLY_CORRECT= works even without `export`, because
          #       use of `export` is not safe at this point.
          #       By contrast, a simple assignment cannot be tampered with.
          POSIXLY_CORRECT=
          
          # If defined, unset unset() and other functions that may shadow crucial commands.
          # Note the \ prefix to ensure that aliases are bypassed.
          \unset -f unset unalias read declare
          
          # Remove all aliases.
          # (Note that while alias expansion is off by default in scripts, it may
          #  have been turned on explicitly in a tampered-with environment.)
          \unalias -a  # Note: After this, \ to bypass aliases is no longer needed.
          
          # Now it is safe to turn POSIX mode back off, so as to reenable all Bash
          # features.
          unset POSIXLY_CORRECT
          
          # Now UNDEFINE ALL REMAINING FUNCTIONS:
          # Note that we do this AFTER switching back from POSIX mode, because
          # Bash in its default mode allows defining functions with nonstandard names
          # such as `[` or `z?`, and such functions can also only be *unset* while
          # in default mode.
          # Also note that we needn't worry about keywords `while`, `do` and `done`
          # being shadowed by functions, because the only way to invoke such functions
          # (which you can only define with the nonstandard `function` keyword) would
          # be with `\` (e.g., `\while`).
          while read _ _ n; do unset -f "$n"; done < <(declare -F)
          
          # IN THE REST OF THE SCRIPT:
          #  - It is now safe to call *builtins* as-is.
          #  - *External utilities* should be invoked:
          #      - by full path, if feasible
          #      - and/or, in the case of *standard utilities*, with
          #        command -p, which uses a minimal $PATH definition that only
          #        comprises the locations of standard utilities.
          #      - alternatively, as @jarno suggests, you can redefine your $PATH
          #        to contain standard locations only, after which you can invoke
          #        standard utilities by name only, as usual:
          #          PATH=$(command -p getconf PATH)
          
          # Example command:
          # Verify that `unset` now refers to the *builtin*:
          type unset
          

          测试命令:

          假设上面的代码保存在当前目录下的文件script中。

          以下命令模拟了一个被篡改的环境,其中unset被别名和函数所遮蔽,并且文件script,导致它看到该函数,并在交互式源时扩展别名:

          $ (unset() { echo hi; }; alias unset='echo here'; . ./script)
          unset is a shell builtin
          

          type unset 输出 unset 是一个shell内置命令 证明函数和别名都已被禁用,遮盖了内置命令 unset


Zsh文档指出unalias有选项-a。该文档适用于Zsh版本5.2。根据我的经验,在Zsh 5.0.2(Ubuntu 14.04存储库中可用)中没有unalias选项,而在Zsh 5.1.1(Ubuntu 15.10存储库中可用)中有unalias选项。 - jarno
谢谢,我已经更新了答案;-a在至少 v5.0.8 中可用——如果你知道确切的引入版本,请告诉我。 - mklement0
我想知道除了Bash外,其他shell是否可以从父环境继承导出的shell函数? - jarno
这是关于 这个问题 的主题。 - jarno
@jarno:我已经大幅更新了答案:虽然 exporting 函数仅适用于 Bash,但还有其他机制,无论是在 Bash 还是其他 shell 中,你都需要关注。然而,最好的方法不是试图预测各种机制,而是始终控制你要调用的内容:删除所有别名,只调用你定义的函数,对于其他所有内容使用 command - mklement0
显示剩余7条评论

1
有趣的是,你已经说出了内置名称 -- command
$ var="FOO"
$ unset() { echo nope; }
$ echo "${var}"
FOO
$ unset var
nope
$ echo "${var}"
FOO
$ command unset var
$ echo "${var}"
<nothing!>

这并不适用于你处于一个敌对环境,有人创建了一个command() { :; }函数的情况。但是如果你处于一个敌对环境,你已经失败了 ;)。
当涉及到将函数导出到环境中时,那是一个特定于Bash的扩展,你不应该真正依赖它。POSIX shell(如dash)从设计上就不支持它。

1
在我确认command未被重新定义之前,我不想使用它,就像我在问题中尝试的那样。 - jarno
4
如果你担心command被覆盖,那么你已经基本上失败了。每个Shell内置命令都可以被覆盖。 - Mike Frysinger
1
command 的手册页面在“EXAMPLES”部分提供了一种确保 command 不是 shell 函数或别名的方法,以便启动“安全 shell 脚本”。链接为:http://www.unix.com/man-page/posix/1p/command/。但如果 unset 是用户函数,则该方法可能会失败。 - jarno

1

我知道可以做什么...

#!/bin/bash --posix
# 如果例如 BASH_FUNC_unset() 环境变量被设置,脚本执行将无法进行到这一步(前提是它是按原样运行的,而不是作为 `bash script ...` 运行) unset -f builtin command declare ...
saved_IFS=$IFS; readonly saved_IFS # 删除所有函数(在子shell中执行的 shell 内置命令 declare) IFS=$'\n'; for f in `declare -Fx`; do unset -f ${f##* }; done; IFS=$saved_IFS

1
我明白了。unset 内置命令在 posix 模式下使用。你确定别名不需要考虑吗?一个问题是除了 unset,环境中可能已经导出 [ 作为函数,但是在 posix 模式下无法取消它。因此,您必须切换回 bash 模式才能取消它。即使脚本被称为 bash script ...,您也可以通过 POSIXLY_CORRECT= 强制启用 posix 模式。 - jarno
补充@jarno的评论:如果您无法控制调用,则别名是一个问题,但是您可以通过调用以\为前缀的命令来绕过它们。 同样,当涉及到通用取消定义函数时,_所有_函数都是问题,而不仅仅是导出的函数(-x)。 如所述,您必须退出POSIX模式,才能取消定义具有非标准名称的函数,例如[a?(错别字)。 名称可能为a?这样的名称意味着for f in \declare -Fx`不是枚举函数名称的稳健方法,除非您还使用(并恢复)set -f`。 - mklement0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接