Back to skill

Security audit

Polymarket Candle Harami Trader

Security checks for vulnerabilities and agentic risk

Overview

This trading skill is open about needing a trading API key, but its live-trading safety limits are incomplete and can fail open, so it needs review before use with real funds.

Review carefully before enabling live mode. Use paper trading first, provide only a least-privilege trading key, set external account-level limits where available, and do not run this with real funds until the fail-open checks, missing volume enforcement, open-position accounting, and dependency pinning are fixed.

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

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:280
Finding
Market Context Safety Checks Fail Open<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:280-297` **Vulnerability Type**: Fail-open error handling in financial safety controls **Risk Level**: High ### Vulnerable Code ```python def context_ok(client: SimmerClient, market_id: str) -> tuple[bool, str]: """Check flip-flop and slippage safeguards.""" try: ctx = client.get_market_context(market_id) if not ctx: return True, "no context" if ctx.get("discipline", {}).get("is_flip_flop"): reason = ctx["discipline"].get("flip_flop_reason", "recent reversal") return False, f"Flip-flop: {reason}" slip = ctx.get("slippage", {}) if isinstance(slip, dict) and slip.get("slippage_pct", 0) > 0.15: return False, f"Slippage {slip['slippage_pct']:.1%}" for w in ctx.get("warnings", []): safe_print(f" [warn] {w}") except Exception as e: safe_print(f" [ctx] {market_id}: {e}") return True, "ok" ``` ### Technical Analysis The function is responsible for rejecting trades when market context indicates excessive slippage or recent flip-flop behavior. However, it approves the trade when no context is returned and also catches every exception before returning `True`. Consequently, timeouts, authentication errors, malformed API responses, SDK failures, and unexpected context schemas all bypass the intended safeguards. This is a fail-open design in a control directly preceding financial order submission. ### Attack Path 1. The program detects a candidate market and produces a trade signal. 2. The program calls `client.get_market_context(market_id)`. 3. The context service returns no data or raises an exception due to an outage, malformed response, or other failure. 4. The function returns `True`, despite not having verified slippage or flip-flop status. 5. In live mode, execution proceeds to `client.trade()`. 6. A real order can therefore be submitted without the advertised cont ...[truncated 514 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Make context validation fail closed in live mode. - Reject the trade if the context response is absent, malformed, or cannot be retrieved. - Catch specific expected exceptions rather than using `except Exception`. - Validate the context schema and types before consuming safety-critical fields. - Consider permitting fail-open behavior only in simulation mode, with an explicit warning. Example: ```python except (TimeoutError, ConnectionError, ValueError, KeyError, TypeError) as e: safe_print(f" [ctx] rejected {market_id}: {e}") return False, "Unable to validate market context" ``` Also replace `return True, "no context"` with a rejection for live trading. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
trader.py:224
Finding
Resolution-Time Safeguard Fails Open on Invalid Timestamps<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:224-232` **Vulnerability Type**: Fail-open validation of market resolution data **Risk Level**: Medium ### Vulnerable Code ```python # Days-to-resolution gate if market.resolves_at: try: resolves = datetime.fromisoformat(market.resolves_at.replace("Z", "+00:00")) days = (resolves - datetime.now(timezone.utc)).days if days < MIN_DAYS: return None, 0, f"Only {days} days to resolve" except Exception: pass ``` ### Technical Analysis The minimum-days safeguard depends on successfully parsing `market.resolves_at`. All parsing and time-calculation exceptions are silently ignored, leaving the market eligible for trading. Market metadata is externally supplied data and must not be trusted to have the expected type, syntax, or timezone information. A malformed timestamp, a naive timestamp that cannot be subtracted from a timezone-aware timestamp, or an unexpected SDK object can bypass the control. ### Attack Path 1. A candidate market matches the title and harami-pattern requirements. 2. Its `resolves_at` value is malformed, has an unexpected type, or lacks compatible timezone information. 3. `datetime.fromisoformat()` or the subsequent date calculation raises an exception. 4. The broad exception handler silently suppresses the error. 5. Signal evaluation continues without enforcing `MIN_DAYS`. 6. In live mode, the application may place a real order in a market that should have been rejected. ### Impact Assessment This issue does not grant new system privileges. It causes misuse of the application's existing trading authority by bypassing a configured risk boundary. The practical impact is exposure to markets resolving sooner than permitted, including reduced time to react, increased execution risk, and possible losses. The scope is limited by account funds and other functioning trade controls. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Treat missing, malformed, or timezone-incompatible resolution timestamps as grounds for rejecting a live trade. - Catch only expected parsing and type errors. - Require a timezone-aware timestamp and normalize it to UTC. - Use total seconds rather than `.days` if the policy needs precise boundary enforcement. - Emit a structured rejection message so operators can diagnose bad market data. Example: ```python if not market.resolves_at: return None, 0, "Missing resolution timestamp" try: resolves = datetime.fromisoformat( str(market.resolves_at).replace("Z", "+00:00") ) if resolves.tzinfo is None: return None, 0, "Resolution timestamp has no timezone" except (TypeError, ValueError) as e: return None, 0, f"Invalid resolution timestamp: {e}" remaining = resolves.astimezone(timezone.utc) - datetime.now(timezone.utc) if remaining.total_seconds() < MIN_DAYS * 86400: return None, 0, "Resolution is too close" ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
trader.py:24
Finding
Declared Minimum-Volume Safeguard Is Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:24-32` **Vulnerability Type**: Missing enforcement of a documented financial risk control **Risk Level**: Medium ### Vulnerable Code ```python # Risk parameters -- declared as tunables in clawhub.json, adjustable from Simmer UI. MAX_POSITION = float(os.environ.get("SIMMER_MAX_POSITION", "40")) MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "3000")) MAX_SPREAD = float(os.environ.get("SIMMER_MAX_SPREAD", "0.10")) MIN_DAYS = int(os.environ.get( "SIMMER_MIN_DAYS", "1")) MAX_POSITIONS = int(os.environ.get( "SIMMER_MAX_POSITIONS", "10")) YES_THRESHOLD = float(os.environ.get("SIMMER_YES_THRESHOLD", "0.38")) NO_THRESHOLD = float(os.environ.get("SIMMER_NO_THRESHOLD", "0.62")) MIN_TRADE = float(os.environ.get("SIMMER_MIN_TRADE", "5")) ``` The value is also reloaded in `get_client()`, but neither `find_markets()` nor `compute_signal()` compares market volume against `MIN_VOLUME`. The control is advertised in `clawhub.json:36-47`: ```json { "env": "SIMMER_MIN_VOLUME", "type": "number", "default": 3000, "range": [ 0, 500000 ], "step": 500, "label": "Min market volume USD" } ``` ### Technical Analysis `SIMMER_MIN_VOLUME` is presented as an adjustable minimum-volume filter and is read into the process, but it is never used to decide whether a market is eligible for trading. Merely loading a configuration value does not enforce the associated security or risk policy. Low-volume prediction markets can have poor liquidity, unstable pricing, large realized slippage, and greater susceptibility to price manipulation. The separate spread and context checks do not reliably substitute for a volume threshold, particularly because context checks also fail open. ### Attack Path 1. An illiquid market is returned by keyword search or broad market discovery. 2. Its title and probability sequence satisfy the interval and harami-pattern checks. ...[truncated 699 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Read the market's authoritative volume field and enforce the threshold before pattern detection or order submission. - Reject missing, nonnumeric, negative, or otherwise invalid volume data in live mode. - Define whether the configured value refers to total volume, rolling volume, or available liquidity and use a consistent SDK field. - Revalidate liquidity immediately before placing the order because discovery data may be stale. - Add tests proving that markets below the threshold cannot reach `client.trade()`. Example: ```python volume = getattr(market, "volume", None) try: volume = float(volume) except (TypeError, ValueError): return None, 0, "Missing or invalid market volume" if volume < MIN_VOLUME: return None, 0, f"Volume ${volume:.2f} below ${MIN_VOLUME:.2f}" ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:318
Finding
Maximum Concurrent Position Limit Only Counts Orders in the Current Run<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:318-345` **Vulnerability Type**: Incorrect enforcement of aggregate financial exposure limit **Risk Level**: High ### Vulnerable Code ```python placed = 0 for m, harami_dir, p_large, p_small in harami_targets: if placed >= MAX_POSITIONS: break side, size, reasoning = compute_signal(m, harami_dir, p_large, p_small) if not side: safe_print(f" [skip] {reasoning}") continue ok, why = context_ok(client, m.id) if not ok: safe_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}" safe_print(f" [trade] {side.upper()} ${size} {tag} {status} -- {reasoning[:70]}") if r.success: placed += 1 ``` ### Technical Analysis The metadata and documentation describe `SIMMER_MAX_POSITIONS` as a maximum number of concurrent open positions. The implementation instead initializes a local counter to zero on every invocation and increments it only after successful orders during that invocation. It does not query existing open positions, pending orders, or orders submitted by concurrent process instances. Therefore, repeated, overlapping, or scheduled executions can each place up to the configured number of additional orders. This is also a time-of-check/time-of-use concern: even if an initial position count were added, concurrent workers would still require atomic reservation or server-side enforcement. ### Attack Path 1. The account already has open positions, or the script completes a run that creates positions. 2. The script is invoked again while those positions remain open. 3. `placed` is reset to zero. 4. The n ...[truncated 633 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Query authoritative open positions and pending orders before submitting any new order. - Calculate remaining capacity as the configured maximum minus existing and reserved positions. - Count unique open positions according to a clearly documented policy. - Enforce the limit atomically on the trading service where possible. - If client-side enforcement is unavoidable, use a cross-process lock and re-query state immediately before each order. - Reject trading when position state cannot be retrieved. - Consider enforcing both position-count and total-notional limits. Illustrative logic: ```python positions = client.get_positions(status="open") pending = client.get_orders(status="pending") used = count_unique_exposures(positions, pending) remaining = MAX_POSITIONS - used if remaining <= 0: safe_print("[skip] Concurrent position limit reached") return ``` A server-side account risk limit should remain the authoritative protection against races and multiple bot instances. ]]>

