Back to skill

Security audit

Polymarket 48h Cross Asset Sync Trader

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed automated trading skill, but its live-trading safeguards and documented risk limits do not fully match the code.

Review before installing. Use paper mode first, use a least-privilege SIMMER_API_KEY with tight account and spending limits, and do not enable live trading until the live-mode cache issue and the advertised volume/open-position controls are fixed or independently verified.

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)

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:47
Finding
Cached Live Client Can Bypass Paper-Trading Mode<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:47-67` **Vulnerability Type**: Unsafe global client state and execution-mode confusion **Risk Level**: High ### Vulnerable Code ```python _client: SimmerClient | None = None def get_client(live: bool = False) -> SimmerClient: global _client, MAX_POSITION, MIN_VOLUME, MAX_SPREAD, MIN_DAYS, MAX_POSITIONS global YES_THRESHOLD, NO_THRESHOLD, MIN_TRADE, MIN_DIVERGENCE if _client is None: venue = "polymarket" if live else "sim" _client = SimmerClient( api_key=os.environ["SIMMER_API_KEY"], venue=venue, ) if live: _client.live = True try: _client.apply_skill_config(SKILL_SLUG) except AttributeError: pass # apply_skill_config only available in Simmer runtime ``` The cached client is subsequently used for order submission: ```python r = client.trade( market_id=market_id, side=side, amount=size, source=TRADE_SOURCE, skill_slug=SKILL_SLUG, reasoning=reasoning, ) ``` ### Technical Analysis The process-global `_client` is initialized only once. The requested `live` mode is considered only while `_client is None`; subsequent calls reuse the existing client without verifying its venue or live-trading state. If `get_client(live=True)` or `run(live=True)` executes first, `_client` is configured for the real Polymarket venue and explicitly marked as live. A later `run(live=False)` in the same Python process reuses that live client even though the caller requested paper trading. This violates the documented safe-by-default guarantee. The issue is particularly relevant in managed runtimes, test harnesses, notebooks, or applications that import and invoke `run()` repeatedly rather than launching a fresh process for each execution. ### Attack Path 1. A process imports `trader.py`. 2. An invocation calls `run(live=True)`, causing `_client` to be initialized with `ve ...[truncated 842 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not use one process-global client for both execution modes. - Cache clients separately by immutable venue, for example with a dictionary keyed by `"sim"` and `"polymarket"`. - Alternatively, construct a new client for every `run()` invocation. - Before each call to `client.trade()`, verify that the client's effective venue and live state match the invocation's requested mode. - Fail closed if the SDK does not expose a verifiable venue or live-state property. - Avoid mutating an SDK client's internal live flag after construction when the SDK provides a dedicated live-client constructor. - Add a regression test that invokes live mode followed by paper mode in the same process and verifies that the second order is simulated. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:187
Finding
Incorrect Probability Normalization Corrupts Trading Signals<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:187-198` **Vulnerability Type**: Financial decision-logic error **Risk Level**: High ### Vulnerable Code ```python direction = infer_direction(q, p) # Normalize to "up probability" for comparison across assets up_prob = p if direction == "up" or p >= 0.50 else 1.0 - p entry = WindowEntry(m, asset, window_key, p, direction, up_prob) groups.setdefault(window_key, []).append(entry) ``` The corrupted value is later used to calculate consensus: ```python entries = list(by_asset.values()) up_probs = [e.up_probability for e in entries] mean_up = statistics.mean(up_probs) ``` ### Technical Analysis The implementation assumes that YES represents the Up outcome. Under that assumption, the Up probability is always the market's current YES probability, `p`. Instead, the expression inverts every probability below 0.50 because such a value causes `direction` to be `"down"` and selects `1.0 - p`. For example, an actual Up probability of 0.40 is stored as 0.60. Consequently, normalized values are effectively constrained to be at least 0.50, which can erase a genuine Down consensus and distort both the group mean and deviation magnitude. The resulting consensus and divergence values feed directly into trade-side selection and position sizing. This is not merely inaccurate reporting; it can cause the program to submit an order based on the opposite of the observable market signal. ### Attack Path 1. The market search returns two or more matching markets in the same time window. 2. One or more markets have a YES/Up probability below 0.50. 3. `build_window_groups()` converts each such probability from `p` to `1.0 - p`. 4. `find_divergences()` computes `mean_up` using the inverted values. 5. The code derives an incorrect consensus direction or deviation. 6. `compute_signal()` applies its thresholds to the resulting opportunity. 7. If the thresholds and context checks pass, `client.trade()` submits the inco ...[truncated 714 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - If YES always means Up, replace the normalization with: ```python up_prob = p ``` - If market outcome ordering can vary, retrieve and validate the actual YES/NO token-to-outcome mapping instead of inferring semantics solely from question text. - Reject probabilities outside the valid `[0.0, 1.0]` range. - Add unit tests for values below, equal to, and above 0.50. - Add integration tests for all-Down, all-Up, and mixed-consensus groups. - Before enabling live execution, compare generated sides against independently calculated fixtures for representative market windows. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:269
Finding
Declared Liquidity and Portfolio Exposure Limits Are Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:269-290, 412-415` **Vulnerability Type**: Missing financial risk-control enforcement **Risk Level**: High ### Vulnerable Code The liquidity control is configured: ```python MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "5000")) MAX_POSITIONS = int(os.environ.get("SIMMER_MAX_POSITIONS", "8")) ``` However, `valid_market()` never evaluates market volume: ```python def valid_market(market) -> tuple[bool, str]: """Check spread and days-to-resolution gates.""" p = getattr(market, "current_probability", None) if not isinstance(p, (int, float)): return False, "missing probability" spread_cents = getattr(market, "spread_cents", None) if isinstance(spread_cents, (int, float)) and spread_cents / 100 > MAX_SPREAD: return False, f"Spread {spread_cents/100:.1%} > {MAX_SPREAD:.1%}" resolves_at = getattr(market, "resolves_at", None) if resolves_at: try: resolves = datetime.fromisoformat(resolves_at.replace("Z", "+00:00")) days = (resolves - datetime.now(timezone.utc)).days if days < MIN_DAYS: return False, f"Only {days} days to resolve" except Exception: pass return True, "ok" ``` The position limit only counts successful orders in the current invocation: ```python placed = 0 for market_id, opp in sorted(all_opps.items(), key=lambda x: -x[1][2]): if placed >= MAX_POSITIONS: break ``` ### Technical Analysis `SIMMER_MIN_VOLUME` is documented and loaded into `MIN_VOLUME`, but no function compares market volume against it. A market can therefore pass validation regardless of how little liquidity it has. `SIMMER_MAX_POSITIONS` is described as a maximum number of concurrent open positions. The implementation instead initializes `placed = 0` on every run and limits only the number of successful orders placed during that single invocation. It does not query exi ...[truncated 1615 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Retrieve the market's authoritative volume field and reject the market when volume is missing, malformed, or below `MIN_VOLUME`. - Define whether the control applies to total volume, recent volume, or immediately executable liquidity, and use the appropriate SDK field. - Query the account's existing open positions and pending orders before entering the trading loop. - Calculate remaining capacity as the configured limit minus current positions and reservations. - Treat repeated orders in the same market as exposure to an existing position rather than automatically counting them as independent safe entries. - Enforce both a count-based limit and an account-wide notional exposure limit. - Recheck limits immediately before each order to reduce race conditions. - Use server-side account or order limits where the SDK supports them. - Add tests covering low-volume markets, pre-existing positions, pending orders, and repeated invocations. ]]>

T08 · Insecure Dependencies

Warning
Location
clawhub.json:6
Finding
Trading SDK Dependency Is Not Version or Integrity Pinned<![CDATA[ ## Vulnerability Details **File Location**: `clawhub.json:6-9` **Vulnerability Type**: Unpinned security-sensitive third-party dependency **Risk Level**: Medium ### Vulnerable Code ```json "requires": { "env": [ "SIMMER_API_KEY" ], "pip": [ "simmer-sdk" ] } ``` The dependency is imported into the trading process: ```python from simmer_sdk import SimmerClient ``` ### Technical Analysis The project requests `simmer-sdk` without an exact version constraint or package-integrity hash. Installation can therefore resolve to a future release that was not part of this audit. This dependency is security-sensitive because it receives `SIMMER_API_KEY`, retrieves market data, and performs simulated or live trade operations. Python package installation and import can execute package-controlled code. A compromised upstream release, dependency confusion event, or malicious transitive dependency could consequently operate with the same process permissions as the skill. The audit found no evidence that the currently intended `simmer-sdk` package is malicious. The finding concerns the absence of controls ensuring that future installations use a specifically reviewed artifact. ### Attack Path 1. The environment installs dependencies declared in `clawhub.json`. 2. Package resolution selects the latest compatible `simmer-sdk` release because no exact version is specified. 3. The selected package or one of its transitive dependencies has been compromised or contains an unauthorized change. 4. Python executes dependency-controlled code during installation, import, client construction, or method calls. 5. That code can read process environment variables, including `SIMMER_API_KEY`, and can alter market data or trade requests. 6. The compromised component may exfiltrate the credential or submit unauthorized transactions using the process's trading authority. This path requires compromise or malicious modification of the dependency supply chain; it i ...[truncated 568 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin `simmer-sdk` to an exact, reviewed version rather than an unconstrained package name. - Use a lock file or deployment manifest containing cryptographic hashes for the package and all transitive dependencies. - Install with hash verification and disallow unlisted dependencies where the runtime permits it. - Review release notes and source changes before upgrading the pinned version. - Use an isolated virtual environment and a restricted service account. - Scope `SIMMER_API_KEY` to the minimum required permissions and financial limits. - Monitor the dependency with vulnerability and package-integrity scanning. - Prefer a trusted internal package mirror or allowlisted package repository for production installations. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The manifest explicitly requires a sensitive API credential and describes an automated trading skill, but provides no user-facing warning about credential use, order placement, or financial risk. In this context, users may authorize the skill without understanding that it can access exchange-integrated capabilities and execute trades, increasing the chance of unintended financial loss or overbroad trust.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script switches from paper mode to live trading solely via the `--live` flag and then proceeds directly to `run()` without any secondary confirmation, interlock, or environment-based authorization check. In a trading skill, this increases the chance of accidental real-money execution from operator error, automation misconfiguration, or wrapper scripts passing the flag unintentionally.

Static analysis

No suspicious patterns detected.