T09 · Insecure Skill Coding Practices
- Location
scripts/copilot-alert.sh:51- Finding
Arbitrary Python Code Injection Through Unquoted Heredoc Interpolation
- Content
View full analysis
Vulnerability Details
File Location:
scripts/copilot-alert.sh, lines 51–64
Vulnerability Type: Python source injection through unsafe shell-variable interpolation
Risk Level: HighVulnerable Code
bash python3 << PYEOF import json, sys, datetime data = json.loads(r"""$DATA""") items = data.get("usageItems", []) total_included = sum(i["discountQuantity"] for i in items) total_overage = sum(i["netQuantity"] for i in items) total_cost = sum(i["netAmount"] for i in items) quota = $QUOTA plan = "$PLAN_NAME" threshold = $THRESHOLDTechnical Analysis
The script uses an unquoted heredoc delimiter, causing the shell to expand
$DATA,$QUOTA,$PLAN_NAME, and$THRESHOLDdirectly into executable Python source.These values cross trust boundaries without syntactic escaping or type validation:
$THRESHOLDcomes directly from the--thresholdcommand-line argument.$QUOTAand$PLAN_NAMEare read from the user-writable~/.config/copilot-usage/config.jsonfile.$DATAcontains the GitHub API response and is inserted into a triple-quoted Python string.
An attacker-controlled value can terminate its intended Python expression and append arbitrary Python statements. For example, a threshold value conceptually shaped as:
text 0; import os; os.system("attacker-command"); #produces executable Python resembling:
python threshold = 0; import os; os.system("attacker-command"); #Similar injection is possible through a crafted plan name that closes the surrounding quoted string. The API response is also not safely transported: a value containing the relevant triple-quote sequence could escape the
json.loadsstring literal.The vulnerability is caused by generating source code from data rather than passing data through a non-executable channel such as standard input, a temporary file, environment variables, or ...[truncated 1711 chars]
- Remediation
View remediation
Remediation Suggestions
-
Use a quoted heredoc delimiter so the shell cannot interpolate data into Python source:
bash python3 - "$QUOTA" "$PLAN_NAME" "$THRESHOLD" <<'PYEOF' import json import sys quota = int(sys.argv[1]) plan = sys.argv[2] threshold = int(sys.argv[3]) data = json.load(sys.stdin) PYEOFBecause both the program and JSON cannot conveniently occupy the same standard-input stream, prefer storing the API response in a securely created temporary file and passing its path through
sys.argv, as already done bycopilot-usage.sh. -
A safe implementation pattern is:
bash TMP_FILE=$(mktemp "${TMPDIR:-/tmp}/copilot-alert.XXXXXX") trap 'rm -f "$TMP_FILE"' EXIT gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "/users/${GH_USER}/settings/billing/premium_request/usage" \ > "$TMP_FILE" python3 - "$TMP_FILE" "$QUOTA" "$PLAN_NAME" "$THRESHOLD" <<'PYEOF' import json import sys data_path, quota_raw, plan, threshold_raw = sys.argv[1:] quota = int(quota_raw) threshold = int(threshold_raw) with open(data_path, encoding="utf-8") as handle: data = json.load(handle) PYEOF -
Validate
--thresholdbefore any network request or Python invocation. Require a decimal integer and enforce a documented range such as 0–100. -
Validate configuration after loading it:
- Require
quotato be a positive bounded integer. - Require
planto match the existing plan allowlist. - Reject malformed JSON and unexpected data types with a clear error.
- Require
-
Quote temporary-file variables in traps and other shell operations. Avoid constructing executable source code from API responses, configuration fields, or command-line arguments under all circumstances.
-