T08 · Insecure Dependencies

Warning
Location
clawhub.json:6
Finding
Security-Critical Trading SDK Dependency Is Unpinned<![CDATA[ ## Vulnerability Details **File Location**: `clawhub.json:6-8` **Vulnerability Type**: Unpinned third-party executable dependency **Risk Level**: Medium ### Vulnerable Code ```json "pip": [ "simmer-sdk" ] ``` The dependency is imported by `trader.py:16`: ```python from simmer_sdk import SimmerClient ``` ### Technical Analysis The project installs `simmer-sdk` without an exact version or integrity hash. Installation can therefore resolve to different package contents over time. The SDK is security-sensitive because it receives `SIMMER_API_KEY`, retrieves market data, performs safety checks, and submits simulated or live orders. A compromised upstream release, malicious package takeover, or unexpected incompatible update would execute in the application's process with access to its environment and network privileges. No evidence was found that the current dependency is malicious. The finding concerns the absence of reproducible and integrity-verified dependency resolution. ### Attack Path 1. The skill is installed in a new environment. 2. Package resolution retrieves the latest version matching the unbounded name `simmer-sdk`. 3. The retrieved release is compromised, unexpectedly modified, or incompatible. 4. Python imports the package into the trader process. 5. Dependency initialization or invoked SDK methods execute with access to the process environment, including `SIMMER_API_KEY`. 6. A compromised dependency could misuse the credential, alter market responses, or change live-order behavior. ### Impact Assessment A malicious dependency would run with the same operating-system permissions as the trader process. It could read environment variables available to that process, including the trading API key, make outbound network requests, falsify safety data, or submit trades within the credential's authority. The potential scope therefore includes process-accessible data and the Polymarket trading privileges granted to `SIMMER_API_KEY`. ...[truncated 86 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin `simmer-sdk` to an exact, reviewed version. - Use a lock file with cryptographic hashes, such as a hash-locked `requirements.txt`. - Install only from the expected package index over authenticated TLS. - Review dependency release provenance and monitor it for compromise. - Use automated dependency scanning and controlled update procedures. - Run the trader under a dedicated least-privilege account with minimal environment variables and network access. - Restrict the API key's permissions and account-level trading limits where supported. Example requirement: ```text simmer-sdk==<reviewed-version> \ --hash=sha256:<verified-distribution-hash> ``` ]]>
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 (2)

Lp3

Medium
Category
MCP Least Privilege
Confidence
85% confidence
Finding
The skill declares access to a high-value environment credential (`SIMMER_API_KEY`) but does not define any explicit tool scope such as `permissions` or `allowed-tools`. In an agent setting, missing scope boundaries can let the skill access environment data more broadly than intended, increasing the chance of credential exposure or misuse if later code or prompts are unsafe.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The manifest clearly configures an automated trader via an API key, managed entrypoint, and tunable position-sizing/risk parameters, but it does not warn users that enabling the skill can place real trades automatically. This is dangerous because users may supply credentials or start the automaton without understanding that it can take market positions and incur financial loss within the configured limits.

Static analysis

No suspicious patterns detected.