在没有 eval 的情况下从 bash 中的“key1='val1' key2='val2'”字符串中解析变量

Posted

技术标签:

【中文标题】在没有 eval 的情况下从 bash 中的“key1=\'val1\' key2=\'val2\'”字符串中解析变量【英文标题】:Parse variables out of a "key1='val1' key2='val2'" string in bash without eval在没有 eval 的情况下从 bash 中的“key1='val1' key2='val2'”字符串中解析变量 【发布时间】:2021-06-14 12:26:42 【问题描述】:

我有一个特定于项目的命令,它产生以下形式的输出:

Parameter1='value1' Parameter2='Value2' ... #单引号值 变量。

但我想显式分配值并需要打印必须显示相应值的参数。

这里 xtc_cmd get 是项目特定的 cmd

root@renway:~# FOO=`xtc_cmd get lan_ifname lan_ipaddr lan_netmask`
root@renway:~#
root@renway:~# echo $FOO
SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1' SYSCFG_lan_netmask='255.255.255.0'
root@renway:~#
root@renway:~# echo $SYSCFG_lan_ifname

root@renway:~# echo $SYSCFG_lan_ipaddr

root@renway:~# echo $SYSCFG_lan_netmask

但是,在变量打印它们的值之后,我尝试了“eval $FOO”。 出于安全原因,我想跳过“评估”。

分享脚本执行的输出:

root@renway:~# /tmp/test.sh
++ xtc_cmd get lan_ifname lan_ipaddr lan_netmask
+ FOO='SYSCFG_lan_ifname='\''br1'\''
SYSCFG_lan_ipaddr='\''10.0.0.1'\''
SYSCFG_lan_netmask='\''255.255.255.0'\'''
+ echo 'SYSCFG_lan_ifname='\''br1'\''' 'SYSCFG_lan_ipaddr='\''10.0.0.1'\''' 'SYSCFG_lan_netmask='\''255.255.255.0'\'''
SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1' SYSCFG_lan_netmask='255.255.255.0'

如何实际分配值并打印这些变量。

输入感兴趣的字符串

FOO='SYSCFG_lan_ipaddr='\''10.0.0.1'\'' SYSCFG_sysdate='\'''\''$(date>> /tmp/date.txt)0'\'''\'' SYSCFG_lan_pd_interfaces='\''brlan0 brlan19 brlan20'\'''

预期输出:

foo_SYSCFG_lan_ipaddr=10.0.0.1
foo_SYSCFG_sysdate='$(date>> /tmp/date.txt)0' #single quoted value
foo_SYSCFG_lan_pd_interfaces=brlan0 brlan19 brlan20 #whitespace separated string

这里的挑战是 SYSCFG_sysdate 单独持有单引号值 '$(date>> /tmp/date.txt)0' 与其他参数相比。

抱歉,我错过了最早强调或提及这个参数。 这是为了测试恶意命令注入攻击。所以这里的期望是要按原样存储但没有命令执行的值。使用 'eval' 内置,日期命令正在执行,这不是预期的。

运行 Zilog80 的 POSIX V1 脚本后得到的所需输出 使用 'set' 内置。

但 POSIX V2 脚本只有在没有 SYSCFG_sysdate 参数的情况下才能正常运行。

特别感谢 @Charles Duffy 和 @Zilog80 为此提供的大量宝贵意见和指导问题。

【问题讨论】:

顺便说一句——养成使用echo "$FOO"而不是echo $FOO的习惯;否则,可能会遇到I just assigned a variable, but echo $variable shows something else! 中描述的问题 请确保您避免使用eval,因为您不相信 xtc_cmd 会正确输出 shell 转义词,而不是因为“eval is evil”的货物崇拜。它们很少而且相差甚远,但在某些情况下,eval 是安全可靠的选择。 @thatotherguy, ...这不是完全不合理的建议,但如果要访问eval shell-escaped 数据,那么值得审计转义的效果如何。我个人发现了两个具有可利用错误的 shell 转义 Java 实现——其中一个是 PLXUTILS-161 / MSHARED-297,另一个是 github.com/rundeck/rundeck/issues/298。我可以看到有人说“哦,文件说应该逃脱,它必须是安全的”;如果不是有人(在那些情况下恰好是我!)检查角落案例,没人会知道。 (...或者,如果运气不好,只有坏人会知道;出售漏洞的市场只会随着时间的推移而增长,所以这肯定是一回事)。 【参考方案1】:

