T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/say.sh:41
- Finding
- PowerShell Command Injection Through Unsafely Interpolated Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/say.sh`, lines 41–59 **Vulnerability Type**: PowerShell command injection **Risk Level**: High ### Vulnerable Code ```bash PS="Add-Type -AssemblyName System.Speech; \$s=[System.Speech.Synthesis.SpeechSynthesizer]::new();" if [[ -n "$VOICE" ]]; then VOICE_ESC=$(printf '%s' "$VOICE" | sed 's/"/`"/g') PS+=" \$s.SelectVoice(\"$VOICE_ESC\");" fi if [[ -n "$RATE" ]]; then PS+=" \$s.Rate=[int]$RATE;" fi if [[ -n "$VOLUME" ]]; then PS+=" \$s.Volume=[int]$VOLUME;" fi TEXT_ESC=$(printf '%s' "$TEXT" | sed 's/"/`"/g') PS+=" \$s.Speak(\"$TEXT_ESC\");" powershell.exe -NoProfile -Command "$PS" >/dev/null ``` ### Technical Analysis The script constructs executable PowerShell source by concatenating command-line arguments into the `PS` variable and then passes the resulting string to `powershell.exe -Command`. The attempted sanitization of `VOICE` and `TEXT` only escapes double-quote characters. These values remain inside PowerShell double-quoted strings, where PowerShell evaluates expandable expressions, including `$()` subexpressions. Consequently, attacker-controlled text containing a PowerShell subexpression can cause commands to execute while PowerShell evaluates the argument to `Speak()` or `SelectVoice()`. The `RATE` and `VOLUME` arguments present an additional and more direct injection path. They are inserted into PowerShell source without quoting, numeric validation, or range validation. Casting the value with `[int]` does not make the construction safe because an attacker can introduce PowerShell statement separators and append another statement after a syntactically valid numeric expression. The documented expected ranges—`-10..10` for rate and `0..100` for volume—are not enforced. ### Attack Path 1. An attacker gains control over text or options passed to `scripts/say.sh`, such as through an untrusted user request processed by an agent. 2. The attacker supplies either: - speech t ...[truncated 1329 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Do not concatenate user-controlled values into PowerShell source.** Use a fixed PowerShell script and transfer text, voice, rate, and volume as data through environment variables, standard input, or safely serialized parameters. 2. **Avoid `Invoke-Expression` and equivalent dynamic evaluation.** PowerShell should parse only trusted, static program text. 3. **Validate numeric arguments in Bash before invoking PowerShell.** - Require `RATE` to match an integer-only pattern and enforce the range `-10` through `10`. - Require `VOLUME` to match an integer-only pattern and enforce the range `0` through `100`. - Reject invalid values rather than attempting to sanitize them. 4. **Validate voice selection.** Retrieve installed voice names and require the requested name to exactly match an installed voice. Even after validation, pass the name as data rather than source code. 5. **Use a static PowerShell command that reads environment variables**, for example: ```bash if [[ -n "$RATE" ]]; then [[ "$RATE" =~ ^-?[0-9]+$ ]] || { printf 'Invalid rate: expected an integer from -10 to 10\n' >&2 exit 1 } (( RATE >= -10 && RATE <= 10 )) || { printf 'Invalid rate: expected an integer from -10 to 10\n' >&2 exit 1 } fi if [[ -n "$VOLUME" ]]; then [[ "$VOLUME" =~ ^[0-9]+$ ]] || { printf 'Invalid volume: expected an integer from 0 to 100\n' >&2 exit 1 } (( VOLUME >= 0 && VOLUME <= 100 )) || { printf 'Invalid volume: expected an integer from 0 to 100\n' >&2 exit 1 } fi TTS_TEXT="$TEXT" \ TTS_VOICE="$VOICE" \ TTS_RATE="$RATE" \ TTS_VOLUME="$VOLUME" \ powershell.exe -NoProfile -Command ' Add-Type -AssemblyName System.Speech $s = [System.Speech.Synthesis.SpeechSynthesizer]::new() if ($env:TTS_VOICE) { $s.SelectVoice($env:TTS_VOICE) } if ($env:TTS_RATE) { $s.Rate = [int]$env:TTS_RATE } if ($env:TTS_VOLUME) { $s.Volume = [int]$env:TTS_VOLUME } $s.Speak($env:TTS_TEXT) ' ``` 6 ...[truncated 205 chars]
