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

Content
View full analysis
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
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:187
Finding

Incorrect Probability Normalization Corrupts Trading Signals

Content
View full analysis
= 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
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:269
Finding

Declared Liquidity and Portfolio Exposure Limits Are Not Enforced

Content
View full analysis
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
View remediation

T08 · Insecure Dependencies

Warning
Location
clawhub.json:6
Finding

Trading SDK Dependency Is Not Version or Integrity Pinned

Content
View full analysis
Remediation
View remediation
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)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.