Back to skill

Security audit

XH Agents x402

Security checks for vulnerabilities and agentic risk

Overview

This skill clearly discloses paid x402 use, but it handles a wallet private key and can sign payments to arbitrary endpoints with weak payment validation.

Use only a dedicated low-balance wallet, never a production wallet or broad private key. Quote first, manually verify the URL, payTo address, network, and asset, and avoid sending sensitive documents or account material through the listed routes. Treat arbitrary payment URLs as unsafe unless independently trusted.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/xh_pay.py:104
Finding

Payment Ceiling Bypass Through Unvalidated Payment Asset

Content
View full analysis

Vulnerability Details

File Location: scripts/xh_pay.py, lines 104–108 and 173–183
Vulnerability Type: Unvalidated payment asset and incorrect denomination assumptions
Risk Level: High

Vulnerable code:

python
amt = a.get("maxAmountRequired") or a.get("amount")
try:
    out["price_usdc"] = round(int(amt) / 1e6, 6) if amt else None
except Exception:  # noqa: BLE001
    out["price_usdc"] = None
python
info = describe(required)
if info["price_usdc"] is None or info["price_usdc"] > max_price:
    return {"ok": False, "payer": acct.address,
            "error": f"harga {info['price_usdc']} di atas plafon {max_price} — dibatalkan", "quote": info}

inner = x402ClientSync()
register_exact_evm_client(inner, signer, [NETWORK])
hc = x402HTTPClientSync(inner)
payload = hc.create_payment_payload(required)
sig = encode_payment_signature_header(payload)

Technical Analysis

The pay workflow obtains its payment requirements from a remote endpoint. The endpoint therefore controls fields including asset, amount, network, and payTo.

The ceiling check interprets every challenge amount as a six-decimal USDC quantity:

python
int(amt) / 1e6

However, the code does not require the challenge's asset field to equal the documented Base USDC contract stored in USDC_BASE. After applying the ceiling under this unverified assumption, it passes the original challenge to create_payment_payload, which prepares the wallet authorization.

Consequently, the value checked against --max may not represent the asset or denomination that the wallet is asked to authorize. Restricting the registered client to Base does not correct this issue because it constrains the chain, not the token contract.

The code does correctly reject missing prices and genuine six-decimal amounts above the configured ceiling. Those controls do not prevent a remote seller from selecting another compatib ...[truncated 1534 chars]

Remediation
View remediation

Remediation Suggestions

  1. Before constructing a payment payload, require a case-insensitive exact match between the challenge's asset address and USDC_BASE.
  2. Require the challenge network to equal eip155:8453 and validate the expected payment scheme explicitly.
  3. Compare the requested amount directly in USDC atomic units rather than converting an unverified asset using a hard-coded decimal assumption. For example, convert the configured ceiling to an integer number of six-decimal USDC units and reject amounts above it.
  4. Reject malformed, negative, fractional, ambiguous, or unexpectedly encoded amount values.
  5. Where the Skill is used specifically with XH Agents, validate payTo against the documented recipient. For payments to other sellers, display the recipient and require explicit approval before signing.
  6. Revalidate all security-relevant challenge fields immediately before signing so that the validated object and the object supplied to create_payment_payload cannot diverge.
  7. Add tests covering alternate token contracts, tokens with different decimals, wrong networks, unexpected schemes, oversized atomic amounts, and recipient substitution.
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
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (10)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The skill is presented as a data-intel capability, but its documented behavior includes loading a private key, checking balances, signing EIP-3009 authorizations, and sending paid requests as a generic x402 payer. That mismatch can mislead operators or orchestrators into granting it wallet/signing access under the assumption it only retrieves analytics, which materially increases the risk of unauthorized spending or secret exposure.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill declares no explicit tool/permission scope even though its documented behavior requires network access and access to a payer key from environment or file. In an agent environment, this missing boundary makes it easier for the skill to be invoked with broader capabilities than intended, increasing the chance of unintended outbound calls or use of sensitive wallet material.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The referenced routes materially exceed the declared skill scope by including document extraction, social/company enrichment, web search, chat, and operational how-to endpoints. In an agent setting, this creates a capability-confusion risk where a caller may invoke sensitive or higher-impact functions under a narrower trust assumption, increasing the chance of unintended data handling or unsafe actions.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

These routes describe connecting a VPS to a user's Google Drive and automating YouTube upload workflows, which can involve OAuth tokens, account access, cloud files, and automated external actions. Presenting them without prominent privacy, consent, and system-impact warnings is dangerous because an agent may facilitate credentialed access or destructive automation without ensuring the user understands what data and permissions are being exposed.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

This code file contains natural-language strings such as the module docstring and CLI help text in a single language, which can force a specific locale on users. The policy allows fixed locale behavior only when the skill offers opt-in or clearly documents a justified regional constraint, neither of which appears here.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

This code can spend funds from a local private key to satisfy x402 payment challenges, which is a materially sensitive capability beyond passive trust-scoring or token-intel behavior. Even with a price cap, the script authorizes real on-chain payments to remote services, so misuse, misconfiguration, or deceptive endpoint selection can cause unintended financial loss.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script accepts an arbitrary user-supplied URL and will perform a signed x402 payment flow against it after reading a challenge, effectively turning the skill into a generic payment client for untrusted endpoints. In the context of an agent skill, this is dangerous because an upstream prompt, tool caller, or compromised workflow could direct funds to attacker-controlled services while appearing consistent with the tool's nominal trust-scoring purpose.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

Document extraction and web retrieval inherently send user-supplied documents, URLs, or page content to an external paid service. Without a clear disclosure, users or downstream agents may unintentionally transmit sensitive files or proprietary content off-platform, causing privacy and confidentiality issues.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/xh_pay.py (reported line 78)May include surrounding context.

python
return default
    if isinstance(obj, dict):
        return obj.get(key, default)
    return getattr(obj, key, default)


def _dict(obj) -> dict:

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/xh_pay.py (reported line 86)May include surrounding context.

python
if obj is None:
        return {}
    for attr in ("model_dump", "dict"):
        fn = getattr(obj, attr, None)
        if callable(fn):
            try:
                return fn()

Static analysis

No suspicious patterns detected.