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=$(($x + 1)))
has the side effect of setting `PATH` to `3` (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=$((x + 1))` 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 `$(($x+1))` or `$((x+1))` 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).
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) 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 2<sup>64</sup>). 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
$ 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:
- http://www.zsh.org/mla/workers/2014/msg01041.html (where Oliver Kiddle brought the `x[0$(...)]` issue to our attention).
- http://thread.gmane.org/gmane.comp.standards.posix.austin.general/9971
- http://thread.gmane.org/gmane.comp.shells.bash.bugs/22737 for another mis-design potentially leading to code injection in `bash`.
- https://unix.stackexchange.com/q/171346 where that and leaving a variable unquoted can aggravate each other.