Back to skill

Security audit

Polymarket Biotech Trader

Security checks for vulnerabilities and agentic risk

Overview

This skill is an automated prediction-market trader with real-money capability, but its advertised research inputs and risk controls do not fully match the implementation.

Review carefully before installing with a live-capable key. Use paper mode first, pin and review the simmer-sdk dependency, set conservative tunables explicitly, and do not rely on the advertised FDA/PDUFA/base-rate intelligence or volume/open-position safeguards unless the implementation is fixed or externally constrained by account-side limits.

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:18
Finding
Declared Minimum-Volume Safeguard Is Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:18-25, 166-193` **Vulnerability Type**: Missing enforcement of a financial risk control **Risk Level**: High ### Complete Code Snippet ```python MAX_POSITION = float(os.environ.get("SIMMER_MAX_POSITION", "35")) MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "1000")) MAX_SPREAD = float(os.environ.get("SIMMER_MAX_SPREAD", "0.1")) MIN_DAYS = int(os.environ.get( "SIMMER_MIN_DAYS", "0")) MAX_POSITIONS = int(os.environ.get( "SIMMER_MAX_POSITIONS", "6")) YES_THRESHOLD = float(os.environ.get("SIMMER_YES_THRESHOLD", "0.42")) NO_THRESHOLD = float(os.environ.get("SIMMER_NO_THRESHOLD", "0.58")) MIN_TRADE = float(os.environ.get("SIMMER_MIN_TRADE", "5")) ``` ```python placed = 0 for m in markets: if placed >= MAX_POSITIONS: break side, size, reasoning = compute_signal(m) if not side: print(f" [skip] {reasoning}") continue ok, why = context_ok(client, m.id) if not ok: print(f" [skip] {why}") continue try: r = client.trade( market_id=m.id, side=side, amount=size, source=TRADE_SOURCE, skill_slug=SKILL_SLUG, reasoning=reasoning, ) tag = "(sim)" if r.simulated else "(live)" status = "OK" if r.success else f"FAIL:{r.error}" print(f" [trade] {side.upper()} ${size} {tag} {status} — {reasoning[:70]}") if r.success: placed += 1 except Exception as e: print(f" [error] {m.id}: {e}") ``` ### Technical Analysis `MIN_VOLUME` is loaded from the environment and presented as a tunable risk parameter, but neither `compute_signal()` nor the trading loop compares a market's volume against it. Consequently, the configured minimum-volume value has no effect. Liquidity checks are important for prediction-market automation because low-volume markets are more suscepti ...[truncated 1008 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Obtain volume from a trusted, normalized market field before signal computation. - Reject markets when volume is missing, malformed, negative, or below `MIN_VOLUME`; use fail-closed behavior for live trading. - Distinguish between total historical volume and currently executable liquidity. - Add tests proving that markets below the threshold cannot reach `client.trade()`. - Log the observed volume and configured threshold for every rejection. - Consider validating order-book depth for the intended position size in addition to aggregate market volume. Example hardening logic: ```python volume = getattr(m, "volume", None) if volume is None: if live: print(f" [skip] Missing volume for {m.id}") continue else: try: if float(volume) < MIN_VOLUME: print(f" [skip] Volume ${float(volume):,.2f} below ${MIN_VOLUME:,.2f}") continue except (TypeError, ValueError): print(f" [skip] Invalid volume for {m.id}") continue ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:166
Finding
Maximum Open-Position Limit Only Restricts Orders Within One Run<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:166-193` **Vulnerability Type**: Incorrect portfolio-limit implementation **Risk Level**: High ### Complete Code Snippet ```python placed = 0 for m in markets: if placed >= MAX_POSITIONS: break side, size, reasoning = compute_signal(m) if not side: print(f" [skip] {reasoning}") continue ok, why = context_ok(client, m.id) if not ok: print(f" [skip] {why}") continue try: r = client.trade( market_id=m.id, side=side, amount=size, source=TRADE_SOURCE, skill_slug=SKILL_SLUG, reasoning=reasoning, ) tag = "(sim)" if r.simulated else "(live)" status = "OK" if r.success else f"FAIL:{r.error}" print(f" [trade] {side.upper()} ${size} {tag} {status} — {reasoning[:70]}") if r.success: placed += 1 except Exception as e: print(f" [error] {m.id}: {e}") ``` ### Technical Analysis The documentation describes `MAX_POSITIONS` as the maximum number of concurrent open positions. The implementation instead initializes `placed` to zero on every invocation and counts only successful orders placed during that invocation. The code never queries existing positions, pending orders, or current exposure. It also does not prevent repeated purchases in a market already held. As a result, repeated executions can exceed the advertised portfolio limit even though each individual run respects its local counter. ### Attack Path 1. The account already has open positions, or a first execution opens up to `MAX_POSITIONS` positions. 2. The skill is executed again manually or by an external scheduler. 3. `placed` is reset to zero. 4. Existing positions are not retrieved or counted. 5. The second run places additional successful orders. 6. Repeated runs continue accumulating exposure beyond the configured maximum. ### Imp ...[truncated 347 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Query all current open positions and pending orders before processing candidate markets. - Calculate remaining capacity as `MAX_POSITIONS - existing_open_positions`. - Count unique market exposures rather than only successful API responses. - Prevent duplicate orders for a market that already has an open position or pending order, unless an explicit position-adjustment policy authorizes it. - Recheck portfolio state immediately before each live trade to reduce race conditions. - Use an account-side atomic risk limit where supported by the SDK or trading venue. - Add integration tests covering repeated invocations, existing positions, pending orders, and concurrent runners. Illustrative logic: ```python positions = client.get_positions() open_market_ids = { p.market_id for p in positions if getattr(p, "is_open", False) } remaining = max(0, MAX_POSITIONS - len(open_market_ids)) for m in markets: if remaining <= 0: break if m.id in open_market_ids: continue # Perform all other checks and submit the order. # Decrement only after confirmed success. ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:18
Finding
Runtime Risk Defaults Contradict Documented Safety Parameters<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:18-25` **Related Locations**: `SKILL.md:62-67, 111-119`; `clawhub.json:17-107` **Vulnerability Type**: Security-relevant configuration inconsistency **Risk Level**: High ### Complete Code Snippet Runtime defaults in `trader.py`: ```python MAX_POSITION = float(os.environ.get("SIMMER_MAX_POSITION", "35")) MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "1000")) MAX_SPREAD = float(os.environ.get("SIMMER_MAX_SPREAD", "0.1")) MIN_DAYS = int(os.environ.get( "SIMMER_MIN_DAYS", "0")) MAX_POSITIONS = int(os.environ.get( "SIMMER_MAX_POSITIONS", "6")) YES_THRESHOLD = float(os.environ.get("SIMMER_YES_THRESHOLD", "0.42")) NO_THRESHOLD = float(os.environ.get("SIMMER_NO_THRESHOLD", "0.58")) MIN_TRADE = float(os.environ.get("SIMMER_MIN_TRADE", "5")) ``` Conflicting documented values in `SKILL.md`: ```markdown | Max position size | $35 USDC | Binary events warrant careful sizing | | Min market volume | $5,000 | FDA markets attract informed traders | | Max bid-ask spread | 10% | Allow wider for rare disease markets | | Min days to resolution | 7 | Don't enter days before PDUFA | | Max open positions | 6 | Biotech events are correlated | ``` ```markdown | `SIMMER_MAX_POSITION` | `35` | Max USDC per trade (reached at 100% conviction) | | `SIMMER_MIN_VOLUME` | `5000` | Min market volume filter (USD) | | `SIMMER_MAX_SPREAD` | `0.10` | Max bid-ask spread (0.10 = 10%) | | `SIMMER_MIN_DAYS` | `7` | Min days until market resolves | | `SIMMER_MAX_POSITIONS` | `6` | Max concurrent open positions | | `SIMMER_YES_THRESHOLD` | `0.38` | Buy YES if market price ≤ this value | | `SIMMER_NO_THRESHOLD` | `0.62` | Sell NO if market price ≥ this value | | `SIMMER_MIN_TRADE` | `5` | Floor for any trade (min USDC regardless of conviction) | ``` Conflicting metadata defaults in `clawhub.json` include: ```json { "env": "SIMMER_MIN_VOLUME", "default": 1000, "range": [ ...[truncated 2393 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Define risk defaults in one authoritative configuration source. - Generate documentation and runtime configuration from that source instead of maintaining separate copies. - Align the intended defaults across all files. Based on the published documentation, that would include: - `SIMMER_MIN_VOLUME=5000` - `SIMMER_MIN_DAYS=7` - `SIMMER_MAX_POSITIONS=6` - `SIMMER_YES_THRESHOLD=0.38` - `SIMMER_NO_THRESHOLD=0.62` - Validate configuration after `apply_skill_config()` and fail closed if values are absent, malformed, contradictory, or outside approved bounds. - Print the final post-configuration values, rather than values captured before `get_client()` applies managed overrides. - Add tests that compare documented defaults, metadata defaults, and effective runtime defaults. - Require an additional explicit confirmation for live mode when configuration is less restrictive than the reviewed baseline. ]]>

