Back to skill

Security audit

Cricket Live Score

Security checks for vulnerabilities and agentic risk

Overview

The skill is a disclosed cricket-score-to-Telegram notifier, with some operational and URL-validation caveats but no evidence of hidden exfiltration or destructive behavior.

Before installing, confirm you are comfortable giving the skill a Telegram bot token and sending score text, chat IDs, and optional generated audio through Telegram. Use an explicit least-privilege bot token when possible, provide a real cricbuzz.com live-score URL, choose a reasonable polling interval, and avoid enabling voice memos if third-party text-to-speech processing is not acceptable.

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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/cricket-live.py:102
Finding
Weak Cricbuzz Hostname Validation Permits Server-Side Request Forgery## Vulnerability Details **File Location**: `scripts/cricket-live.py`, lines 102-115 **Vulnerability Type**: Server-Side Request Forgery caused by incomplete hostname and redirect validation **Risk Level**: Medium ```python def validate_url(url): """Ensure the URL is a valid Cricbuzz live score page to prevent SSRF.""" parsed = urllib.parse.urlparse(url) if parsed.scheme not in ('http', 'https'): print(f"Error: URL must use http or https (got {parsed.scheme})", file=sys.stderr) sys.exit(1) if not parsed.hostname or not parsed.hostname.endswith('cricbuzz.com'): print(f"Error: URL must be a cricbuzz.com domain (got {parsed.hostname})", file=sys.stderr) sys.exit(1) return url def fetch_raw(match_url): req = urllib.request.Request(match_url, headers={"User-Agent": "Mozilla/5.0"}) return urllib.request.urlopen(req, timeout=15).read().decode("utf-8", errors="ignore") ``` ### Technical Analysis The hostname check uses `parsed.hostname.endswith('cricbuzz.com')` without requiring a DNS label boundary. Consequently, unrelated attacker-controlled domains such as `evilcricbuzz.com` satisfy the validation rule. The implementation also does not validate the resolved IP address. An accepted hostname could resolve or rebind to a loopback, private, link-local, or otherwise restricted address. In addition, `urllib.request.urlopen()` follows HTTP redirects by default, but redirect destinations are not passed through `validate_url()`. An initially permitted destination could therefore redirect the request to an internal service. Although the function claims to prevent SSRF, these weaknesses allow the application to issue requests beyond the Cricbuzz hosts required for its declared functionality. ### Attack Path 1. An attacker or untrusted caller supplies a URL such as `https://evilcricbuzz.com/live-score`. 2. `urlparse()` extracts `evilcricbuzz.com` as the hostname ...[truncated 1383 chars]
Remediation
## Remediation Suggestions 1. Permit only exact Cricbuzz domains with a DNS label boundary: ```python hostname = (parsed.hostname or "").rstrip(".").lower() if hostname != "cricbuzz.com" and not hostname.endswith(".cricbuzz.com"): raise ValueError("Only Cricbuzz hosts are permitted") ``` 2. Prefer a narrow allowlist of the exact hostnames genuinely required by the Skill rather than permitting every subdomain. 3. Require HTTPS and reject embedded credentials, fragments, unexpected ports, and malformed hostnames. 4. Disable automatic redirects or implement a redirect handler that revalidates every destination before following it. 5. Resolve all destination addresses before connecting and reject loopback, private, link-local, reserved, multicast, and unspecified IP ranges for both IPv4 and IPv6. 6. Protect against DNS rebinding by ensuring the validated address is the address used for the connection, or enforce equivalent egress restrictions outside the application. 7. Apply network-level egress controls so this process can connect only to approved Cricbuzz, Telegram, and text-to-speech endpoints.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/cricket-live.py:448
Finding
Unbounded Update Interval Allows Excessive Network Polling## Vulnerability Details **File Location**: `scripts/cricket-live.py`, lines 448-456 and 529 **Vulnerability Type**: Missing numeric input bounds and resource-consumption control **Risk Level**: Low ```python parser.add_argument("--interval", type=int, default=300, help="Update interval in seconds (default: 300)") parser.add_argument("--chat-id", required=True, help="Telegram chat ID") parser.add_argument("--bot-token", default=None, help="Telegram bot token (falls back to OpenClaw config if not provided)") parser.add_argument("--voice", action="store_true", default=False, help="Send voice memo with each update") args = parser.parse_args() bot_token = args.bot_token or os.environ.get("TELEGRAM_BOT_TOKEN") or load_bot_token() match_url = validate_url(args.url) interval = args.interval chat_id = args.chat_id voice_enabled = args.voice ``` ```python time.sleep(interval) ``` ### Technical Analysis The `--interval` argument accepts any integer without lower or upper bounds. Supplying zero causes `time.sleep(0)` to return immediately, so the infinite loop repeatedly downloads the score page without an intentional delay. A negative value causes `time.sleep()` to raise an exception and terminate the program after the first iteration. The absence of a minimum interval permits unnecessary CPU activity and excessive outbound traffic to Cricbuzz. Depending on score changes and voice settings, it may also contribute to repeated downstream Telegram or text-to-speech traffic, although score deduplication reduces that secondary effect. ### Attack Path 1. A caller starts the Skill with `--interval 0`. 2. The script enters its indefinite polling loop. 3. Each iteration invokes `fetch_raw(match_url)`. 4. `time.sleep(0)` provides no meaningful pause. 5. The process continuously sends network requests until it is stopped, the match is detected as complete, or another error interrupts execution. ### Impact Asses ...[truncated 521 chars]
Remediation
## Remediation Suggestions 1. Reject intervals below a safe operational minimum, such as 30 or 60 seconds. 2. Enforce the constraint during argument parsing: ```python def valid_interval(value): interval = int(value) if interval < 30: raise argparse.ArgumentTypeError("interval must be at least 30 seconds") return interval parser.add_argument("--interval", type=valid_interval, default=300) ``` 3. Consider defining a reasonable maximum to prevent accidental configurations that effectively disable useful updates. 4. Add exponential backoff and jitter after network failures or rate-limit responses. 5. Document the permitted interval range in `SKILL.md` and ensure callers cannot bypass it through alternative invocation paths.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • 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 (11)

