bash - 您如何使用内置的纯未设置外壳程序?您可以编写不受篡改的shell脚本吗?

标签 bash security sh zsh dash-shell

我的意思是我想使用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(破折号),我想我可以相信\unset是纯文本。至少我不能在unset中定义名为dash的shell函数。在bashzsh中,我可以定义unset() { echo fake unset; },此后我无法取消设置该功能:\unset -f unset输出“ fake unset”。

与此相关,在bash脚本中,可以通过export -f <function name>导出函数,以便可以在该脚本调用的bash脚本中使用它。但是,这在dash脚本中不起作用。我想知道,如果我使用的是dash,是否不必担心将命令定义为正在编写的脚本文件之外的shell函数?其他POSIX兼容的外壳怎么样?

最佳答案

注意:以下内容适用于所有主要的POSIX兼容外壳程序,除非另有说明:bashdashkshzsh。 (dash,Debian Almquist Shell,是基于Debian的Linux发行版(如Ubuntu)上的默认Shell(sh))。


unset具有其原始含义-a builtin that can undefine shell functions with its -f option-是确保任何其他shell关键字,命令或内建函数具有其原始含义的关键。


从未修改的unset开始,可以确保未修改的shopt和/或command,并且它们可以一起用于绕过或取消定义可能遮盖shell关键字,内置函数和外部实用程序的任何别名或shell函数。
作为未定义函数的替代方法,可以使用command绕过它们,包括通过环境绕过它们,包括那些可能在代码外部定义的函数。
bash支持的导出功能只是这些机制之一;不同的外壳有不同的外壳,可能支持多个外壳-参见下文。

在POSIX兼容模式下,只有dashkshbash保证未重新定义unset


dashksh是安全的,因为正如您所发现的,它们不允许定义名为unset的函数,并且可以通过调用\unset来绕过任何别名形式。
在POSIX兼容模式下,bash允许您定义一个名为unset的函数,但是在调用unset时将其忽略,并始终执行该内置函数,这将在以后发现。


鉴于POSIX兼容模式限制了Bash的功能集并修改了其行为,通常不希望在其中运行Bash代码。这篇文章的底部是您建议的解决方法的实现,该方法临时激活POSIX兼容模式,以确保未定义任何unset函数。


不幸的是,据我所知,在zsh以及bash的默认模式下,无法保证unset本身没有被重新定义,并且可能还有其他类似POSIX的外壳表现类似。


\unset调用它(引用名称的任何部分)将绕过别名重定义,而不是函数重定义-并撤消您将需要原始的unset本身:捕获22。

因此,在无法控制执行环境的情况下,您无法编写完全不受篡改的shell脚本,除非您知道您的代码将由dashkshbash执行(有适当的解决方法) )。


如果您愿意假定unset未被篡改,则最可靠的方法是:


