Back to skill

Security audit

Odds for sports events

Security checks for vulnerabilities and agentic risk

Overview

The skill is a legitimate Odds-API.io helper, but it can expose the user's API key through dry-run output and through an unrestricted custom API endpoint option.

Install only if you are comfortable reviewing or hardening the helper first. Avoid using --dry-run with a real key, avoid --base-url entirely unless it is a trusted test endpoint with a non-production key, and prefer running it in an environment where ODDS_API_KEY is not broadly exposed.

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/odds_api.py:112
Finding
API Key Disclosure Through Dry-Run Output<![CDATA[ ## Vulnerability Details **File Location**: `scripts/odds_api.py:112-127` **Additional Locations**: `scripts/odds_api.py:153-165`, `scripts/odds_api.py:173-184`, `scripts/odds_api.py:207-222`; `SKILL.md:32` **Vulnerability Type**: Sensitive credential exposure through standard output **Risk Level**: Medium ### Vulnerable Code ```python def command_events(args): api_key = get_api_key(args, required=True) params = { "apiKey": api_key, "sport": args.sport, "league": args.league, "participantId": args.participant_id, "status": args.status, "from": args.from_time, "to": args.to_time, "bookmaker": args.bookmaker, } url = build_url(args.base_url, "/events", params) if args.dry_run: print(url) return 0 ``` The same pattern appears in the authenticated `search`, `odds`, and `matchup` command paths. The corresponding documentation encourages this behavior: ```markdown Prefer `--dry-run` to preview the URL when testing without a key. ``` ### Technical Analysis Authenticated commands retrieve the API key from `--api-key` or the `ODDS_API_KEY` environment variable, insert it into the `apiKey` query parameter, and construct the complete request URL before processing `--dry-run`. When dry-run mode is enabled, the complete URL is printed without redacting the credential. Contrary to the documentation's statement that dry-run can be used when testing without a key, the authenticated commands call `get_api_key(args, required=True)` before checking `args.dry_run`. A real key is therefore still required and then disclosed. Because standard output is frequently captured by CI systems, Agent transcripts, shell session recording, test harnesses, and support tooling, printing the credential creates an avoidable disclosure risk. ### Attack Path 1. A user stores a valid Odds-API.io credential in `ODDS_API_KEY` or supplies it through `--api-key`. 2. The user, an aut ...[truncated 834 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Process dry-run mode without requiring a real API key. 2. Substitute a fixed placeholder such as `REDACTED` when generating preview URLs. 3. Introduce a centralized URL-redaction function and apply it before printing URLs or including them in errors. 4. Never write query-string credentials to stdout, stderr, logs, exceptions, or Agent responses. 5. Update `SKILL.md` to state explicitly that credentials are redacted and are not required for dry-run previews. 6. Add regression tests that assert neither environment-provided nor command-line API keys appear in captured output. Example hardening pattern: ```python def redact_url(url): parsed = urllib.parse.urlsplit(url) params = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True) safe_query = urllib.parse.urlencode( [(key, "REDACTED" if key == "apiKey" else value) for key, value in params] ) return urllib.parse.urlunsplit( (parsed.scheme, parsed.netloc, parsed.path, safe_query, parsed.fragment) ) if args.dry_run: preview_params = dict(params) preview_params["apiKey"] = "REDACTED" print(build_url(args.base_url, "/events", preview_params)) return 0 ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/odds_api.py:253
Finding
Unrestricted Base URL Allows API Key Exfiltration to Arbitrary Hosts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/odds_api.py:253-259` **Related Locations**: `scripts/odds_api.py:14-27`, `scripts/odds_api.py:112-127`, `scripts/odds_api.py:153-165`, `scripts/odds_api.py:173-184`, `scripts/odds_api.py:207-222` **Vulnerability Type**: User-controlled credential destination and insecure endpoint configuration **Risk Level**: High ### Vulnerable Code The destination is exposed as an unrestricted command-line argument: ```python def build_parser(): parser = argparse.ArgumentParser( description="Odds-API.io CLI helper", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("--api-key", help="Odds-API.io API key") parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="API base URL") parser.add_argument("--timeout", type=int, default=20, help="Request timeout in seconds") parser.add_argument("--dry-run", action="store_true", help="Print request URL without calling the API") ``` The supplied URL is used directly: ```python def build_url(base_url, path, params): base = base_url.rstrip("/") clean_params = {k: v for k, v in params.items() if v is not None} query = urllib.parse.urlencode(clean_params) if query: return f"{base}{path}?{query}" return f"{base}{path}" ``` The resulting request is sent without scheme or hostname validation: ```python def request_json(url, timeout): req = urllib.request.Request(url, headers={"Accept": "application/json"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: raw = resp.read() charset = resp.headers.get_content_charset() or "utf-8" text = raw.decode(charset, errors="replace") ``` ### Technical Analysis The Skill's declared purpose requires sending a user-provided API key to the official `https://api.odds-api.io/v3` service. However, `--base-url` permits the caller to replace that endpoint with any URL. Auth ...[truncated 1931 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `--base-url` if endpoint customization is not essential. 2. Otherwise, parse the destination with `urllib.parse.urlsplit` and require: - Scheme exactly equal to `https` - Hostname exactly equal to `api.odds-api.io` - Expected API path prefix, such as `/v3` - No embedded username or password 3. Reject redirects to unapproved hosts, because standard URL handlers may follow redirects and thereby move a credential-bearing request to another destination. 4. Do not attach production credentials when a custom testing endpoint is used. 5. If development overrides are required, place them behind a separate explicit unsafe-development option and require a dedicated non-production credential. 6. Consider moving authentication to a request header if the upstream API supports it. If query-string authentication is mandatory, ensure URLs are never logged or displayed. 7. Add tests covering HTTP URLs, lookalike domains, subdomains, embedded credentials, alternate ports, redirects, and attacker-controlled hosts. Example validation pattern: ```python ALLOWED_HOST = "api.odds-api.io" def validate_base_url(base_url): parsed = urllib.parse.urlsplit(base_url) if parsed.scheme != "https": raise RuntimeError("The API endpoint must use HTTPS.") if parsed.hostname != ALLOWED_HOST: raise RuntimeError("The API endpoint hostname is not permitted.") if parsed.username or parsed.password: raise RuntimeError("Embedded URL credentials are not permitted.") return base_url ``` Validation must occur before combining the endpoint with any sensitive query parameters. ]]>
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
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (11)

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill describes use of environment variables and outbound network access via a CLI helper, but the manifest does not declare any explicit tool scope such as permissions or allowed-tools. This creates a least-privilege gap: an agent/runtime may permit broader capabilities than intended, making secret access and external exfiltration harder to govern or review.

External Transmission

Medium
Category
Data Exfiltration
Content
import urllib.parse
import urllib.request

DEFAULT_BASE_URL = "https://api.odds-api.io/v3"


def build_url(base_url, path, params):
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
import urllib.parse
import urllib.request

DEFAULT_BASE_URL = "https://api.odds-api.io/v3"


def build_url(base_url, path, params):
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Context-Inappropriate Capability

Medium
Confidence
99% confidence
Finding
In the events command, dry-run prints the fully constructed URL, and this URL includes the apiKey query parameter. That exposes the secret to terminal history, logs, screenshots, and any calling framework that captures stdout. In an agent skill context, dry-run output may be surfaced back to users or observability systems, increasing the chance of credential leakage.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
This is the same underlying issue as the SDI-2 finding at the events command: the code prints a credential-bearing query string during dry-run. Query-string secrets are particularly prone to disclosure through logs and copied output, so this is a genuine vulnerability rather than a false positive.

Context-Inappropriate Capability

Medium
Confidence
99% confidence
Finding
In the search command, dry-run outputs a URL containing the apiKey in the query string. Even without making the network call, this leaks the credential locally and to any system that records command output. Because this skill is designed for user-supplied API keys, protecting that value is especially important.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
This search-path finding is valid because the dry-run path displays a URL whose query string contains the API key. Exposing secrets in query strings is dangerous in CLI and agent environments because output is often retained by shells, wrappers, and telemetry systems.

Context-Inappropriate Capability

Medium
Confidence
99% confidence
Finding
In the odds command, dry-run reveals the complete request URL including the apiKey. Secrets embedded in printed URLs are commonly captured by logs, chat transcripts, and monitoring tools, allowing reuse of the key by unauthorized parties. The betting-odds context does not reduce the sensitivity of the API credential.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
This odds-path finding is also a true positive: a secret is embedded in a query string and then printed. That can lead to credential compromise if an attacker gains access to logs, terminal history, or captured agent output.

Context-Inappropriate Capability

Medium
Confidence
99% confidence
Finding
In the matchup command, dry-run prints the search URL containing the apiKey query parameter. This is a real secret-disclosure issue because dry-run is often used for debugging, where stdout is more likely to be copied into tickets, logs, or interactive agent responses. The skill context makes this more dangerous because agent tooling may automatically expose or retain command output.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
This matchup dry-run branch prints a full search URL containing the API key in the query string. In practice, this can leak credentials through debug sessions and automated output collection, making it a real information disclosure issue.

Static analysis

No suspicious patterns detected.