Tainted flow: 'req' from os.environ.get (line 82, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
data = json.dumps({"chat_id": chat_id, "text": text, "parse_mode": "Markdown"}).encode()
    req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
    try:
        urllib.request.urlopen(req, timeout=10)
    except Exception:
        data = json.dumps({"chat_id": chat_id, "text": text}).encode()
        req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 53, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
except Exception:
        data = json.dumps({"chat_id": chat_id, "text": text}).encode()
        req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
        urllib.request.urlopen(req, timeout=10)


def send_voice(text, bot_token, chat_id):
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 53, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
url = f"https://api.telegram.org/bot{bot_token}/sendVoice"
        req = urllib.request.Request(url, data=bytes(body),
            headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
        urllib.request.urlopen(req, timeout=15)
        os.unlink(tmp.name)
    except Exception as e:
        print(f"Voice error: {e}", file=sys.stderr)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 53, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
def fetch_raw(match_url):
    req = urllib.request.Request(match_url, headers={"User-Agent": "Mozilla/5.0"})
    return urllib.request.urlopen(req, timeout=15).read().decode("utf-8", errors="ignore")


def extract_match_info(raw):
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The declared purpose understates important behaviors: the skill can source Telegram credentials from local config or environment and uses external services/libraries for voice generation and message delivery. This mismatch is dangerous because users may authorize a seemingly simple score-following skill without realizing it accesses secrets and transmits content to third-party services.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill documents network access and credential consumption from environment/config, but it does not declare any explicit tool scope or permissions boundary. In an agent ecosystem, missing scope declarations reduce transparency and can allow a skill to use broader capabilities than a user expects, especially when it can read local secrets and send data over the network.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The introductory description emphasizes convenience but does not prominently warn that match data and optional generated voice content will be transmitted to Telegram via the Bot API. In privacy- or enterprise-sensitive environments, undisclosed outbound transmission can cause unintended data sharing and weakens informed consent.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The skill lists multiple credential sources, including environment variables and a local config file, without a strong warning about secret handling. This increases the risk of accidental secret exposure, misuse of unrelated bot tokens present on the host, or user surprise that local credentials will be consumed automatically.

External Transmission

Medium
Category
Data Exfiltration
Content
# ─── Telegram helpers ─────────────────────────────────────────────────────────

def send_telegram(text, bot_token, chat_id):
    url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    data = json.dumps({"chat_id": chat_id, "text": text, "parse_mode": "Markdown"}).encode()
    req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
    try:
Confidence
60% 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
# ─── Telegram helpers ─────────────────────────────────────────────────────────

def send_telegram(text, bot_token, chat_id):
    url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    data = json.dumps({"chat_id": chat_id, "text": text, "parse_mode": "Markdown"}).encode()
    req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
    try:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The script sets `gTTS(clean, lang='en')`, forcing spoken output to English regardless of user preference. This is a natural-language locale policy issue because the file does not offer a language choice or explain why English is required.