Back to skill

Security audit

Tennis Live-State Gate

Security checks for vulnerabilities and agentic risk

Overview

This observe-only tennis gate is mostly coherent, but it has credential-handling and fail-open freshness issues that users should review before installing.

Install only if you are comfortable with a read-only market decision aid that uses Live Tennis and optional Simmer API keys. Run it with least-privilege keys, avoid setting LIVETENNIS_BASE_URL in production, and consider fixing the timestamp handling so missing, invalid, or future timestamps fail closed before using its output in any workflow that can move money.

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)

T09 · Insecure Skill Coding Practices

Error
Location
tennis_live_gate_skill.py:363
Finding

Live Tennis API Key Can Be Sent to an Arbitrary Endpoint

Content
View full analysis

Vulnerability Details

File Location: tennis_live_gate_skill.py:363-368; externally reachable through scripts/status.py:38-42
Vulnerability Type: Unrestricted credential forwarding
Risk Level: High

Vulnerable Code

python
url = base_url.rstrip("/") + path
if params:
    url += "?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers={"X-API-Key": api_key})
try:
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8"))

The status script permits the destination to be selected through an environment variable:

python
base_url = os.environ.get("LIVETENNIS_BASE_URL", DEFAULT_BASE_URL)
players = [sys.argv[1], sys.argv[2]]

print(f"Resolving live match for: {players[0]} vs {players[1]}\n")
try:
    state = fetch_live_tennis_state(players, api_key=api_key, base_url=base_url)

Technical Analysis

The HTTP helper attaches the sensitive LIVETENNIS_API_KEY value to every request as an X-API-Key header. However, base_url is not constrained to the official Live Tennis API origin and is not required to use HTTPS.

In scripts/status.py, this value comes directly from LIVETENNIS_BASE_URL. Consequently, anyone who can influence the process environment or convince a user to configure a custom endpoint can redirect requests and their credential to an attacker-controlled server. An http:// destination also exposes the key to network interception.

A base URL override can be useful for testing, but unrestricted forwarding of a production credential exceeds the minimum privileges required by the declared functionality. The normal operation only needs access to the official Live Tennis API host.

Attack Path

  1. An attacker influences deployment instructions, shell configuration, a task runner, or the environment used to launch the status script.
  2. The attacker sets LIVETENNIS_BASE_URL to a ...[truncated 833 chars]
Remediation
View remediation

Remediation Suggestions

  1. Remove the production base URL override unless it is operationally necessary.
  2. Require the https scheme before attaching any credential.
  3. Allowlist the exact official hostname, api.livetennisapi.com, and the expected API path prefix.
  4. Reject URLs containing user information, unexpected ports, fragments, or ambiguous host representations.
  5. Prevent credential forwarding when an HTTP redirect changes the origin.
  6. If testing against alternate services is required, use an explicitly named test-only key and require a separate opt-in flag.
  7. Add tests confirming that HTTP URLs, unknown hosts, and cross-origin redirects are rejected before a request is sent.
  8. Avoid printing the key or including it in raised exceptions and diagnostic logs.

T09 · Insecure Skill Coding Practices

Error
Location
tennis_live_gate_skill.py:275
Finding

Missing or Invalid Score Timestamps Bypass the Freshness Gate

Content
View full analysis

Vulnerability Details

File Location: tennis_live_gate_skill.py:275-276 and tennis_live_gate_skill.py:454-455
Vulnerability Type: Fail-open validation of unverifiable live data
Risk Level: High

Vulnerable Code

Timestamp parsing failures and missing timestamps are converted to age_s=None:

python
as_of = (score or {}).get("timestamp")
as_of_epoch = _parse_iso8601(as_of)
age_s = (now - as_of_epoch) if as_of_epoch is not None else None

The freshness check only rejects a state when an age is available:

python
if state.age_s is not None and state.age_s > max_staleness_s:
    return _skip("state_stale", state)

The fail-open behavior is explicitly accepted by a test:

python
def test_no_age_means_not_stale(self):
    # age_s None (no timestamp) should not trip the staleness gate.
    d = live_tennis_state_gate(_state(age_s=None))
    self.assertTrue(d.allowed)

Technical Analysis

The gate is documented as failing closed when it cannot verify live state. In practice, an absent or malformed timestamp causes _parse_iso8601 to return None, which results in age_s=None. The gate then skips the staleness comparison and may return allowed=True if the remaining score fields appear valid.

Timestamp freshness is a security-relevant precondition for this Skill because its output may be used to size a time-sensitive market position. Treating an unverifiable timestamp as acceptable contradicts the declared fail-closed model and allows stale cached, delayed, malformed, or manipulated responses to authorize sizing.

The same logic also lacks explicit handling for timestamps materially in the future. Such values produce a negative age and pass the current maximum-age comparison.

Attack Path

  1. The Live Tennis API, an intermediary, a compromised test endpoint, or malformed fixture data supplies a score object with no timestamp, an invalid timestamp, or a material ...[truncated 1038 chars]
Remediation
View remediation