T08 · Insecure Dependencies

Error
Location
clawhub.json:7
Finding
Trading SDK Dependency Is Unpinned and Receives a Live-Capable Credential<![CDATA[ ## Vulnerability Details **File Location**: `clawhub.json:7-9` **Related Locations**: `trader.py:10, 41-50`; `SKILL.md:132-138` **Vulnerability Type**: Unpinned security-critical third-party dependency **Risk Level**: High ### Complete Code Snippet Dependency declaration in `clawhub.json`: ```json "pip": [ "simmer-sdk" ] ``` The dependency is imported and receives the API credential in `trader.py`: ```python from simmer_sdk import SimmerClient ``` ```python venue = "polymarket" if live else "sim" _client = SimmerClient( api_key=os.environ["SIMMER_API_KEY"], venue=venue, ) ``` ### Technical Analysis The package requirement specifies only `simmer-sdk`, without an exact version, lockfile, package hash, or integrity constraint. Installation can therefore resolve to whichever version the package index currently serves. This dependency is security-critical: imported package code runs with the Python process's permissions, receives `SIMMER_API_KEY`, performs network requests, retrieves market information, and submits trades. A compromised publisher account, malicious package release, dependency takeover, or unsafe future update could alter behavior after the skill itself has been reviewed. The audit found no evidence that the currently referenced package is malicious. The confirmed issue is the absence of version and integrity controls around a highly privileged dependency. ### Attack Path 1. An attacker compromises the package publication channel or causes a malicious `simmer-sdk` release to become the version selected during installation. 2. A user installs or reinstalls the skill without a lockfile or hash constraint. 3. The package manager downloads the attacker-controlled release. 4. Python imports the dependency when `trader.py` starts. 5. The package code executes with the process's permissions and receives `SIMMER_API_KEY`. 6. The compromised dependency can exfiltrate the credential, falsify market data, modify order paramete ...[truncated 444 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin `simmer-sdk` to an exact version that has been reviewed, for example `simmer-sdk==<reviewed-version>`. - Use a requirements lockfile with cryptographic hashes and install with hash verification. - Retrieve packages only from an explicitly configured trusted index. - Review release signatures, source code, ownership changes, and dependency changes before upgrading. - Run the skill in a restricted environment with minimal filesystem and network access. - Use a dedicated API key with the minimum required trading permissions and strict account-side spending limits. - Separate paper-trading and live-trading credentials. - Rotate the API key immediately if an installed dependency version is later found to be compromised. - Consider vendoring or independently verifying the small portion of SDK functionality required for live trading. ]]>
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)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The skill markets itself as using FDA calendars, PDUFA timing, and clinical trial registry intelligence, but the described implementation is only a broad keyword scanner with threshold-based trading. In a financial automation context, that mismatch is dangerous because users may grant trading authority under false assumptions about diligence, safeguards, and signal quality, leading to misplaced trust and potentially real monetary loss.

Lp3

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding
The skill references environment-based credentials (`SIMMER_API_KEY`) and discusses trade execution, but it declares no explicit tool or permission scope. That weakens least-privilege guarantees and makes it harder to constrain what an agent may access or expose at runtime, especially in a trading context involving live-capable credentials.

Intent-Code Divergence

Medium
Confidence
89% confidence
Finding
The file asserts that the skill defaults to paper trading, does not autostart, and only goes live with an explicit `--live` flag, but those are unenforced documentation claims in the provided content. If operators rely on that text without code-level enforcement, the skill could execute live trades or run automatically in a misconfigured environment, causing direct financial impact.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The manifest explicitly requires an API key and describes an automated trading skill, but it provides no user-facing warning that credentials will be used for networked market activity or that the skill can place trades. In a trading context, this omission is risky because users may grant sensitive credentials without clear consent to automated financial actions, increasing the chance of unintended account usage or losses.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The docstring states that trading signals are informed by external biotech sources such as ClinicalTrials.gov, BioMedTracker, and short-interest data, but the implementation only uses market price thresholds and basic market metadata. In an automated trading skill, this mismatch is security-relevant because operators may deploy it with inflated trust in its sophistication and risk controls, leading to unsafe live trading based on misleading documentation.

Static analysis

No suspicious patterns detected.