借用一个密切相关问题的答案 (Reading quoted/escaped arguments correctly from a string):

#!/usr/bin/env bash
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1' SYSCFG_lan_netmask='255.255.255.0'"
 
case $BASH_VERSION in
  ''|[1-3].*) echo "ERROR: Bash 4.0 required; this is $BASH_VERSION:-not bash" >&2; exit 1;;
esac
 
declare -A kwargs=( )
while IFS= read -r -d ''; do
  [[ $REPLY = *=* ]] || 
    printf 'ERROR: Item %q is not in assignment form\n' "$REPLY" >&2
    continue
  
  kwargs[$REPLY%%=*]=$REPLY#*=
done < <(xargs printf '%s\0' <<<"$FOO")
 
# show what we parsed for demonstration purposes
declare -p kwargs >&2

您可以在https://ideone.com/KniaC4; 的在线沙盒中看到它运行,它的输出是以下形式的关联数组:

declare -A kwargs=([SYSCFG_lan_ifname]="br1" [SYSCFG_lan_netmask]="255.255.255.0" [SYSCFG_lan_ipaddr]="10.0.0.1" )

...所以你可以参考"$kwargs[SYSCFG_lan_ifname]",或"$kwargs[SYSCFG_lan_ipaddr]"

这比分配给常规 bash 变量更安全,因为它不允许攻击者修改 PATH、LD_PRELOAD 或其他修改 shell、链接器、加载器、标准 C 库等行为的环境变量。(请注意,即使您没有明确export 此代码创建的分配,分配给已导出的变量也会自动导出新值;因此,仅适用于环境变量而非常规 shell 变量的安全问题仍然存在在这里)。


警告:xargs 解析字符串的方式与 POSIX sh 标准不兼容相当 -- 请参阅link given above 了解详细信息和其他选项(Python 有一个完全兼容的解析器,f/e ,并且链接的答案描述了如何从 bash 中使用它)。


或者,使用较旧的 Bash 版本

当关联数组不可用时,可以为常规变量添加前缀:

#!/usr/bin/env bash
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1' SYSCFG_lan_netmask='255.255.255.0'"
 
while IFS= read -r -d ''; do
  [[ $REPLY = *=* ]] || 
    printf 'ERROR: Item %q is not in assignment form\n' "$REPLY" >&2
    continue
  
  printf -v "foo_$REPLY%%=*" '%s' "$REPLY#*="
done < <(xargs printf '%s\0' <<<"$FOO")
 
# show what we parsed for demonstration purposes

for var in $!foo_*; do
  echo "$var has value: $!var"
done

在https://ideone.com/7UZJkT 看到这个运行,输出:

foo_SYSCFG_lan_ifname has value: br1
foo_SYSCFG_lan_ipaddr has value: 10.0.0.1
foo_SYSCFG_lan_netmask has value: 255.255.255.0

【讨论】:

谢谢,您的输入在使用常规 bash 变量和 PATH、LD_PRELOAD 环境变量时的安全漏洞方面更有价值。不幸的是,我拥有的 bash 版本是 3.2.57(1)-release,因此无法使用您建议的关联数组。 @renga_in_stack,见附录显示不需要关联数组的版本。 感谢提供的解决方案对我有用。请确认是否可以忽略以下输入字符串FOO='SYSCFG_lan_ipaddr='\''10.0.0.1'\'' SYSCFG_sysdate='\'''\''$(date&gt;&gt; /tmp/date.txt)0'\'''\'''的错误 我了解 read 中的分隔符 -d ' ' 正在对参数进行标记。如果我的参数值为SYSCFG_lan_pd_interfaces='\''brlan0 brlan1 brlan2'\'',那么在这种情况下它会失败。任何有助于使这种情况也起作用的帮助。 @renga_in_stack,你认为什么是“失败”?内引号是字面的;将它们视为数据是正确的。如果这不在这里,则 xargs 在解析引号的方式上不太符合 POSIX 的错,这就是为什么链接的问题有一个基于 Python 的字符串拆分例程 is 完全符合 POSIX .我已经在答案中谈到了这一点,甚至在第一次写的时候也是如此。【参考方案2】:

