Back to skill

Security audit

PayLock SOL Escrow

Security checks for vulnerabilities and agentic risk

Overview

Review recommended: this escrow skill handles financial actions and tokens, but its custody and security claims do not consistently match the bundled client behavior.

Install only after reviewing the trust model. Treat production v1 as custodial, use only localhost or a trusted HTTPS PayLock endpoint, avoid passing payer/payee tokens on the command line, and verify token scope, expiry, revocation, server custody, and release behavior before using real funds or sensitive deliverables.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/paylock.py:27
Finding
Authentication Tokens Exposed Through Command-Line Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/paylock.py:27-35`, `scripts/paylock.py:63-74` **Vulnerability Type**: Command-line credential exposure **Risk Level**: High ### Vulnerable Code ```python d = sub.add_parser("deliver", help="Deliver work for contract") d.add_argument("--id", required=True) d.add_argument("--delivery-payload", required=True) d.add_argument("--delivery-hash", required=True) d.add_argument("--payee-token", required=True) v = sub.add_parser("verify", help="Verify delivery") v.add_argument("--id", required=True) v.add_argument("--payer-token", required=True) ``` ```python elif args.command == "deliver": result = client.request( "POST", f"/{args.id}/deliver", payload={ "delivery_payload": args.delivery_payload, "delivery_hash": args.delivery_hash, "payee_token": args.payee_token, }, ) elif args.command == "verify": result = client.request( "POST", f"/{args.id}/verify", payload={"payer_token": args.payer_token}, ) ``` ### Technical Analysis The unified CLI requires payer and payee authentication tokens to be supplied as command-line arguments. Command-line arguments are not an appropriate secret-transport mechanism because they may be retained or exposed through: - Shell history files - Process inspection tools such as `ps` - `/proc/<pid>/cmdline` on Linux - Process-monitoring and orchestration systems - Audit, debugging, and terminal-session logs - Wrapper scripts or automation logs This behavior also contradicts `SKILL.md:21-25`, which states that authentication tokens are passed through environment variables and never through CLI arguments. Although the dedicated `deliver_contract.py` and `verify_contract.py` scripts support environment variables, the documented unified `paylock.py` interface does not. ### Attack Path 1. A user invokes the unified CLI with `--payee-token` or `--payer-token`. 2. The token ...[truncated 1073 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove `--payer-token` and `--payee-token` from the unified CLI, or make them deprecated emergency options that emit a prominent warning. - Read the credentials from `PAYLOCK_PAYER_TOKEN` and `PAYLOCK_PAYEE_TOKEN`, consistently with the documentation and dedicated scripts. - For interactive use, support protected input through `getpass.getpass()` or a dedicated file descriptor rather than ordinary stdin or command-line arguments. - Prefer short-lived, contract-scoped tokens with explicit action restrictions. - Implement server-side token expiration, revocation, replay prevention, and rate limiting. - Ensure error messages and API responses never echo submitted tokens. - Update tests and documentation to verify that secrets do not appear in process arguments. A safer implementation pattern is: ```python import os d.add_argument("--payee-token", default=None, help=argparse.SUPPRESS) v.add_argument("--payer-token", default=None, help=argparse.SUPPRESS) payee_token = os.getenv("PAYLOCK_PAYEE_TOKEN") payer_token = os.getenv("PAYLOCK_PAYER_TOKEN") ``` For stronger protection, remove the arguments entirely and fail safely when the required environment variable is absent. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/paylock_api.py:14
Finding
Sensitive Financial Requests May Be Transmitted over Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `scripts/paylock_api.py:14-43`, `SKILL.md:13-19` **Vulnerability Type**: Missing transport-security enforcement **Risk Level**: High ### Vulnerable Code ```python DEFAULT_API = "http://localhost:8767" class PayLockClient: def __init__(self, base_url: Optional[str] = None) -> None: self.base_url = ( base_url or os.getenv("PAYLOCK_API_BASE") or DEFAULT_API ).rstrip("/") def request( self, method: str, path: str, payload: Optional[Dict[str, Any]] = None, timeout: int = 30, ) -> Dict[str, Any]: url = f"{self.base_url}/{path.lstrip('/')}" data = None headers = {"Accept": "application/json"} if payload is not None: data = json.dumps(payload).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request( url=url, method=method.upper(), data=data, headers=headers, ) try: with urllib.request.urlopen(req, timeout=timeout) as resp: ``` The corresponding documentation permits an operator-provided endpoint without requiring HTTPS: ```bash export PAYLOCK_API_BASE="http://localhost:8767" ``` ```text Agents running their own PayLock instance use localhost. For hosted PayLock, set the URL provided by your PayLock operator. ``` ### Technical Analysis Using plaintext HTTP for a loopback-only development service can be acceptable under a defined local trust model. However, the client accepts arbitrary remote HTTP endpoints through both `PAYLOCK_API_BASE` and the `--api` argument without validation or warning. The request body can contain: - Payer and payee authentication tokens - Wallet addresses - Transaction hashes - Contract descriptions and participant identifiers - Delivery payloads and hashes - Verification requests that may cause funds to be released When a rem ...[truncated 1811 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require HTTPS for every non-loopback API endpoint. - Permit plaintext HTTP only when the parsed hostname is a validated loopback address such as `localhost`, `127.0.0.1`, or `::1`. - Do not rely on hostname string prefixes; parse the URL and validate the resolved destination carefully. - Reject URLs containing unexpected credentials, fragments, or unsupported schemes. - Reject insecure redirects and prevent HTTPS-to-HTTP downgrade redirects. - Consider restricting redirects entirely for requests carrying authentication tokens. - Use normal certificate and hostname verification for hosted services. - For high-value deployments, consider certificate or public-key pinning with a documented rotation process. - Add a conspicuous warning if an explicit development override permits insecure transport. - Ensure tokens are short-lived and replay-resistant so interception has limited value. For example, validate the endpoint during initialization: ```python import ipaddress import urllib.parse parsed = urllib.parse.urlparse(self.base_url) host = parsed.hostname is_loopback = host == "localhost" if host: try: is_loopback = is_loopback or ipaddress.ip_address(host).is_loopback except ValueError: pass if parsed.scheme != "https" and not ( parsed.scheme == "http" and is_loopback ): raise ValueError("HTTPS is required for non-loopback PayLock endpoints") ``` Redirect behavior should also be explicitly constrained so sensitive requests cannot be redirected to another origin or downgraded to HTTP. ]]>

other

Warning
Location
SKILL.md:1
Finding
Security and Custody Claims Conflict with the Bundled Client Behavior<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:1-9`, `SKILL.md:21-25`, `SKILL.md:98-109`, `scripts/paylock.py:27-35`, `scripts/paylock_api.py:34-40` **Vulnerability Type**: Misleading security and trust-boundary documentation **Risk Level**: Medium ### Conflicting Documentation and Implementation The skill is presented as non-custodial: ```yaml --- name: paylock description: Non-custodial SOL escrow for AI agent deals. Create, fund, deliver, verify contracts from chat. No browser needed. version: 1.1.0 --- ``` ```text Non-custodial escrow infrastructure. Your agent handles deals from chat — no websites, no manual steps. ``` The architecture section later identifies the production service as custodial: ```text ## Architecture - **v1 (Production):** REST API, custodial escrow, SOL transfers - **v2 (Devnet):** Solana Anchor program, non-custodial PDA escrow ``` The documentation also states: ```text **Authentication:** Tokens are passed via environment variables, never CLI arguments: ``` However, the unified CLI requires token arguments: ```python d.add_argument("--payee-token", required=True) v = sub.add_parser("verify", help="Verify delivery") v.add_argument("--id", required=True) v.add_argument("--payer-token", required=True) ``` The documentation claims HMAC authentication: ```text - **HMAC authentication:** All sensitive endpoints authenticated via HMAC tokens ``` The bundled client does not construct an HMAC signature. It serializes a raw token into the request body: ```python if payload is not None: data = json.dumps(payload).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request( url=url, method=method.upper(), data=data, headers=headers, ) ``` ### Technical Analysis Custodial and non-custodial escrow have materially different trust and failure models. In a custodial REST architecture, users depend on the service operator and server implementation to safeguard a ...[truncated 2158 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Change the headline and summary to state clearly that the production v1 workflow is custodial. - Reserve the term “non-custodial” specifically for the v2 on-chain implementation and identify it as devnet-only until production deployment is independently verifiable. - Document the complete trust boundary, including: - Who controls escrow keys - Whether the operator can move or freeze funds - Recovery behavior during service outages - Token scope, lifetime, revocation, and replay controls - Which guarantees are client-side, server-side, or on-chain - Replace the “HMAC authentication” statement unless the protocol actually computes and verifies a message authentication code. - If HMAC signing is intended, sign a canonical representation of the method, path, body digest, timestamp, and unique nonce. - Keep shared secrets out of command-line arguments and send authentication data through a protected header over HTTPS. - Clearly distinguish implemented, externally verifiable controls from roadmap features. - Provide links to the exact audited server and on-chain source versions corresponding to the published skill version. - Reconcile the version mismatch between `SKILL.md` version `1.1.0` and `_meta.json` version `1.0.0` to improve release traceability. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (5)

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
The manifest describes the skill as non-custodial, but later documentation states that v1 production is custodial escrow. This is security-relevant misrepresentation because users and agent operators may make materially different trust and fund-risk decisions based on whether the system ever takes custody of assets.

Intent-Code Divergence

High
Confidence
98% confidence
Finding
The documentation repeatedly markets the system as non-custodial while later disclosing that the production architecture is custodial. In a financial escrow skill, that inconsistency is especially dangerous because it can mislead operators into exposing funds to a custody model they did not intend to trust.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill exposes capabilities that rely on environment variables and network access, but the manifest does not declare any tool scope or permissions. In an agent ecosystem, this weakens the trust boundary because a user or platform cannot accurately understand what resources the skill expects to access before execution.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The CLI forwards highly sensitive values such as payer/payee tokens and delivery payloads directly to a remote API, yet this file provides no warning, consent prompt, or disclosure to the operator that secrets and potentially proprietary work product are being transmitted off-host. In the context of an AI-agent escrow skill that advertises 'No browser needed' and is likely to be invoked non-interactively, this increases the risk of accidental secret exfiltration or unintended disclosure of sensitive deliverables to a configurable endpoint.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The client performs network requests with `urlopen`, and when `payload` is provided it serializes and sends JSON to the configured API endpoint. Although the module docstring states it is an API client, there is no user-facing warning, confirmation, or inline disclosure near the request path about transmitting data over the network.