T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/garmin-login.sh:18
- Finding
- Python Code Injection Through Credential Interpolation into a Here-Document<![CDATA[ ## Vulnerability Details **File Location**: `scripts/garmin-login.sh`, lines 18-38 **Vulnerability Type**: Python source-code injection through unsafe string interpolation **Risk Level**: High ### Vulnerable Code ```bash EMAIL=$(op item get "$GARMIN_1P_ITEM_NAME" --vault "$GARMIN_1P_VAULT" --fields username 2>/dev/null) PASSWORD=$(op item get "$GARMIN_1P_ITEM_NAME" --vault "$GARMIN_1P_VAULT" --fields password --reveal 2>/dev/null) if [ -z "$EMAIL" ] || [ -z "$PASSWORD" ]; then echo "❌ Credentials not found: '$GARMIN_1P_ITEM_NAME' in vault '$GARMIN_1P_VAULT'" echo " Set GARMIN_1P_ITEM_NAME / GARMIN_1P_VAULT if yours differ." exit 1 fi mkdir -p /tmp/garmin-session python - <<PYEOF import sys, os os.environ['GARMIN_EMAIL'] = "$EMAIL" os.environ['GARMIN_PASSWORD'] = "$PASSWORD" try: from garminconnect import Garmin client = Garmin(os.environ['GARMIN_EMAIL'], os.environ['GARMIN_PASSWORD']) client.login() client.garth.dump(dir_path='/tmp/garmin-session/') PYEOF ``` ### Technical Analysis The username and password obtained from 1Password are inserted directly into dynamically generated Python source code. The here-document delimiter is unquoted, and the credential values are placed inside Python string literals without Python-compatible escaping. Although shell expansion results are not recursively evaluated as new shell syntax, crafted credential content can terminate the Python string and introduce arbitrary Python statements. Ordinary credentials containing quotation marks, backslashes, or line breaks can also corrupt the generated program and cause authentication failures. The vulnerable pattern crosses a trust boundary: data retrieved from a credential store is treated as executable source text rather than as data. ### Attack Path 1. An attacker obtains permission to modify the configured Garmin item in the selected 1Password vault, or convinces the user to select an attacker-controlled item through `GARMIN_1P_ITEM_NAM ...[truncated 1059 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Never interpolate credential values into source code. Pass them as environment variables or through a protected input channel, and quote the here-document delimiter: ```bash export GARMIN_EMAIL="$EMAIL" export GARMIN_PASSWORD="$PASSWORD" python - <<'PYEOF' import os import sys from garminconnect import Garmin email = os.environ["GARMIN_EMAIL"] password = os.environ["GARMIN_PASSWORD"] client = Garmin(email, password) client.login() PYEOF ``` Additional hardening should include: 1. Unset `GARMIN_EMAIL` and `GARMIN_PASSWORD` immediately after the Python process exits. 2. Restrict the 1Password service account to the single required vault item where supported. 3. Avoid placing secret values in command-line arguments, logs, or exception messages. 4. Prefer implementing authentication entirely in Python, as already done in `get-stats.py`, so credentials never become generated program text. 5. Add tests using passwords containing quotes, backslashes, dollar signs, and line breaks. ]]>
