如何在算术表达式中安全地使用关联数组?

如何在算术表达式中安全地使用关联数组?

一些类似 Bourne 的 shell 支持关联数组:(ksh93自 1993 年起)、zsh(自 1998 年起)、bash(自 2009 年起),尽管这 3 者之间的行为存在一些差异。

常见用途是计算某些字符串的出现次数。

然而,我发现这样的事情:

排版 -A 计数
(( count[$var]++ ))

不要为某些价值观$var而工作,我听说这甚至构成了一种任意命令执行脆弱性如果 的内容$var处于或可能处于攻击者的控制之下。

这是为什么?有哪些有问题的价值观?我该如何解决这个问题?

答案1

问题是在 shell 算术表达式中,例如

  • 里面$((...))(POSIX),
  • ((...))(ksh/bash/zsh)
  • 数组索引
  • 一些 shell 内置函数的一些参数
  • 内部数值比较运算符的操作数[[...]]

单词扩展 ( ${param}, $((...)), $[...], $(...), `...`,${ ...; }执行第一的,然后将结果文本解释为算术表达式。

对于 的情况$((...)),这甚至是 POSIX 要求。

这使得类似的事情op=+; echo "$(( 1 $op 2 ))"能够工作,这也解释了为什么a=1+1; echo "$(($a * 2))"输出3而不是4,因为它是1+1 * 2被评估的表达式。

这也是部分为什么在算术表达式中使用未经清理的数据是一个安全漏洞一般来说。

很容易忽视的是,它也适用于诸如

(( assoc[$var]++ ))

以上,除了ksh93(编辑现在 bash 5.2+,见下文),$var首先扩展,然后解释结果。

这意味着如果$var包含@or *,则assoc[@]++orassoc[*]++表达式将被求值,并且@/*在那里具有特殊含义。如果$var是的话x] + 2 + assoc[y,那就变成了assoc[x] + 2 + assoc[y]

现在通常情况下,在 中$(( $var )),即使$var包含类似 的内容$(reboot),也不会发生第二轮扩展,reboot也不会运行。但正如已经看到的在 Shell 算术评估中使用未经净化的数据的安全影响word[...],如果它出现在内部以允许递归扩展,则存在例外。问题的根源是不幸的特征Korn shell 的其中 ifvar包含算术表达式,则在$((var))算术表达式 in中$var进行求值,甚至递归地(如 when var2='var3 + 1' var='var2 + 1'),这是 POSIX 允许但不要求的操作。

由于它扩展到数组成员,这意味着数组索引的内容最终会被递归计算。所以,如果$var$(reboot),那么(( assoc[$var]++ ))最终会调用reboot

ksh93似乎有一定程度的解决办法,但只有当$var不包含$它时。因此,虽然 ksh93 可以使用var=']', var='@', 或var='`reboot`',但不能使用$(reboot).

举个例子,如果我们reboot用无害的替换uname>&2

$ var='1$(uname>&2)' ksh -c 'typeset -A a; (( a[$var]++ )); typeset -p a'
Linux
typeset -A a=([1]=1)
$ var='1$(uname>&2)' bash -c 'typeset -A a; (( a[$var]++ )); typeset -p a'
Linux
Linux
declare -A a=([1]="1" )
$ var='1$(uname>&2)' zsh -c 'typeset -A a; (( a[$var]++ )); typeset -p a'
Linux
Linux
typeset -A a=( [1]=1 )

uname命令最终确实被运行了(在bash(<5.2) 和中运行了两次zsh,我想一次是为了获取当前值,第二次是为了执行分配)。

在 5.0 版本中,bash 添加了一个assoc_expand_once更改行为的选项:

$ var='1$(uname>&2)' bash -O assoc_expand_once -c 'typeset -A a; ((a[$var]++)); typeset -p a'
declare -A a=(["1\$(uname>&2)"]="1" )

现在可以了,但它没有解决@*或 的问题],因此它没有解决任意命令执行漏洞:

$ var='x]+b[1$(uname>&2)' bash -O assoc_expand_once -c 'typeset -A a; ((a[$var]++)); typeset -p a'
Linux
declare -A a

(这次,uname正在作为评估的一部分运行清楚的数组 ( b) 索引评估)。

有问题的字符列表随 shell 的不同而变化。$是所有三个的问题,\`[是和,,的]问题。和以及空值相同。另请注意,在某些语言环境中,某些字符的编码确实包含、或至少的编码,并且可能会导致问题。如何逃避这些必须在所有三个 shell 中以不同的方式完成。bashzsh"'bash@*\`[]

要解决这个问题,可以这样做:

assoc[$var]=$(( ${assoc[$var]} + 1 ))

反而。那是:

  1. 不执行对关联数组成员的赋值作为一部分的算术表达式,但仅执行裸关联数组成员赋值。换句话说,不要将=, ++, --, +=, /=... 算术运算符与关联数组成员作为目标一起使用。
  2. 在算术表达式中引用关联数组时,请勿使用assoc[$var], but ${assoc[$var]}(或$assoc[$var]in zsh),或者(${assoc[$var]})如果这意味着包含算术表达式而不仅仅是数字。

但是,一如既往价值该关联数组成员的 必须在您的控制之下,最好是一个普通数字,并且与任何其他参数扩展一样,最好在它们周围放置空格。例如,((1 - $var))优于((1-$var))后者,因为后者会导致负值出现问题(((1--1))在某些 shell 中导致语法错误,因为这是--应用于1.

另一个需要注意的是,当$var为空时, in(( 1 + var ))仍然var是算术表达式语法中的一个标记,以及相应的值 if 0。但在 中(( 1 + $var )),算术表达式变为 ,1 +这是一个语法错误((( $var + 1 ))不过没关系,因为它变为+ 1,调用一元+运算符)。

其他方法bash(5.1 或更早版本,并且当assoc_expand_once选项为不是启用)或zsh(但不ksh93]\字符仍然有问题),将延迟扩展直到上面提到的第二个递归解释。

  • (( assoc[\$var]++ ))
  • let 'assoc[$var]++'(确保使用单身的此处引用)
  • incr='assoc[$var]++'; (($incr))(甚至((incr))
  • ((' assoc[$var]++ '))(( assoc['$var']++ ))bash仅)。

这些具有保留算术评估产生的退出状态的优点(成功如果非零),因此可以执行以下操作:

if (( assoc[\$var]++ )); then
  printf '%s\n' "$var was already seen"
fi

现在,这留下了 shell 特有的一个问题bashbash关联数组不支持空键。虽然assoc[]=xbashand zsh(not ksh93) 中都失败了,但assoc[$var]when $varisempty 在zshor中起作用,ksh93但在 or 中不起作用bash。 Evenzsh现在assoc+=('' value)由 bash-5.1 支持,但在bash.

因此,如果专门使用bash并且空键是可能的值之一,则唯一的选择是添加固定的前缀/后缀。因此使用例如:

assoc[.$var]=$(( ${assoc[.$var]} + 1 ))

或者:

let 'assoc[.$var]++'
(( assoc[.\$var]++ ))
...

2023年编辑

上面的大多数解决方法不再适用于 bash-5.2。

在 bash-5.2.21 中,(( assoc[$var]++ ))(对于非空$var) 和似乎是安全的,(( assoc[.$var]++ ))即使$var包含]\或。*@

它们仍然是旧版本的 ACE 漏洞。

唯一适用于 5.2 和 5.2 之前版本的方法似乎是:

其中只有第二个适用于 ksh93、zsh 和 bash。

特别是,(( assoc[\$var] ))在 zsh 中也适用的方法在 bash-5.2 中不再适用。

相关内容