使用\unset -f确保unaliascommand未被修改(没有被shell函数遮盖:\unset -f unalias command


与别名不同,函数必须按名称明确地未定义,但是并非所有的shell都提供一种枚举所有已定义函数的机制(不幸的是,typeset -fbashkshzsh中起作用,但是dash appears to have no mechanism at all ),因此不一定总是无法定义所有功能。

使用\unalias -a删除所有别名。
然后用command [-p]调用除定义的功能以外的所有内容。调用外部实用程序时,请尽可能使用显式路径,和/或在使用标准实用程序的情况下,请使用command -p,它使用仅限于标准位置的最小$PATH定义(运行command -p getconf PATH可以看到该定义)。






附加信息:


对于每个POSIX,用命令名引用命令名称的任何部分(例如\unset)都会绕过该名称的任何别名形式或关键字形式(在POSIX和zsh措辞中保留的单词)-但不提供Shell函数。
对于POSIX,unalias -a取消定义所有别名。没有等效的,符合POSIX的命令来取消定义所有功能。


注意:较旧的zsh版本不支持-a;至少从v5.0.8开始,但是确实如此。

内置command可用于绕过关键字,别名,bashdashksh中的函数-换句话说:command仅执行内置函数和外部实用程序。相比之下,默认情况下zsh也绕过内置函数。要使zsh也执行内置函数,请使用options[POSIX_BUILTINS]=on
以下内容可用于在所有shell上仅执行名为<name>的外部实用程序:
"$(command which <name>)" ...
请注意,尽管which不是POSIX实用程序,但在类似Unix的现代平台上可以广泛使用。
命令格式的优先顺序:


bashzsh:别名> shell关键字> shell函数>内置>外部实用程序
kshdash:shell关键字>别名> shell函数>内置>外部实用程序
即:在bashzsh中,别名可以覆盖shell关键字,而在kshdash中,别名不能。

bashkshzsh-但不允许dash-都允许使用非标准功能签名function <name> { ...,以替代符合POSIX的<name>() { ...格式。


function语法是以下条件的先决条件:


在定义函数之前,请确保<name>本身不受别名扩展的影响。
能够选择也是外壳关键字的<name>
注意,这样的函数只能以引号形式调用;例如\while
(对于ksh,使用function语法还意味着typeset语句创建局部变量。)

在POSIX模式下,dashkshbash还会阻止特殊内置函数的命名功能(例如unsetbreaksetshift);可以在here中找到POSIX定义的特殊内置函数的列表; dashksh都添加了一些无法重新定义的内容(例如,local中的dashtypeset中的unaliasksh),但是两个shell都具有其他非特殊的内置函数可以重新定义(例如,type)。
请注意,在ksh的情况下,无论是否使用function语法,都适用上述规则。

在您的代码范围内,环境外壳函数的潜在来源:


注意:防止这些情况的最简单方法是,每当您要调用内置函数或外部实用程序时,都使用(未修改的)command内置函数(在zshoptions[POSIX_BUILTINS]=on中,也可以防止绕过内置函数)。
POSIX要求将其在环境变量ENV中的绝对路径指定的脚本用于交互式外壳程序(有一些限制-请参见the spec); kshdash总是很荣幸,而bash仅在作为sh或在v4.2 +中与--posix一起调用时才这样做。相比之下,zsh从不接受此变量。


注意:您的代码作为脚本运行通常会在非交互式shell中运行,但这不能保证;例如,您的代码可能来自交互式脚本,或者有人可以使用sh -i调用您的脚本来强制执行交互式实例。

bash具有2种机制:


export -fdeclare -fx导出单个函数(其他shell仅支持导出变量)
在可选的环境变量BASH_ENV中启动非交互式shell时,指定脚本源的完整路径。

ksh支持通过可选的FPATH环境变量自动加载函数:隐式且自动加载包含位于FPATH指定的任何目录中的函数定义的文件。


zsh也支持FPATH,但是自动加载功能需要明确的autoload <name>语句,因此,除非您明确要求使用给定名称的功能来自动加载,否则不会将任何功能添加到您的shell中。)

zsh支持通过其zsh/etc/zshenv初始化文件为任何~/.zhsenv实例(无论是否交互)采购脚本。
dash似乎不支持通过环境定义功能的任何机制。)





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

仅当您知道bash将执行您的脚本时,此解决方法才是安全的,但不幸的是,该脚本本身无法得到保证。

另外,由于它修改了外壳环境(去除了别名和函数),因此它不适用于旨在作为源代码的脚本。

如前所述,通常不希望在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 is a shell builtin可以证明该功能和遮盖内置unset的别名均已停用。

关于bash - 您如何使用内置的纯未设置外壳程序?您可以编写不受篡改的shell脚本吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35916983/

相关文章:

bash - 找到: paths must precede expression

javascript - 功能策略 : Skipping unsupported feature name “picture-in-picture” / “autoplay” / “encrypted-media”

java - 如何保护基于客户端-服务器的 Java 桌面应用程序的数据库凭据

bash - Linux bash 解析 URL

docker - 如何在Docker容器中启用yum repo

PHP 在运行后台进程的 bash 脚本上调用 shell_exec 超时

bash - unix bash 命令

php - 如何从密码中获取盐并用它来验证用户?

linux - 如何在删除前检查文件是否存在?

regex - sed 替换文件组字符串