Back to skill

Security audit

GOG Stale Game Cleanup

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated purpose, but its script has unsafe code-injection paths and performs email/reminder side effects by default, so users should review it before installing.

Review and fix scripts/stale_games.sh before running it, especially the dynamic Python snippets. Use DRY_RUN=true first, do not schedule it with cron until the injection issues are fixed, and only use it if you are comfortable emailing game names, last-played dates, and install paths to the configured recipient and creating reminder entries.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/stale_games.sh:27
Finding
Arbitrary Python Code Injection Through GOG_LIBRARY## Vulnerability Details **File Location**: `scripts/stale_games.sh`, lines 27–53 **Vulnerability Type**: Environment-variable injection into dynamically constructed Python source **Risk Level**: High ### Vulnerable Code ```bash # Extract stale installed games via python (jq may not be available) STALE_JSON=$(python3 -c " import json, sys from datetime import datetime, timezone with open('$GOG_LIBRARY') as f: lib = json.load(f) cutoff = datetime.fromisoformat('$CUTOFF').replace(tzinfo=None) stale = [] for g in lib.get('games', []): if not g.get('installed'): continue lp = g.get('last_played') if lp is None: stale.append(g) continue try: dt = datetime.fromisoformat(lp).replace(tzinfo=None) except Exception: stale.append(g) continue if dt < cutoff: stale.append(g) print(json.dumps(stale)) ") ``` ### Technical Analysis `GOG_LIBRARY` is supplied through an environment variable and inserted directly into a Python program passed to `python3 -c`. Shell quoting does not make this safe because the expansion occurs inside a double-quoted shell string, while the resulting value is placed inside a single-quoted Python string: ```python with open('$GOG_LIBRARY') as f: ``` A value containing a single quote, newline, and additional Python syntax can terminate the intended string and alter the generated program. Python then evaluates the injected statements with the privileges of the user running the Skill. This is source-code injection rather than shell command injection. Argument-array use elsewhere in the script does not mitigate this source-to-interpreter path. Exploitation requires the attacker to control or influence `GOG_LIBRARY`, such as through a wrapper, automation configuration, inherited environment, or unsafe scheduled-task configuration. ### Attack Path 1. An attacker gains influence ove ...[truncated 1189 chars]
Remediation
## Remediation Suggestions Never interpolate file paths or other data into Python source. Pass values as positional arguments: ```bash STALE_JSON=$(python3 - "$GOG_LIBRARY" "$CUTOFF" <<'PY' import json import sys from datetime import datetime library_path = sys.argv[1] cutoff_text = sys.argv[2] with open(library_path, encoding="utf-8") as f: lib = json.load(f) cutoff = datetime.fromisoformat(cutoff_text).replace(tzinfo=None) # Continue processing here. PY ) ``` Additionally: - Verify that the path exists, is a regular file, and is readable before invoking Python. - Consider rejecting symbolic links if the automation is expected to read only a fixed configuration file. - Use a fixed script file or a single-quoted heredoc so shell expansion cannot modify Python source. - Run scheduled instances under the least-privileged user and with a controlled environment.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/stale_games.sh:60
Finding
Arbitrary Python Code Injection Through STALE_DAYS## Vulnerability Details **File Location**: `scripts/stale_games.sh`, lines 60–82 **Vulnerability Type**: Unvalidated configuration value interpolated into Python source **Risk Level**: High ### Vulnerable Code ```bash # Build email body EMAIL_BODY=$(echo "$STALE_JSON" | python3 -c " import json, sys from datetime import datetime stale = json.load(sys.stdin) lines = ['GOG Stale Game Report', '=' * 40, ''] lines.append('The following installed games have not been played in ${STALE_DAYS}+ days:') lines.append('') for i, g in enumerate(stale, 1): lp = g.get('last_played', 'Never') name = g.get('name', 'Unknown') path = g.get('install_path', 'N/A') lines.append(f'{i}. {name}') lines.append(f' Last played: {lp}') lines.append(f' Install path: {path}') lines.append('') lines.append(f'Total: {len(stale)} game(s)') print('\n'.join(lines)) ") ``` ### Technical Analysis `STALE_DAYS` is intended to be a numeric threshold, but the script performs no numeric validation. It is directly expanded into a single-quoted Python string contained in the `python3 -c` program. An attacker-controlled value can close the argument to `lines.append()`, insert another Python statement, and then provide syntax that makes the rest of the line valid. For example, the injection structure can terminate the current string, execute a benign `print()` call, and open another string for the remaining `+ days:` text. The same structure can execute arbitrary Python APIs or operating-system commands. Although `STALE_DAYS` is also used as part of a quoted argument to `date`, shell metacharacters in that use are not independently interpreted as shell syntax. The vulnerable execution sink is the later interpolation into Python source. ### Attack Path 1. An attacker controls or modifies `STALE_DAYS` in the environment or scheduled-task configuration. 2. The supplied value includes a quote and valid Pytho ...[truncated 991 chars]
Remediation
## Remediation Suggestions Validate `STALE_DAYS` immediately after loading it: ```bash if [[ ! "$STALE_DAYS" =~ ^[0-9]+$ ]] || (( STALE_DAYS < 1 || STALE_DAYS > 36500 )); then echo "ERROR: STALE_DAYS must be a positive integer in the supported range" >&2 exit 1 fi ``` Validation is defense in depth; the value should still be passed as data rather than embedded into Python source: ```bash EMAIL_BODY=$(printf '%s\n' "$STALE_JSON" | python3 -c ' import json import sys stale_days = sys.argv[1] stale = json.load(sys.stdin) lines = ["GOG Stale Game Report", "=" * 40, ""] lines.append( f"The following installed games have not been played in {stale_days}+ days:" ) # Continue report construction here. ' "$STALE_DAYS") ``` Prefer moving the Python logic into a fixed `.py` file to eliminate repeated dynamic-source construction.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/stale_games.sh:88
Finding
Arbitrary Python Code Injection Through REMINDERS_LIST## Vulnerability Details **File Location**: `scripts/stale_games.sh`, lines 88–99 **Vulnerability Type**: Environment-variable injection into reminder-generation Python source **Risk Level**: High ### Vulnerable Code ```bash # Add to Apple Reminders if [[ "$DRY_RUN" != "true" ]]; then echo "$STALE_JSON" | python3 -c " import json, sys, subprocess stale = json.load(sys.stdin) for g in stale: name = g.get('name', 'Unknown') title = f'Consider uninstalling: {name}' try: subprocess.run(['remindctl', 'add', '--title', title, '--list', '$REMINDERS_LIST'], check=True) print(f' ✓ Reminder added: {title}') except Exception as e: print(f' ✗ Failed to add reminder for {name}: {e}', file=sys.stderr) " ``` ### Technical Analysis `REMINDERS_LIST` is inserted directly into a Python list literal inside dynamically generated source. A crafted value can terminate the string or list expression and insert additional Python statements. The normal `subprocess.run()` call safely places game names and the list name into an argument array, so ordinary reminder names do not cause shell interpretation. However, this protection is bypassed before `subprocess.run()` is reached because `REMINDERS_LIST` has already been parsed as Python source. The vulnerable branch is disabled only when `DRY_RUN` is exactly `true`. In normal execution, the injected source is evaluated once stale games have been identified. ### Attack Path 1. An attacker controls `REMINDERS_LIST` through the execution environment or automation configuration. 2. The attacker supplies Python syntax that closes the intended list-name string and surrounding expression. 3. Execution proceeds with `DRY_RUN` not equal to `true` and with at least one stale game. 4. Bash expands the crafted value into the `python3 -c` program. 5. Python executes the inserted statements as the current user. A non-destructive proof of conc ...[truncated 602 chars]
Remediation
## Remediation Suggestions Pass the reminder list as a positional argument while continuing to read JSON from standard input: ```bash printf '%s\n' "$STALE_JSON" | python3 -c ' import json import subprocess import sys reminders_list = sys.argv[1] stale = json.load(sys.stdin) for game in stale: name = game.get("name", "Unknown") title = f"Consider uninstalling: {name}" subprocess.run( ["remindctl", "add", "--title", title, "--list", reminders_list], check=True, ) ' "$REMINDERS_LIST" ``` Further hardening should include: - Rejecting empty list names and optionally enforcing a reasonable maximum length. - Resolving trusted executables by a controlled absolute path for scheduled runs. - Defining a restricted `PATH` in cron rather than inheriting an uncontrolled environment. - Preserving argument-array subprocess invocation; do not introduce `shell=True` or `eval`. - Moving all Python logic into a fixed script to prevent future source-interpolation defects.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • YARA SignaturesMalware Match, Webshell Match, Cryptominer Match
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (5)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared purpose understates important behavior: the skill exfiltrates local game library details by email and writes reminders, yet those side effects are not clearly represented as permissions or warnings. It also classifies never-played or malformed metadata as stale, which broadens the action scope beyond the advertised '30+ days' criterion and can lead to inaccurate reporting or unwanted cleanup prompts.

