Back to skill

Security audit

ChessGuardian

Security checks for vulnerabilities and agentic risk

Overview

This skill mostly does what it says, but its board-snapshot workflow can run an unpinned Playwright package through npx, so it should be reviewed before installation.

Install only if you trust the ChessGuardian endpoint and are comfortable with the skill making network requests and running local bot processes. Before using board snapshots, prefer installing and pinning Playwright in a controlled environment instead of allowing `npx` to resolve packages at runtime; also restrict custom `--url` use to trusted ChessGuardian instances.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T08 · Insecure Dependencies

Warning
Location
SKILL.md:70
Finding
Unpinned Playwright Package Execution Through npx<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:70-73` **Vulnerability Type**: Unpinned third-party package execution **Risk Level**: Medium ### Vulnerable Code ```markdown ### Render board snapshot 1. Generate an HTML file using chessboard.js with the FEN from game state 2. Screenshot with Playwright: `npx playwright screenshot --browser chromium --viewport-size=440,520 board.html output.png` 3. Send the image to user ``` ### Technical Analysis The documented workflow instructs the agent to execute Playwright through `npx` without specifying a package version, lockfile, integrity hash, or verified local installation. When the package is not already available locally, `npx` can retrieve and execute the currently resolved package from the configured package registry. Consequently, the code executed by this workflow can differ from the code that existed when the skill was reviewed. This creates a supply-chain risk because the effective executable dependency is mutable and outside the audited project. There is no evidence in the reviewed project that the current Playwright package is malicious. The vulnerability is the unsafe, unpinned package-resolution and execution mechanism. ### Attack Path 1. A user asks the agent to render a chessboard snapshot. 2. The agent follows the workflow in `SKILL.md`. 3. The environment does not contain a verified local Playwright installation. 4. `npx` resolves and potentially downloads the package from its configured registry. 5. If the resolved package, a transitive dependency, registry response, or package-resolution configuration has been compromised, attacker-controlled package code executes. 6. The malicious code runs with the operating-system permissions and data access of the account invoking the skill. ### Impact Assessment Successful exploitation could permit arbitrary local code execution within the privileges of the agent process. Depending on the execution environment, this could expose files ...[truncated 368 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Declare Playwright as a project dependency at an explicitly approved version. 2. Commit and enforce a package lockfile containing dependency versions and integrity metadata. 3. Install dependencies during a controlled setup or build phase rather than during normal skill execution. 4. Invoke the verified local binary, for example: ```bash ./node_modules/.bin/playwright screenshot \ --browser chromium \ --viewport-size=440,520 \ board.html output.png ``` 5. Use deterministic installation such as `npm ci` and configure normal skill execution to reject implicit package downloads. 6. Apply registry allowlisting, dependency scanning, and lockfile integrity verification in the deployment pipeline. 7. Where practical, run screenshot generation in a sandbox with restricted filesystem, credential, and network access. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/autoplay_minimax.py:250
Finding
Missing HTTP Request Timeouts in Minimax Autoplay Client<![CDATA[ ## Vulnerability Details **File Location**: `scripts/autoplay_minimax.py:250-268` **Vulnerability Type**: Unbounded outbound HTTP operations **Risk Level**: Low ### Vulnerable Code ```python def start_new_game(base_url): resp = requests.post(f"{base_url}/api/live/start", json={"mode": "ai"}) resp.raise_for_status() data = resp.json() game_id = data["id"] print(f"🆕 New game started: {game_id}") print(f" First move: {data['history'][0]}") return game_id def get_state(base_url, game_id): resp = requests.get(f"{base_url}/api/live/{game_id}") resp.raise_for_status() return resp.json() def make_move(base_url, game_id, move): resp = requests.post(f"{base_url}/api/live/{game_id}/move", json={"move": move}) return resp.json() ``` ### Technical Analysis The `requests.get` and `requests.post` calls do not provide a `timeout` value. Python Requests does not impose a default overall request timeout, so a remote endpoint that accepts a connection but delays or never completes its response can block the autoplay process indefinitely. The affected base URL can be supplied through the `--url` command-line option, which makes this behavior directly reachable when the bot is configured to communicate with an untrusted or malfunctioning service. ### Attack Path 1. An operator launches the Minimax bot with a slow or attacker-controlled `--url`, or the configured ChessGuardian service becomes unresponsive. 2. The bot calls `start_new_game`, `get_state`, or `make_move`. 3. The endpoint accepts the connection but withholds part or all of the response. 4. Because no connect or read timeout is configured, the request remains blocked. 5. The autoplay loop stops progressing and retains its process resources until externally terminated or the connection fails. ### Impact Assessment The primary impact is denial of service against the individual autoplay process. An attacker can cause the process to hang, prevent furt ...[truncated 283 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Set explicit connect and read timeouts on every request: ```python REQUEST_TIMEOUT = (5, 30) resp = requests.post( f"{base_url}/api/live/start", json={"mode": "ai"}, timeout=REQUEST_TIMEOUT, ) ``` 2. Apply the same timeout to `get_state` and `make_move`. 3. Catch `requests.Timeout` and `requests.ConnectionError` so failures are reported cleanly. 4. Add a bounded retry policy with exponential backoff and jitter for idempotent requests. 5. Avoid automatically retrying move-submission requests unless the API supports idempotency keys, because an ambiguous timeout could otherwise result in duplicate submissions. 6. Validate that `--url` uses an approved `https` origin when arbitrary endpoints are not required. 7. Call `raise_for_status()` in `make_move` before parsing the response, while preserving any documented API error handling. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/autoplay_stockfish.py:38
Finding
Missing HTTP Request Timeouts in Stockfish Autoplay Client<![CDATA[ ## Vulnerability Details **File Location**: `scripts/autoplay_stockfish.py:38-56` **Vulnerability Type**: Unbounded outbound HTTP operations **Risk Level**: Low ### Vulnerable Code ```python def start_new_game(base_url): resp = requests.post(f"{base_url}/api/live/start", json={"mode": "ai"}) resp.raise_for_status() data = resp.json() game_id = data["id"] print(f"🆕 New game started: {game_id}") print(f" First move: {data['history'][0]}") return game_id def get_state(base_url, game_id): resp = requests.get(f"{base_url}/api/live/{game_id}") resp.raise_for_status() return resp.json() def make_move(base_url, game_id, move): resp = requests.post(f"{base_url}/api/live/{game_id}/move", json={"move": move}) return resp.json() ``` ### Technical Analysis All ChessGuardian API calls omit the Requests `timeout` parameter. A server can therefore leave a connection or response incomplete for an unbounded period, causing the Stockfish autoplay process to block inside the HTTP call. The `--url` argument permits selection of the remote endpoint. Thus, a malicious endpoint can intentionally trigger the condition, while an unavailable or degraded legitimate endpoint can trigger the same availability failure accidentally. ### Attack Path 1. The Stockfish bot is started with an attacker-controlled endpoint, or the legitimate service becomes unresponsive. 2. The bot issues a request to start a game, retrieve game state, or submit a move. 3. The remote service accepts the request but does not finish its response. 4. The HTTP operation blocks because there is no configured timeout. 5. The autoplay loop and its Stockfish subprocess remain occupied until the process is externally terminated or the connection eventually fails. ### Impact Assessment Exploitation causes denial of service for the affected bot session and can retain both the Python process and the associated Stockfish engine process. Repeated stalled i ...[truncated 277 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define and enforce bounded connect and read timeouts: ```python REQUEST_TIMEOUT = (5, 30) resp = requests.get( f"{base_url}/api/live/{game_id}", timeout=REQUEST_TIMEOUT, ) ``` 2. Add equivalent timeout handling to all POST requests. 3. Catch `requests.Timeout` and connection exceptions, terminate or retry safely, and ensure the Stockfish engine is still closed through the existing `finally` block. 4. Use limited exponential-backoff retries for game-state retrieval. 5. Do not blindly retry move submissions unless the service provides idempotency guarantees. 6. Restrict `--url` to trusted HTTPS origins where arbitrary ChessGuardian instances are not a functional requirement. 7. Add `raise_for_status()` to `make_move` and handle non-JSON error responses without crashing. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (11)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill explicitly instructs the agent to use both network access and shell execution, but it declares no tool scope or permission boundaries. That means an orchestrator may expose broader capabilities than intended, increasing the chance of arbitrary command execution, external requests, or unsafe chaining when the skill is invoked.

Vague Triggers

Medium
Confidence
96% confidence
Finding
The manifest description says to use the skill for specific chess actions and then broadens scope to 'analyze a chess position, or any chess-related interaction.' That catch-all phrasing is ambiguous and could cause unintended invocation on ordinary chess discussion rather than only when the user wants API-backed gameplay or board operations.

Rp1

Medium
Category
MCP Rug Pull
Confidence
88% confidence
Finding
Using `npx playwright` without a pinned version allows the runtime to resolve whatever package version is current or otherwise available at execution time. This creates a supply-chain risk and undermines reproducibility, because behavior or dependencies could change unexpectedly and potentially introduce malicious or vulnerable code into the skill's execution path.

Tainted flow: 'game_id' from requests.post (line 272, network input) → requests.get (network output)

Medium
Category
Data Flow
Content
def get_state(base_url, game_id):
    resp = requests.get(f"{base_url}/api/live/{game_id}")
    resp.raise_for_status()
    return resp.json()
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'game_id' from requests.post (line 272, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
def make_move(base_url, game_id, move):
    resp = requests.post(f"{base_url}/api/live/{game_id}/move", json={"move": move})
    return resp.json()
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

External Transmission

Medium
Category
Data Exfiltration
Content
def start_new_game(base_url):
    resp = requests.post(f"{base_url}/api/live/start", json={"mode": "ai"})
    resp.raise_for_status()
    data = resp.json()
    game_id = data["id"]
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def start_new_game(base_url):
    resp = requests.post(f"{base_url}/api/live/start", json={"mode": "ai"})
    resp.raise_for_status()
    data = resp.json()
    game_id = data["id"]
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'game_id' from requests.post (line 40, network input) → requests.get (network output)

Medium
Category
Data Flow
Content
def get_state(base_url, game_id):
    resp = requests.get(f"{base_url}/api/live/{game_id}")
    resp.raise_for_status()
    return resp.json()
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

External Transmission

Medium
Category
Data Exfiltration
Content
def make_move(base_url, game_id, move):
    resp = requests.post(f"{base_url}/api/live/{game_id}/move", json={"move": move})
    return resp.json()
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
def make_move(base_url, game_id, move):
    resp = requests.post(f"{base_url}/api/live/{game_id}/move", json={"move": move})
    return resp.json()
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'game_id' from requests.post (line 40, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
def make_move(base_url, game_id, move):
    resp = requests.post(f"{base_url}/api/live/{game_id}/move", json={"move": move})
    return resp.json()
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.