要添加到@CharlesDuffy 的答案,对于那些仍然坚持使用旧的“不可升级”硬件/虚拟机的人,这里有一种 POSIX / 旧的 bash 方法可以安全地实现这一目标。使用 dash、ksh93、bash 2.05b、3 和 4 进行测试。无法检索我的旧 Bourne shell 92。

编辑:感谢有用的@CharlesDuffy cmets:

    更新以处理 'value' 部分中的空白/空格/换行符/wathever。以一种基本的方式(多个空格减少到一个空格,新行被吞下)。正在努力寻找更好的方法来处理这个问题。

    生成的变量名称现在以_ 为前缀,以防止任何 尝试覆盖PATHLD_PRELOAD等。

EDIT2: 添加了处理值中的制表符/空格/换行符的 Bash 2/3/4 和 ksh 版本。见下文。

EDIT3:添加一个符合 POSIX 标准的 rev 2,可以处理 TABNEWLINE 和多个 SPACE

POSIX 兼容 V1 :

这个不能很好地处理变量值部分的换行符和制表符。它不会崩溃,但相关变量将被“压缩”在一行中,使用空格而不是换行符,并且所有制表符/多个空格都减少为一个空格。

#!/bin/sh
# If you only have a bash 4.x, you can test with compat 3.1 bash
# shopt -s compat31
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1' 
SYSCFG_lan_netmask='255.255.255.0' SYSCFG_space='my   space' SYSCFG_newline='I have 
many multi
lines input"
# An "env variable" definer that use the read command 
# to parse and define the env  variable
define() 
  IFS=\= read -r key value <<EOF
$1
EOF

  # Unquotting the value, adapt as it fit your needs
  value="$value#\'"
  value="$value%\'"
  read -r "_$key" << EOF
$value
EOF


unset _SYSCFG_lan_ifname
unset _SYSCFG_lan_ipaddr
unset _SYSCFG_lan_netmask
unset _SYSCFG_space
unset _SYSCFG_newline
# Using the set command to "parse" the variables string
set $FOO
while [ "$1" ] ; do
  key_value="$1"
  while [ "$1" ] && [ "$key_value%\'" = "$key_value" ]  ; do
    shift
    key_value="$key_value $1"
  done
  define "$key_value"
  [ "$1" ] && shift
done
echo "$_SYSCFG_lan_ifname"
echo "$_SYSCFG_lan_ipaddr"
echo "$_SYSCFG_lan_netmask"
echo "$_SYSCFG_space"
echo "$_SYSCFG_newline"

输出与ksh93、bash 2..4、破折号相同:

br1 10.0.0.1 255.255.255.0 我的空间 我有很多多行输入

POSIX 兼容 V2 :

这个版本可以处理特殊字符和,部分,换行符。它不使用set 命令来解析字符串,避免了任何潜在的全局效应。我们依赖于基本的 shell tr​​imer #%。这个还可以处理字符串中的不同引用和转义引号/双引号。 define 函数通过 here-doc 中的 \n 处理多行,因此它们的翻译将留给脚本用户。

#!/bin/sh
# If you only have a bash 4.x, you can test with compat 3.1 bash
# shopt -s compat31
# Test string. There is a TAB between "input" and "and".
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1 *' 
SYSCFG_lan_netmask=\"255.255.255.0\" SYSCFG_space='mypath\\ so\'urce\\my   space' 
SYSCFG_newline='I have 
many  multi 
 lines input    and /path/to/thi ngs'"