YARA rule 'backdoor_persistence': Backdoor persistence with malicious payloads (shell commands, SSH key injection, hidden root users) [malware]

High
Category
YARA Match
Content
LIBRARY` | Yes | — | Path to GOG library JSON |
| `EMAIL_TO` | Yes | — | Recipient email address |
| `EMAIL_ACCOUNT` | No | `personal` | Himalaya account name |
| `REMINDERS_LIST` | No | `Gaming` | Apple Reminders list name |
| `STALE_DAYS` | No | `30` | Days threshold for stale |
| `DRY_RUN` | No | `false` | Preview without sending |

### Scheduling

To run weekly via cron:

```bash
# Add to crontab or use OpenClaw cron
0 10 * * 1 GOG_LIBRARY=... EMAIL_TO=... bash /path/to/scripts/stale_games.sh
```

## Output

- **Email**: Formatted report listing each stale game with last-played date and install path
- **Reminders**: One reminder per stale game titled "Consider uninstalling: <game name>" in the Gaming list
- **Console**: Summary of findings and action confirmations
Confidence
75% confidence
Finding
YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill documents shell-based execution, email sending, and reminder creation, but it declares no explicit tool scope or permissions. That creates an authorization gap where a caller may invoke side-effecting operations without clear user consent boundaries or platform enforcement.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The trigger phrases are broad enough that the skill could activate on vague requests about game cleanup or stale games without the user specifically intending email transmission or reminder creation. In context, this increases the chance of surprise side effects involving local data disclosure and persistent task creation.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill description does not clearly warn that it sends game library details by email, including last-played dates and install paths. Even if the content is low sensitivity, outbound transmission of local system metadata without an explicit warning undermines informed consent and can leak private usage patterns.

Static analysis

No suspicious patterns detected.