Back to skill

Security audit

Leetify

Security checks for vulnerabilities and agentic risk

Overview

This CS2 stats skill mostly does what it says, but its demo-analysis code lets API-supplied data control downloads and cache paths, creating local file and network/resource risks.

Install only if you are comfortable running it in a constrained environment with a Leetify API key and local Steam ID mappings. Prefer a revised version that pins dependencies, sanitizes cache filenames, confines cache reads and writes, allowlists replay hosts, and enforces download, decompression, parsing, and HTTP timeouts.

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

T08 · Insecure Dependencies

Warning
Location
SKILL.md:10
Finding

Unpinned Third-Party Dependencies Create a Supply-Chain Risk

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/analyze_last_demo.py:1075
Finding

Remote Match Identifier Can Escape the Cache Directory

Content
View full analysis
str: os.makedirs(MATCHES_DIR, exist_ok=True) return os.path.join(MATCHES_DIR, f"{match_id}.txt") def load_cache(match_id: str) -> str | None: path = get_cache_path(match_id) if os.path.isfile(path): with open(path, "r") as f: return f.read() return None def save_cache(match_id: str, log: str): path = get_cache_path(match_id) with open(path, "w") as f: f.write(log) ``` The cache identifier originates in the remotely supplied match object: ```python match = matches[args.match_index] match_id = match.get("id", "unknown") replay_url = match.get('replay_url') log = None if not args.no_cache: log = load_cache(match_id) if log is None: if not replay_url: sys.exit(1) with tempfile.TemporaryDirectory(prefix="cs2demo_") as tmp_dir: demo_path = download_demo(replay_url, tmp_dir) log = parse_demo_to_log(demo_path, match) save_cache(match_id, log) ``` ### Technical Analysis `match_id` is obtained from the Leetify API response and is interpolated directly into a filesystem path. The code does not enforce an identifier format, remove path separators, canonicalize the resulting path, or verify that it remains under `MATCHES_DIR`. A value such as `../../target` would cause the generated path to resolve outside the intended cache directory as `../../target.txt`. The fixed `.txt` suffix limits the set of target filenames but does not prevent unauthorized reads or writes. On the read path, the contents of an existing traversed file are treated as a cached match log and printed to standard output. On the write path, the parsed log is written to the traversed location. Exploitation requires control o ...[truncated 1237 chars]
Remediation
View remediation
str: cache_root = Path(MATCHES_DIR).resolve() cache_root.mkdir(parents=True, exist_ok=True) cache_name = hashlib.sha256(str(match_id).encode("utf-8")).hexdigest() + ".txt" return str(cache_root / cache_name) ``` 3. Alternatively, enforce a strict allowlist such as `[A-Za-z0-9_-]+` with a reasonable maximum length. 4. Resolve the final path and verify that it is a descendant of the resolved cache root before every read or write. 5. Open cache files with restrictive permissions. 6. Where practical, use atomic writes and reject symbolic links to reduce race and link-based attacks. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/analyze_last_demo.py:116
Finding

API-Controlled Replay URL Is Downloaded Without Destination or Resource Restrictions

Content
View full analysis
str: filename = os.path.basename(replay_url).split('?')[0] local_path = os.path.join(tmp_dir, filename) print(f"⬇️ Скачиваю демку: {replay_url}", file=sys.stderr) urllib.request.urlretrieve(replay_url, local_path) if local_path.endswith('.bz2'): print("📦 Распаковка .bz2...", file=sys.stderr) subprocess.run(['bunzip2', '-f', local_path], check=True) local_path = local_path[:-4] elif local_path.endswith('.gz'): print("📦 Распаковка .gz...", file=sys.stderr) subprocess.run(['gunzip', '-f', local_path], check=True) local_path = local_path[:-3] if not os.path.isfile(local_path): print(f"❌ Файл не найден после распаковки: {local_path}", file=sys.stderr) sys.exit(1) size_mb = os.path.getsize(local_path) / (1024 * 1024) print(f"✅ Демка готова: {size_mb:.1f} MB", file=sys.stderr) return local_path ``` The URL is taken directly from the match API response: ```python match_id = match.get("id", "unknown") replay_url = match.get('replay_url') if log is None: if not replay_url: print("❌ Нет ссылки на демку", file=sys.stderr) sys.exit(1) with tempfile.TemporaryDirectory(prefix="cs2demo_") as tmp_dir: demo_path = download_demo(replay_url, tmp_dir) print("🔍 Парсинг демки...", file=sys.stderr) log = parse_demo_to_log(demo_path, match) ``` ### Technical Analysis The remote API fully controls `replay_url`. Before retrieval, the code does not: - Require the HTTPS scheme. - Restrict the hostname to approved replay providers. - Validate redirect destinations. - Block loopback, link-local, private, or cloud-metadata addresses. - Im ...[truncated 1984 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/profile_with_stats.py:15
Finding

Authenticated API Requests Have No Timeout

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (27)

Tainted flow: 'headers' from os.environ.get (line 18, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/match_details.py (reported line 21)May include surrounding context.

python
headers = {"Authorization": f"Bearer {API_KEY}"}
    url = f"{API_BASE}{endpoint}"
    try:
        response = requests.get(url, params=params, headers=headers)
        if response.status_code == 200:
            return {"success": True, "data": response.json()}
        else:

Tainted flow: 'headers' from os.environ.get (line 16, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/profile_with_stats.py (reported line 19)May include surrounding context.

python
headers = {"Authorization": f"Bearer {API_KEY}"}
    url = f"{API_BASE}{endpoint}"
    try:
        response = requests.get(url, params=params, headers=headers)
        if response.status_code == 200:
            return {"success": True, "data": response.json()}
        else:

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

This is a clear description-behavior mismatch. The declared purpose centers on querying Leetify for CS2 analytics features, but the supplied code is a local storage utility for mapping usernames to Steam IDs. Its primary purpose is identity/mapping persistence using a JSON file, not analytics or API-backed stat retrieval. While Steam ID mapping could be a supporting component of a broader CS2 stats skill, this code chunk itself does not implement any of the declared user-facing capabilities and instead introduces a separate undeclared capability: storing and managing user identifiers locally.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill advertises executable capabilities including environment access, shell execution, network access, and file read/write, but does not declare any explicit tool scope such as permissions or allowed-tools. This weakens containment and reviewability because an agent may invoke broader capabilities than a user or platform expects, increasing the chance of unintended command execution, data access, or exfiltration if the skill or its companion scripts are modified or abused.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/analyze_last_demo.py (reported line 94)May include surrounding context.

python
# ==========================================

def get_steam_id(username: str) -> str:
    result = subprocess.run(
        [sys.executable, STEAM_IDS_SCRIPT, "get", "--username", username],
        capture_output=True, text=True
    )

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

Multiple user-visible status and error messages are written only in Russian, such as the message on this line. This imposes a specific language choice on all users without offering a locale selection or documenting a justified region-specific constraint.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

This API error message is presented only in Russian. Under the language/locale policy, user-facing text should not force a specific language unless the user opted in or the regional limitation is explicitly documented.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The download and extraction progress messages in this block are user-facing and exclusively Russian. Because the script provides no language selection and no documented locale restriction, these messages violate the language/locale policy.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/analyze_last_demo.py (reported line 125)May include surrounding context.

python
if local_path.endswith('.bz2'):
        print("📦 Распаковка .bz2...", file=sys.stderr)
        subprocess.run(['bunzip2', '-f', local_path], check=True)
        local_path = local_path[:-4]
    elif local_path.endswith('.gz'):
        print("📦 Распаковка .gz...", file=sys.stderr)

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/analyze_last_demo.py (reported line 129)May include surrounding context.

python
local_path = local_path[:-4]
    elif local_path.endswith('.gz'):
        print("📦 Распаковка .gz...", file=sys.stderr)
        subprocess.run(['gunzip', '-f', local_path], check=True)
        local_path = local_path[:-3]

    if not os.path.isfile(local_path):

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

This missing-match error is shown only in Russian, imposing a fixed locale on all users. The file does not provide an alternative language or indicate that the tool is restricted to Russian-speaking users.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

These cache/download/parse/save status messages are visible to the user and written only in Russian. Since the script accepts general CLI input and does not document a Russian-only audience, this is a language policy violation.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The final completion message is user-facing and exclusively Russian. This forces a language choice on every user without offering opt-in, fallback, or documented justification.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The script emits multiple user-facing strings in Russian, such as headings and labels for profile statistics, without offering any language selection or indicating that the skill is intentionally restricted to Russian-speaking users. This is a natural-language locale constraint that can violate language/locale policy when imposed without opt-in.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

The code makes authenticated HTTP requests to an external Leetify API using a Steam identifier and then processes match data, but there is no confirmation prompt or user-facing notice that account-related data will be sent to a third-party service. For code files, outbound network transmission of user or system data should have some visible disclosure unless the warning is documented elsewhere or the behavior is clearly disclosed as part of the skill description, which is not shown in this file.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/match_details.py (reported line 88)May include surrounding context.

python
script_dir = os.path.dirname(os.path.abspath(__file__))
    try:
        # Resolve username to Steam ID
        res = subprocess.check_output(
            ['python3', os.path.join(script_dir, 'steam_ids.py'), 'get', '--username', args.username],
            text=True
        ).strip()

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest describes a skill for retrieving CS2 statistics, match analysis, and gameplay insights from the Leetify API. This file adds process-spawning behavior by invoking another script via subprocess, which is a broader execution capability than the stated API-query purpose requires.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The script sends user-supplied Steam IDs and retrieves profile/match data from an external third-party service without any user-facing disclosure or consent mechanism. In an agent skill context, this can cause unexpected external data transfer and privacy issues because users may not realize their identifiers and query context are being shared with Leetify.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The manifest describes a skill for retrieving CS2 player statistics, match analysis, gameplay insights, comparison, and season stats from the Leetify API. This file instead implements persistent local storage and management of Telegram/username-to-Steam-ID mappings, including save, list, and delete operations, which is a separate identity-management capability not described in the manifest.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The code persists Telegram-linked identity data, including username, optional display name, and optional telegram_id, to a local JSON file without any apparent minimization, retention controls, or access protections. For a Leetify stats skill, storing cross-platform identity linkage is more sensitive than needed and creates unnecessary privacy risk if the file is exposed, reused, or retained indefinitely.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

This code emits the resolved Steam ID directly to stderr, which exposes a user-linked identifier during execution. Although it is logged visibly, the script docstring and argument help do not disclose that a personal identifier will be displayed and used as part of the analysis flow.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The manifest describes a skill for CS2 player statistics, match analysis, and gameplay insights from the Leetify API, but this script's documented and implemented entry point is comparing players by Telegram usernames and resolving them through a local steam_ids mapping first. That introduces an identity-lookup behavior not reflected in the manifest's stated scope.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The error messages at L23 and L28 are hard-coded in Russian ("Steam ID не найден...") with no indication that the user can choose another language. This can violate a language/locale policy when skills are expected not to force a specific language without user opt-in.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
97% confidence
Finding

The error message is hard-coded in Russian ("Steam ID не найден") with no indication that the skill is region-specific or that users can choose their language. This creates a natural-language locale policy issue because the script imposes a specific language on all users.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The script reads LEETIFY_API_KEY from the environment and uses it for authenticated API access, but there is no comment, docstring, or user-facing notice explaining that a credential is required and will be used. Under the code-file criteria, access to sensitive environment variables should have some form of disclosure unless clearly documented elsewhere, which is not evident in this file.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.