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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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.

Content

No source excerpt is available for this finding.

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

High
Category
YARA Match
Confidence
75% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · SKILL.md (reported line 55)May include surrounding context.

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: " in the Gaming list
  • Console: Summary of findings and action confirmations

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.