#
# Define here the prefix you want for the variables. A prefix is required
# to avoid LD_PRELOAD, PATH, etc. override from a variable.
_prefix="_"
# 
# The POSIX way for a new line constant.
_NL="
"
# An "env variable" definer that use the read command to parse and define 
define() 
  _key="$1"
  _value="$2"
  _quote="$3"
  _tmp=""
  # The POSIX read command can only read one line at a time.
  # For multiline, we loop to rebuild the full value.
  while read -r _line ; do 
    [ "$_tmp" ] && _tmp="$_tmp\n$_line" || _tmp="$_line";
  done  <<EOF
$_value
EOF

  read -r "$_prefix$_key" << EOF
$_tmp
EOF


unset _SYSCFG_lan_ifname
unset _SYSCFG_lan_ipaddr
unset _SYSCFG_lan_netmask
unset _SYSCFG_space
unset _SYSCFG_newline
# First, we trim blanks
_FOO="$FOO# * "
_FOO="$_FOO% * "
# We use shell basic trimer to "parse" the string
while [ "$_FOO" ] ; do
  # Get the first assignation from the beginning
  _FOO_next="$_FOO#*="
  if [ "$_FOO_next" != "$_FOO" ] ; then
    # If there is backslash in the string we need to double escape them for 
    # using it as a pattern. We do that in a safe manner regarding FOO content.
    _FOO_next_pattern="$( sed 's/\\/\\\\/g' <<EOF
$_FOO_next
EOF
    )"
    # We have an assignation to parse
    _key="$_FOO%=$_FOO_next_pattern"
    # We must have a key, assignation without key part are ignored.
    # If need, you can output error message in the else branch.
    if [ "$_key" ] ; then
      # Triming space and newlines
      _key="$_key## "
      _key="$_key##$_NL"
      _key="$_key## "
      _quote="\'"
      # Test if it  is quote, if not quote then try double quote
      [ "$_FOO_next" = "$_FOO_next#$_quote" ] && _quote="\""  
      # If not double quote, consider unquoted...
      [ "$_FOO_next" = "$_FOO_next#$_quote" ] && _quote=""  
      # Extracting value part and trim quotes if any
      if [ "$_quote" ] ; then 
        _FOO_next="$_FOO_next#$_quote"
        _FOO_next_pattern="$_FOO_next_pattern#$_quote"
      fi
      _value="$_FOO_next"
      if [ "$_quote" ] ; then 
        _FOO_next="$_FOO_next#*[^\\]$_quote"
        _FOO_next_pattern="$_FOO_next_pattern#*[^\\]$_quote"
      else
        # If the value part is not quoted, we look for the next unescaped space
        # as the delimiter for the next key/value pair.
        _FOO_next="$_FOO_next#*[^\\] "
        _FOO_next_pattern="$_FOO_next_pattern#*[^\\] "
      fi
      _value="$_value%$_quote$_FOO_next_pattern"
      # We have parse everything need to set the variable
      define "$_key" "$_value" "$_quote"
      _FOO="$_FOO_next"
    else
      _FOO="$_FOO_next#*[^\\] "
    fi
  else
    # Nothing more to parse
    _FOO=""
  fi
done
printf "%s\n" "$_SYSCFG_lan_ifname"
printf "%s\n" "$_SYSCFG_lan_ipaddr"
printf "%s\n" "$_SYSCFG_lan_netmask"
printf "%s\n" "$_SYSCFG_space"
printf "%s\n" "$_SYSCFG_newline"

输出与ksh93、bash 2..4、破折号相同:

br1 10.0.0.1 * 255.255.255.0 我的路径\所以\'urce\我的空间 我有许多多行输入和 /path/to/thi ngs

BASH V2+ 和 KSH93 兼容:

它不符合 POSIX,因为按模式 (/) 的变量替换不是 POSIX。文字 ASCII 推断 $'\x&lt;hex ASCII code&gt;' 确实不是 POSIX,以下脚本只能与基于 ASCII 的 UNIX shell 一起使用(忘记 EBCDIC...)。无论如何,这个可以处理变量值部分中的换行符/制表符/多个空格。

