32

In a comment to a recent question, Stéphane Chazelas mentions that there are security implications to double parentheses arithmetic such as:

x=$((1-$x))

on most shells.

My Google skills seem to be rusty and I can't find anything. What are the security implications of double parentheses arithmetic?

1 Answer 1

39

The problem is in cases where the content of $x has not been sanitized and contains data that could potentially be under the control of an attacker in cases that shell code may end up being used in a privilege escalation context (for instance a script invoked by a setuid application, a sudoers script or used to process off-the-network data (CGI, DHCP hook...) directly or indirectly).

If:

x='(PATH=2)'

Then:

x=$((1-$x)))

has the side effect of setting PATH to 2 (a relative path that could very well be under control of the attacker). You can replace PATH with LD_LIBRARY_PATH or IFS... The same happens with x=$((1-x)) in bash, zsh or ksh (not dash nor yash which only accept numerical constants in variables there).

Note that:

x=$((1-$x))

won't work properly for negative values of $x in some shells that implement the (optional as per POSIX) -- (decrement) operator (as with x=-1, that means asking the shell to evaluate the 1--1 arithmetic expression). "$((1-x))" doesn't have the problem as x is expanded as part of (not before) the arithmetic evaluation.

In bash, zsh and ksh (not dash or yash), if x is:

x='a[0$(uname>&2)]'

Then the expansion of $((1-$x)) or $((1-x)) causes that uname command to be executed (for zsh, a needs to be an array variable, but one can use psvar for instance for that).

In summary, one shouldn't use uninitialised or non-sanitized external data in arithmetic expressions in shells.

Note that arithmetic evaluation can be done by $((...)) (aka $[...] in bash or zsh) but also depending on the shell in the let, [/test, declare/typeset/export..., return, break, continue, exit, printf, print builtins, array indices, ((..)) and [[...]] constructs to name a few).

Because it applies to array indices in ksh/zsh/bash, it also applies to all builtins that take variable names as argument ([/test with -v, read, unset, export/typeset/readonly, print/printf with -v, getopts...).

The fact that operands of numeric test operators are treated as arithmetic expressions with [[...]] and not with the [/test builtin is one reason why in bash or zsh, it's often preferable to use the latter.

Compare:

$ a='x[1$(uname>&2)]' bash -c '[ "$a" -eq "$b" ]'
bash: line 0: [: x[1$(uname>&2)]: integer expression expected

(safe) with:

$ a='x[1$(uname>&2)]' bash -c '[[ "$a" -eq "$b" ]]'
Linux

(uname was executed).

(in ksh, both [ and [[ ... ]] have the problem)

To check that a variable contains a literal decimal integer number, you can use POSIXly:

case $var in
  ("" | - | *[!0123456789-]* | ?*-*) echo >&2 not a valid number; exit 1;;
esac

Beware that [0-9] and [[:digit:]] in some locales matches more than 0123456789 so should be avoided for any form of input validation/sanitisation.

Also remember that numbers with leading zeros are treated as octal in some contexts (010 is sometimes 10, sometimes 8) and beware that the check above will let through numbers that are potentially bigger than the maximum integer supported by your system (or whatever application you will use that integer in; bash for instance treats 18446744073709551616 as 0 as that's 264). So you might want to add extra checks in that case statement above like:

(0?* | -0?*)
  echo >&2 'Only decimal numbers without leading 0 accepted'; exit 1;;
(-??????????* | [!-]?????????*)
  echo >&2 'Only numbers from -999999999 to 999999999 supported'; exit 1;;

Examples:

$ export 'x=psvar[0$(uname>&2)]'
$ ksh93 -c 'echo "$((x))"'
Linux
ksh93: psvar: parameter not set
$ ksh93 -c '[ x -lt 2 ]'
Linux
ksh93: [: psvar: parameter not set
$ bash -c 'echo "$((x))"'
Linux
0
$ bash -c '[[ $x -lt 2 ]]'
Linux
$ bash -c 'typeset -i a; export a="$x"'
Linux
$ bash -c 'typeset -a a=([x]=1)'
Linux
$ bash -c '[ -v "$x" ]'
Linux
$ bash -c 'read "$x"' < /dev/null
Linux
$ env psvar= bash -c 'unset "$x"'
Linux
$ mksh -c '[[ $x -lt 2 ]]'
Linux
$ zsh -c 'echo "$((x))"'
Linux
0
$ zsh -c 'printf %d $x'
Linux
0
$ zsh -c 'integer x'
Linux
$ zsh -c 'exit $x'
Linux

More reading at:

2
  • 1
    x='P=3'; : $(($x + 5)) will set P to 8, but x='P=3'; : $((x + 5)) will set P to 3 (in zsh, ksh or bash). "The same happens with $((x + 1)) ..." is not correct now; it will set PATH to 2, as of old.
    – user313992
    Jan 9, 2019 at 22:51
  • @mosvy, right thanks (and for your earlier edit). Corrected now. Jan 9, 2019 at 23:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .