Back to skill

Security audit

Crypto Price Skill

Security checks for vulnerabilities and agentic risk

Overview

This paid crypto price skill has a coherent purpose, but its billing path is unsafe and under-scoped enough to require review before installation.

Review this skill carefully before installing. It may contact CoinGecko and SkillPay, and its billing implementation exposes an API key, trusts caller-provided billing identities, and may charge before delivering a result. The publisher should rotate the key, remove it from the package, bind charges to authenticated platform identity, validate requests before charging, and fail closed on billing errors.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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)

T09 · Insecure Skill Coding Practices

Error
Location
handler.py:10
Finding
Hardcoded SkillPay API Credential Exposed in Source Code and Documentation<![CDATA[ ## Vulnerability Details **File Location**: `handler.py:10-14`; duplicate disclosure in `SKILL.md:18-21` **Vulnerability Type**: Hardcoded secret and plaintext credential exposure **Risk Level**: High ### Vulnerable Code ```python # SkillPay Configuration SKILLPAY_API_KEY = "sk_93c5ff38cc3e6112623d361fffcc5d1eb1b5844eac9c40043b57c0e08f91430e" PRICE_USDT = "0.001" SKILLPAY_API_URL = "https://skillpay.me/api/v1/billing" ``` The same credential is published in the documentation: ```markdown ## Integration - API Key: sk_93c5ff38cc3e6112623d361fffcc5d1eb1b5844eac9c40043b57c0e08f91430e - Price: 0.001 USDT per call ``` ### Technical Analysis A live-formatted SkillPay API key is embedded directly in the Python source and repeated in public-facing skill documentation. Secrets packaged with source code cannot be kept confidential because every recipient of the package can read and reuse them. The key is submitted both in the JSON body and in the `X-API-Key` header when issuing billing requests. If the billing service recognizes the credential, an attacker can impersonate the skill and invoke any endpoint authorized for that key. ### Attack Path 1. An attacker downloads or otherwise obtains the skill package. 2. The attacker reads `handler.py` or `SKILL.md` and extracts the API key. 3. The attacker constructs requests to the SkillPay API using the stolen key in the expected header or request body. 4. The attacker performs any billing operations permitted by the credential until the key is revoked or additional server-side controls block the requests. ### Impact Assessment Successful exploitation could permit unauthorized API calls, fraudulent or manipulated billing operations, consumption of the account's service quota, and impersonation of the legitimate skill. The exact scope depends on the server-side permissions assigned to the key. The credential must be considered compromised because it has been distributed in plaintext. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions 1. Immediately revoke and rotate the exposed SkillPay credential. 2. Remove the credential from `handler.py`, `SKILL.md`, package archives, and version-control history. 3. Retrieve the key at runtime from a secret manager or a protected environment variable. 4. Abort startup when the required secret is absent rather than falling back to a bundled value. 5. Assign the replacement key only the minimum billing permissions required by this skill. 6. Apply server-side rate limits, endpoint restrictions, transaction limits, and monitoring. 7. Review SkillPay logs for unauthorized use of the disclosed credential. 8. Add secret-scanning checks to development and release pipelines to prevent recurrence. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
handler.py:35
Finding
Caller-Controlled User Identifier Is Trusted for Billing<![CDATA[ ## Vulnerability Details **File Location**: `handler.py:35-46`, with untrusted input accepted at `handler.py:88-92` and `handler.py:129-130` **Vulnerability Type**: Missing authorization and insecure direct object reference in billing identity **Risk Level**: High ### Vulnerable Code ```python def charge_user(user_id: str) -> dict: """Charge user via SkillPay""" try: payload = { "api_key": SKILLPAY_API_KEY, "user_id": user_id, "amount": PRICE_USDT, "skill_id": SKILL_ID, "currency": "USDT", "description": "Crypto price query" } headers = {"Content-Type": "application/json", "X-API-Key": SKILLPAY_API_KEY} response = requests.post(f"{SKILLPAY_API_URL}/charge", json=payload, headers=headers, timeout=10) ``` The handler accepts the identifier directly from its caller: ```python def handle(input_text: str, user_id: str = "default") -> dict: """Main handler""" crypto = extract_crypto(input_text) charge_result = charge_user(user_id) ``` The command-line entry point also allows arbitrary selection of the billing identifier: ```python user_id = sys.argv[2] if len(sys.argv) > 2 else "cli" print(json.dumps(handle(user_input, user_id), indent=2, ensure_ascii=False)) ``` ### Technical Analysis The billing subject is selected using a raw `user_id` parameter supplied by the caller. No authenticated session, signed identity claim, ownership check, or authorization decision binds that identifier to the person initiating the request. Consequently, the code crosses an access-control boundary using an untrusted object identifier. If SkillPay relies on the skill's API key and supplied `user_id` when authorizing a charge, possession or discovery of another user's identifier may be sufficient to target that user. The current file also contains an indentation error and an undefined `SKILL_ID`, which prevent normal billing execution as written. Thes ...[truncated 1162 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the caller-controlled `user_id` parameter from the public skill interface. 2. Derive the billing identity from a server-validated authentication context. 3. Where identity must cross a trust boundary, use short-lived signed claims and validate issuer, audience, subject, expiration, and signature. 4. Verify server-side that the authenticated principal owns or is authorized to bill the target account. 5. Use opaque, non-guessable account identifiers without treating unpredictability as a substitute for authorization. 6. Record authenticated principal, billing subject, skill identifier, transaction identifier, and authorization result in tamper-resistant audit logs. 7. Add negative tests proving that one authenticated user cannot charge another user's account. 8. Correct the syntax and undefined-variable defects only together with the authorization redesign, not as an isolated repair. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
handler.py:35
Finding
Billing Exceptions Fail Open and Enable Payment Bypass<![CDATA[ ## Vulnerability Details **File Location**: `handler.py:35-53`, with the bypass trusted at `handler.py:90-99` and reported at `handler.py:110` **Vulnerability Type**: Fail-open payment authorization **Risk Level**: Medium ### Vulnerable Code ```python def charge_user(user_id: str) -> dict: """Charge user via SkillPay""" try: payload = { "api_key": SKILLPAY_API_KEY, "user_id": user_id, "amount": PRICE_USDT, "skill_id": SKILL_ID, "currency": "USDT", "description": "Crypto price query" } headers = {"Content-Type": "application/json", "X-API-Key": SKILLPAY_API_KEY} response = requests.post(f"{SKILLPAY_API_URL}/charge", json=payload, headers=headers, timeout=10) if response.status_code == 200: return {"success": True, "data": response.json()} return {"success": False, "error": response.text} except Exception as e: return {"success": True, "demo": True, "error": str(e)} ``` The handler permits execution when the exception result includes `demo`: ```python charge_result = charge_user(user_id) if not charge_result.get("success") and not charge_result.get("demo"): return { "payment_required": True, "amount": PRICE_USDT, "skill_id": SKILL_ID, "payment_url": charge_result.get("payment_url", "https://skillpay.me") } ``` It then labels the unpaid operation as a demo: ```python result["payment_status"] = "free_demo" if charge_result.get("demo") else "paid" ``` ### Technical Analysis Every exception raised while constructing or submitting a billing request is converted into `{"success": True, "demo": True}`. The main handler interprets that result as authorization to continue. This conflates an operational failure with an explicitly approved free transaction. Exceptions can result from timeouts, DNS failures, TLS errors, malformed responses, programming errors, or missing configuratio ...[truncated 1169 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Return a failure state for all billing exceptions and deny paid service until payment is confirmed. 2. Remove `success=True` from exception handling. 3. Enable demo behavior only through an explicit, trusted server-side configuration setting. 4. Define and validate all required configuration, including `SKILL_ID`, during application startup. 5. Catch narrow exception classes and distinguish timeouts, transport errors, malformed responses, and application defects. 6. Require a validated transaction identifier and an explicit successful billing status before releasing the paid result. 7. Log billing failures securely and alert on elevated exception or payment-bypass rates. 8. Add tests for network failures, timeouts, invalid JSON, missing configuration, and non-success HTTP responses. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
handler.py:88
Finding
User Is Charged Before Input and Service-Result Validation<![CDATA[ ## Vulnerability Details **File Location**: `handler.py:88-110` **Vulnerability Type**: Incorrect billing transaction order **Risk Level**: Medium ### Vulnerable Code ```python def handle(input_text: str, user_id: str = "default") -> dict: """Main handler""" crypto = extract_crypto(input_text) charge_result = charge_user(user_id) if not charge_result.get("success") and not charge_result.get("demo"): return { "payment_required": True, "amount": PRICE_USDT, "skill_id": SKILL_ID, "payment_url": charge_result.get("payment_url", "https://skillpay.me") } if "top" in input_text.lower() or "list" in input_text.lower() or "all" in input_text.lower(): result = get_top_coins() elif crypto: result = get_crypto_price(crypto) else: return {"error": "Please specify a crypto", "usage": "Example: 'Bitcoin price' or 'Top cryptos'"} result["payment_status"] = "free_demo" if charge_result.get("demo") else "paid" return result ``` ### Technical Analysis `charge_user()` is called before the handler determines whether the request is valid and before CoinGecko successfully returns the requested information. A successful charge may therefore be followed by: - Rejection because no cryptocurrency was specified. - An unsupported cryptocurrency response. - A CoinGecko timeout or network exception. - A non-success response from CoinGecko. - A response that does not contain the requested asset. Charging before validation and fulfillment breaks transaction atomicity: payment can be committed even though the paid service is not delivered. ### Attack Path 1. A caller submits empty, malformed, or unsupported cryptocurrency input, or submits a valid request while CoinGecko is unavailable. 2. The handler calls `charge_user()` before validating the input or obtaining a result. 3. SkillPay confirms and commits the charge. 4. Input validation or the upst ...[truncated 901 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate and normalize the input before initiating any billing operation. 2. Confirm that the requested asset is supported or can be resolved before charging. 3. Prefer an authorization-and-capture workflow: - Authorize the amount before contacting the upstream service. - Obtain and validate the requested price data. - Capture the payment only after successful fulfillment. - Void the authorization if fulfillment fails. 4. If SkillPay does not support authorization and capture, fetch the result first and charge immediately before returning it. 5. Implement automatic refunds or transaction reversal when a post-charge failure occurs. 6. Use idempotency keys so retries cannot create duplicate charges. 7. Persist transaction state and correlate every charge with a successfully delivered result. 8. Add tests for empty input, unsupported assets, upstream timeouts, malformed upstream data, retries, and partial failures. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (9)

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill states it uses the CoinGecko API but does not clearly warn users that their requests will be sent to an external third-party service. This creates a transparency and privacy issue because users may unknowingly transmit their query content to an outside provider, and the risk is heightened here because the skill is also monetized and includes sensitive operational material such as an exposed API key.

Vague Triggers

Medium
Confidence
86% confidence
Finding
The usage example "Crypto prices" is overly broad and could cause the skill to trigger on generic cryptocurrency-related requests rather than clear user intent to invoke this paid third-party skill. In a paid skill context, broad matching increases the risk of unintended invocation, unnecessary external API calls, and unexpected charges to the user.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The skill is presented as a simple crypto price lookup tool, but its main workflow also performs billing through a third-party service before returning results. That hidden monetization behavior is risky because users and integrators may invoke the skill expecting a read-only information query, while the code transmits identifiers and attempts a charge as a side effect.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The code sends user_id, amount, skill identifier, and description to an external billing API without any visible disclosure, consent flow, or minimization in this file. In a skill context, silent transmission of user-linked billing data to a third party raises privacy and compliance concerns, especially when the skill appears informational rather than transactional.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The billing function claims to charge the user, but on any exception it returns success with a demo flag instead of a clear failure. This can mask payment-system errors, create inconsistent billing state, and allow the skill to serve results without a successful charge while falsely signaling that everything is fine.

External Transmission

Medium
Category
Data Exfiltration
Content
"description": "Crypto price query"
        }
            headers = {"Content-Type": "application/json", "X-API-Key": SKILLPAY_API_KEY}
        response = requests.post(f"{SKILLPAY_API_URL}/charge", json=payload, headers=headers, timeout=10)
        if response.status_code == 200:
            return {"success": True, "data": response.json()}
        return {"success": False, "error": response.text}
Confidence
97% confidence
Finding
This line transmits billing data to an external payment service and includes a hard-coded API key in both the payload and header path of the request flow. Combined with the embedded secret in source code, this creates a serious risk of unauthorized billing activity, credential leakage, and abuse of the payment account if the code is exposed.

External Transmission

Medium
Category
Data Exfiltration
Content
"""Get crypto price from CoinGecko"""
    try:
        coin_id = CRYPTO_MAP.get(crypto.lower(), crypto.lower())
        url = f"https://api.coingecko.com/api/v3/simple/price?ids={coin_id}&vs_currencies=usd&include_24hr_change=true"
        response = requests.get(url, timeout=10)
        
        if response.status_code == 200:
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
"""Get crypto price from CoinGecko"""
    try:
        coin_id = CRYPTO_MAP.get(crypto.lower(), crypto.lower())
        url = f"https://api.coingecko.com/api/v3/simple/price?ids={coin_id}&vs_currencies=usd&include_24hr_change=true"
        response = requests.get(url, timeout=10)
        
        if response.status_code == 200:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The skill constructs a CoinGecko request from user-provided crypto text and sends it over the network, but this file provides no visible user warning that input-derived data will be transmitted to a third-party service. The docstring is developer-facing and does not constitute a user-facing disclosure.

Static analysis

Detected: suspicious.exposed_secret_literal

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
handler.py:11

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
SKILL.md:25