#!/bin/sh
# If you only have a bash 4.x, you can test with compat 3.1 bash
# shopt -s compat31
# Test string. There is a TAB between "input" and "and".
FOO="SYSCFG_lan_ifname='br1' SYSCFG_lan_ipaddr='10.0.0.1 *' 
SYSCFG_lan_netmask='255.255.255.0' SYSCFG_space='mypath\\ source\\my   space' 
SYSCFG_newline='I have 
many  multi 
 lines input    and /path/to/thi ngs"
# 
# For bash 2.0, we can't make inline subsitution of ESC nor NL nor TAB  because
# of the following bug :
#  FOO="`echo -e \"multi\nline\"`";echo "$FOO//$'\x0a'/$'\x1b'" ==> multi'line
# Bash 2.0 wrongly include one quote to the output in this case.
# To avoid that, we store ESC and NL in local variable, and it is better 
# for readability.
_ESC=$'\x1b'
_NL=$'\x0a'
_TAB=$'\x09'
# Same kind of trouble with the backslash in bash 2.0, the substiution need 
# 'double' escape for them in bash 2.0, so we store BKS, test it and double it 
# if required.
# However, if used as a variable in pattern or subsitution part, we have then to
# deal with two forms of escaped bakcslash since shells don't "dedouble"/escape
# them  for the substitute value, only for the pattern.
_BKS_PATTERN="\\\\"
_BKS="\\"
if [ "$_BKS_PATTERN//\\/X" != "XX" ] ; then
  # Hello bash 2.0
  _BKS_PATTERN="\\\\\\\\"
  _BKS="\\\\"
fi
# An "env variable" definer that use the read command to parse and define 
define() 
  IFS=\= read -r _key _value <<EOF
$1
EOF

  # Unquotting the _value, adapt as it fit your needs
  _value="$_value#\'"
  _value="$_value%\'"
  _value="$_value%\'$_BKS_PATTERN"
  # Unescape the _key string to trim escaped nl
  _key="$_key#$_ESC"
  _key="$_key%$_ESC"
  # Unescape the _value string
  _value="$_value//$_BKS_PATTERN / "
  _value="$_value//$_ESC$_ESC/$_TAB"
  _value="$_value//$_ESC/$_NL"
  read -d\' -r "_$_key" <<EOF
$_value'
EOF


unset _SYSCFG_lan_ifname
unset _SYSCFG_lan_ipaddr
unset _SYSCFG_lan_netmask
unset _SYSCFG_space
unset _SYSCFG_newline
# First, we escape the new line with 0x1B
_FOO="$FOO//$_NL/$_ESC"
# Second, escape each tab with double ESC. All tabs.
_FOO="$_FOO//$_TAB/$_ESC$_ESC"
# Third, escape each space. All space.
_FOO="$_FOO// /$_BKS "
# Using the set command to "parse" the variables string
set $_FOO
while [ "$1" ] ; do
  _key_value="$1"
  while [ "$1" ] && [ "$_key_value%\'$_BKS_PATTERN" = "$_key_value" ] ; do
    shift
    _key_value="$_key_value $1"
  done
  define "$_key_value"
  [ "$1" ] && shift
done
printf "%s\n" "$_SYSCFG_lan_ifname"
printf "%s\n" "$_SYSCFG_lan_ipaddr"
printf "%s\n" "$_SYSCFG_lan_netmask"
printf "%s\n" "$_SYSCFG_space"
printf "%s\n" "$_SYSCFG_newline"

输出与 ksh93、bash 2 和 + 相同:

(请注意,我们使用printf 来呈现“输入”和“和”之间的TAb 字符。)

br1 10.0.0.1 * 255.255.255.0 我的路径\源\我的空间 我有 许多多 行输入和 /path/to/things

【讨论】:

顺便说一句,获得相同安全性的一种方法是,将所有内容放在带有 POSIX-y 答案的 kwargs 关联数组中,即为变量添加前缀——比如说,@987654337 @ 以foo_ 为所有内容添加前缀,因此不能覆盖PATHLD_PRELOAD 之类的变量。 ...顺便说一句,我不太确定 set 是否按照您的意愿行事——它不会按原样尊重引号。让我在沙箱中构建一个示例,以稍微修改数据进行演示... 请参阅ideone.com/5DWuwS 以了解上面承诺的示例如何使用var_with_two_words='foo bar' 失败 @CharlesDuffy 不错,我专注于问题字符串而没有考虑空格/空格/换行符。基本上,read 不应该与变量名或值中的$ 和` ` 一起使用(预期为 varname,但不适用于值)。为 var 名称添加前缀,防止 LD_PRELOAD ^^ 我会等你完成对令人惊讶的值的稳健处理,然后再给 +1,但这一切看起来很有希望。顺便说一句,考虑在您的测试集中包含'value with * a glob'(并确保在非空目录中执行代码时* 不会被文件名列表替换)。【参考方案3】:

如何实际分配值并打印这些变量。

编写解析器并解析输入并分配变量。读取一行,在第一个=处拆分行,左边部分是变量,右边部分是变量值,删除'引号或以命令输出的任何样式解析引号样式并将结果分配给提取的变量名从线。沿着伪代码

while <split line on first =>; do
    vareiable_value=$(parse second part after =)
    print -v "$variable_name" %s "$variable_value"
done <<<"$FOO"

【讨论】:

需要注意的是,虽然这不使用eval,但它仍然会产生安全影响;只有有限访问权限的攻击者(例如写入/tmp 的能力)可以设置PATH=/tmp/evil:/bin:/usr/bin:/usr/local/bin,从而导致以这些权限运行的可执行文件运行他们选择的可执行文件。不是唯一构成风险的——请参阅LD_PRELOAD 等。【参考方案4】:

假设您已经知道变量的名称(或有办法找到它们),并且您不必担心安全问题(请参阅 Charles Duffy 的回答),您可以source from a string:

$ FOO='SYSCFG_lan_ifname='\''br1'\'' SYSCFG_lan_ipaddr='\''10.0.0.1'\'' SYSCFG_lan_netmask='\''255.255.255.0'\'''

$ set | grep SYSCFG
FOO='SYSCFG_lan_ifname='\''br1'\'' SYSCFG_lan_ipaddr='\''10.0.0.1'\'' SYSCFG_lan_netmask='\''255.255.255.0'\'''

$ . <(echo "$FOO")

$ set | grep SYSCFG
FOO='SYSCFG_lan_ifname='\''br1'\'' SYSCFG_lan_ipaddr='\''10.0.0.1'\'' SYSCFG_lan_netmask='\''255.255.255.0'\'''
SYSCFG_lan_ifname=br1
SYSCFG_lan_ipaddr=10.0.0.1
SYSCFG_lan_netmask=255.255.255.0

【讨论】:

如果我有如下FOO='SYSCFG_lan_ifname='\''br1'\'' SYSCFG_lan_ipaddr='\''10.0.0.1'\'' SYSCFG_lan_netmask='\''255.255.255.0'\'' SYSCFG_sysdate='\'''\''$(date&gt;&gt; /tmp/date.txt)0'\'''\''' 的 FOO,源将使命令可执行。我主要关心的是命令不应该被执行。

以上是关于在没有 eval 的情况下从 bash 中的“key1='val1' key2='val2'”字符串中解析变量的主要内容,如果未能解决你的问题,请参考以下文章

在没有打印预览或打开报表的情况下从 Access 中的表单打印报表?

在没有表单的情况下从 Django 中的 jQuery 检索 POST 数据

如何在没有主键的情况下从 mySQL 中的两个表中获取不常见的记录

如何在没有 Web 源模块的情况下从 Oracle APEX 中的 API 获取数据

如何在没有授权令牌的情况下从 python 脚本查询 google-bigquery 中的私有表?

如何在没有任何循环php的情况下从数组中随机获取项目