Remediation Suggestions

  1. Fail closed whenever as_of is absent or cannot be parsed.
  2. Introduce a distinct reason such as timestamp_unavailable, or consistently classify the result as state_undetermined.
  3. Reject timestamps beyond a small permitted clock-skew tolerance in the future.
  4. Require age_s to be finite and within a valid range such as -allowed_clock_skew_s <= age_s <= max_staleness_s.
  5. Validate the timestamp before evaluating score or break-point data.
  6. Replace test_no_age_means_not_stale with a test asserting that missing age produces allowed=False.
  7. Add tests for missing timestamps, malformed ISO-8601 values, naive timestamps, extreme values, and future timestamps.
  8. Update documentation so its stated fail-closed behavior exactly matches the implementation.

T08 · Insecure Dependencies

Warning
Location
clawhub.json:9
Finding

Mutable Unpinned Dependencies Can Execute with User Credentials

Content
View full analysis

Vulnerability Details

File Location: clawhub.json:9-11; additional publishing command at README.md:101
Vulnerability Type: Unpinned executable third-party dependencies
Risk Level: Medium

Vulnerable Code

The runtime dependency is declared without a version or integrity constraint:

json
"pip": [
  "simmer-sdk"
]

The publishing instructions also execute the latest available package release:

bash
npx clawhub@latest publish . --slug simmer-tennis-live-gate --version 0.1.0

Technical Analysis

Both dependency declarations resolve to mutable package-registry content. The unversioned simmer-sdk requirement can install a different release over time, while npx clawhub@latest explicitly downloads and executes whichever release currently carries the latest tag.

Package installation and npx execution can run third-party code locally. If a package account, registry, maintainer release process, or transitive dependency is compromised, malicious code can execute before or during normal Skill operation. No lockfile, hash verification, or reviewed version constraint is provided to make installation reproducible.

This is particularly sensitive because the documented workflows use environment variables containing LIVETENNIS_API_KEY and potentially SIMMER_API_KEY.

Attack Path

  1. An attacker compromises the package, its maintainer account, the registry distribution process, or a transitive dependency.
  2. A malicious release becomes the newest simmer-sdk version or receives the latest tag for the ClawHub package.
  3. A user installs the Skill dependency or runs the documented npx clawhub@latest command.
  4. The package manager downloads and executes the changed package code.
  5. The malicious package runs with the privileges of the invoking user.
  6. It can read available environment credentials, inspect or modify repository files, and perform network requests within t ...[truncated 580 chars]
Remediation
View remediation

Remediation Suggestions

  1. Pin simmer-sdk to a specific reviewed version rather than an unconstrained package name.
  2. Use a lockfile or hashed requirements file so package artifacts and transitive dependencies are integrity-checked.
  3. Replace npx clawhub@latest with a specific reviewed version.
  4. Commit the selected package version and integrity metadata to the repository.
  5. Run publishing tools in an isolated environment with only the minimum credentials and filesystem access required.
  6. Do not expose SIMMER_API_KEY or LIVETENNIS_API_KEY to a publishing process that does not need them.
  7. Enable dependency monitoring and review package ownership, release provenance, and transitive dependency changes before upgrades.
  8. Prefer package-manager options that disable unnecessary lifecycle scripts where compatible with the workflow.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (9)

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding

npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding

npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Content

No source excerpt is available for this finding.

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

Scanner excerpt · README.md (reported line 105)May include surrounding context.

text

The Simmer sync job picks it up within ~1 hour (hourly at :45 UTC) and lists it at
<https://simmer.markets/skills>. No approval step.

> **Validator note.** simmer-sdk's `validate_skill.py` is written for *auto-run
> trader* skills, so it flags a missing `automaton.entrypoint`. That is expected

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill documentation describes capabilities that require environment access and outbound network access, but it does not declare any explicit tool scope or permissions. In agent frameworks, missing scope declarations can lead to overbroad default privileges, making it easier for the skill to access secrets like API keys or make unintended external requests if executed in a permissive runtime.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

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

md
sys.stdout.reconfigure(line_buffering=True)

DEFAULT_BASE_URL = "https://api.livetennisapi.com/api/public/v1"

# Match lifecycle / stop reasons the gate distinguishes.
STATUS_LIVE = "live"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · clawhub.json (reported line 27)May include surrounding context.

json
sys.stdout.reconfigure(line_buffering=True)

DEFAULT_BASE_URL = "https://api.livetennisapi.com/api/public/v1"

# Match lifecycle / stop reasons the gate distinguishes.
STATUS_LIVE = "live"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · tennis_live_gate_skill.py (reported line 87)May include surrounding context.

python
sys.stdout.reconfigure(line_buffering=True)

DEFAULT_BASE_URL = "https://api.livetennisapi.com/api/public/v1"

# Match lifecycle / stop reasons the gate distinguishes.
STATUS_LIVE = "live"

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

The manifest frames the skill as a gate over live tennis state and emphasizes observe-only sizing output, but _run_live_data() additionally fetches up to 25 markets from Simmer/Polymarket using SIMMER_API_KEY and SimmerClient.get_markets(...). While still read-only, market discovery is an extra operational behavior not stated in the manifest description itself.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The stated purpose is to gate tennis entries based on Live Tennis API match state and return a decision/size suggestion without placing orders. Reading SIMMER_API_KEY and instantiating SimmerClient to fetch markets is not necessary to implement the live-state gate itself; it is an ancillary integration capability beyond the core purpose.

Content

No source excerpt is available for this finding.