Skip to main content
11 of 23
added 37 characters in body
Stéphane Chazelas
  • 586.3k
  • 96
  • 1.1k
  • 1.7k

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).

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).

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 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 and ((..)) and [[...]] constructs to name a few).

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] in some locales matches more than 0123456789. [[:digit:]] should be OK but I wouldn't bet on it.

Also remember that numbers with leading zeros are treated as octal in some contexts (010 is sometimes 10, sometimes 8).

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 $ 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:

Stéphane Chazelas
  • 586.3k
  • 96
  • 1.1k
  • 1.7k