Is there a way to prevent injection attacks when building a command-line from untrusted input in bash?

时间:2019-04-08 13:59:29

标签: bash

I have a situation where a Bash script runs and parses a user-supplied JSON file using jq. Since it's supplied by the user, it's possible for them to include values in the JSON to perform an injection attack.

I'd like to know if there's a way to overcome this. Please note, the setup of: 'my script parsing a user-supplied JSON file' cannot be changed, as it's out of my control. Only thing I can control is the Bash script.

I've tried using jq with and without the -r flag, but in each case, I was successfully able to inject.

Here's what the Bash script looks like at the moment:

#!/bin/bash

set -e

eval "INCLUDES=($(cat user-supplied.json | jq '.Include[]'))"
CMD="echo Includes are: "
for e in "${INCLUDES[@]}"; do
    CMD="$CMD\\\"$e\\\" "
done

eval "$CMD"

And here is an example of a sample user-supplied.json file that demonstrates an injection attack:

{
  "Include": [
    "\\\";ls -al;echo\\\""
  ]
}

The above JSON file results in the output:
Includes are: ""
, followed by a directory listing (an actual attack would probably be something far more malicious).

What I'd like instead is something like the following to be outputted:
Includes are: "\\\";ls -al;echo\\\""

Edit 1

I used echo as an example command in the script, which probably wasn’t the best example, as then the solution is simply not using eval.

However the actual command that will be needed is dotnet test, and each array item from Includes needs to be passed as an option using /p:<Includes item>. What I was hoping for was a way to globally neutralise injection regardless of the command, but perhaps that’s not possible, ie, the technique you go for relies heavily on the actual command.

2 个答案:

答案 0 :(得分:2)

对于eval,您也不需要使用dotnet test。 POSIX sh中不存在的许多bash扩展特定存在,从而使eval的使用成为不必要。如果您认为,您需要eval,那么您应该提供足够的详细信息,以使我们解释为什么实际上不需要它。 :)

#!/usr/bin/env bash
#              ^^^^- Syntax below is bash-only; the shell *must* be bash, not /bin/sh

include_args=( )
IFS=$'\n' read -r -d '' -a includes < <(jq -r '.Include[]' user-supplied.json && printf '\0')
for include in "${includes[@]}"; do
  include_args+=( "/p:$include" )
done

dotnet test "${include_args[@]}"

对正在发生的事情说一些话:

  • IFS=$'\n' read -r -d '' -a arrayname读取stdin中的下一个NUL字符(-d指定一个字符停止;由于C字符串以NUL终止,因此空字符串中的第一个字符为NUL字节),在换行符上分割,然后将结果放入arrayname

    在bash 4.0或更高版本中编写此命令的较短方法是readarray -t arrayname,但这没有让您检测生成输入的程序是否失败的优点:因为我们有&& printf '\0'附加到jq代码上的read期望的NUL终止符仅在jq成功的情况下存在,从而导致read的退出状态仅在{{ 1}}也报告了成功。

  • jq正在从process substitution重定向标准输入,替换为文件名,当从文件名读取文件名时,该文件名返回运行代码< <(...)的输出。
  • 我们可以设置...并使其与include_args+=( "/p:$include" )完全相同的原因是引号由shell本身读取,并用于确定在何处执行字符串拆分和globbing;它们不会保留在生成的内容中(并随后传递给include_args+=( /p:"$include" ))。

其他一些有用的参考文献:

  • BashFAQ #50我试图将命令放入变量中,但是复杂的情况总是失败!-深入说明了为什么不能在字符串中存储命令< em>不使用(使用dotnet test),并描述了更好的做法(将命令存储在函数中;将命令存储在数组中;等等)。
  • BashFAQ #48评估命令和安全性问题-详细介绍了为什么eval广为人知的原因。

答案 1 :(得分:0)

您根本不需要eval

INCLUDES=( $(jq '.Include[]' user-supplied.json) )
echo "Includes are: "
for e in "${INCLUDES[@]}"; do
  echo "$e"
done

可能发生的最糟糕的情况是,不带引号的命令替换可能会在您不希望的位置执行单词拆分或路径名扩展(这同样在您的原始版本中也是一个问题),但是不可能执行任意命令。