Back to skill

Security audit

Questrade

Security checks for vulnerabilities and agentic risk

Overview

The skill is not malware, but its live-trading checklist can falsely present financial safety checks as passed, so it needs careful review before use.

Install only if you treat the generated checklist as drafting aid, not as proof that a trade is safe or compliant. Manually verify risk, buying power, quote freshness, symbol, side, quantity, and broker state in Questrade before submitting any order, and avoid using untrusted input for symbols, account IDs, or notes.

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/build_trade_checklist.py:48
Finding
Non-Finite Numeric Values Bypass Live-Trade Safety Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/build_trade_checklist.py:48-64, 110-130` **Vulnerability Type**: Improper numeric input validation / fail-open safety checks **Risk Level**: High ### Vulnerable Code ```python def validate_price(value: str) -> float: number = float(value) if number <= 0: raise argparse.ArgumentTypeError("Price must be greater than zero.") return number def validate_non_negative_float(value: str) -> float: number = float(value) if number < 0: raise argparse.ArgumentTypeError("Value must be zero or greater.") return number ``` ```python if args.environment == "live": if args.risk_cap_usd is None: parser.error("--risk-cap-usd is required for live trading mode") if args.data_age_seconds is None: parser.error("--data-age-seconds is required for live trading mode") if args.data_age_seconds > args.max_data_age_seconds: parser.error( f"data is stale ({args.data_age_seconds}s > {args.max_data_age_seconds}s)" ) if args.order_type == "market" and not args.allow_market_order_live: parser.error( "live market orders are blocked by default; pass --allow-market-order-live " "to override explicitly" ) if ( args.observed_price_drift_pct is not None and args.observed_price_drift_pct > args.max_price_drift_pct ): parser.error( "observed price drift exceeds configured maximum " f"({args.observed_price_drift_pct}% > {args.max_price_drift_pct}%)" ) ``` ### Technical Analysis Python's `float()` accepts special IEEE-754 values such as `nan`, `inf`, and `-inf`. The validators only use ordinary comparisons and do not verify that the parsed value is finite. In particular, comparisons involving `NaN` are false: ```python float("nan") <= 0 # False float("nan") < 0 # False float("nan") > 1.0 # False ``` Consequently, `n ...[truncated 1706 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject every non-finite floating-point value immediately after parsing: ```python import math def validate_price(value: str) -> float: try: number = float(value) except ValueError as exc: raise argparse.ArgumentTypeError("Price must be numeric.") from exc if not math.isfinite(number) or number <= 0: raise argparse.ArgumentTypeError( "Price must be a finite number greater than zero." ) return number ``` 2. Apply `math.isfinite()` to all floating-point validators, including non-negative percentages and thresholds. 3. Consider using `decimal.Decimal` for financial values to avoid binary floating-point behavior. 4. Generate strict JSON with: ```python json.dumps(payload, indent=2, allow_nan=False) ``` 5. Add automated tests covering `nan`, `NaN`, `inf`, `-inf`, overflow values, negative values, and zero. 6. Treat serialization failure as a blocked checklist rather than replacing invalid data silently. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/build_trade_checklist.py:110
Finding
Live-Trade Risk Cap Is Required but Never Enforced<![CDATA[ ## Vulnerability Details **File Location**: `scripts/build_trade_checklist.py:110-113, 139-145, 160-180` **Vulnerability Type**: Missing risk-limit enforcement **Risk Level**: High ### Vulnerable Code ```python if args.environment == "live": if args.risk_cap_usd is None: parser.error("--risk-cap-usd is required for live trading mode") ``` ```python reference_price: Optional[float] = args.limit_price or args.stop_price estimated_notional = None if reference_price is not None: estimated_notional = reference_price * args.quantity ``` ```python "special_safety_check": { "status": "pass", "policy_acknowledged": True, "user_authorized": args.confirm_user_authorized, "manual_execution_confirmed": args.confirm_manual_execution, "no_secrets_shared_confirmed": args.confirm_no_secrets_shared, "data_age_seconds": args.data_age_seconds, "max_data_age_seconds": args.max_data_age_seconds, "observed_price_drift_pct": args.observed_price_drift_pct, "max_price_drift_pct": args.max_price_drift_pct, "risk_cap_usd": args.risk_cap_usd, }, ``` ### Technical Analysis The live-mode gate checks only whether `risk_cap_usd` was supplied. It never compares that cap with: - The calculated `estimated_notional` - Quantity multiplied by an authoritative or reference price - The distance between entry and stop price - A maximum-loss estimate - Any other exposure or risk measurement The script calculates `estimated_notional` when a limit or stop price is available, but the result is only displayed. It is not used to accept or reject the checklist. This behavior conflicts with the documented claim that live mode enforces a risk cap. Any positive value satisfies the presence check, including a cap that is negligible relative to the proposed trade. The output nevertheless records the special safety check as passed. For market orders, the script does not have an order price with which to calculate exposure. Even in that case, ...[truncated 1309 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define the exact meaning of `risk_cap_usd`. Distinguish between: - Maximum order notional - Maximum estimated loss - Maximum loss relative to a stop - Account-level aggregate risk 2. Calculate and enforce the corresponding metric. For example, if the cap means maximum estimated loss: ```python estimated_loss = abs(entry_price - stop_price) * args.quantity if estimated_loss > args.risk_cap_usd: parser.error( f"estimated loss ${estimated_loss:.2f} exceeds " f"risk cap ${args.risk_cap_usd:.2f}" ) ``` 3. If the cap means maximum notional, compare `reference_price * quantity` directly with the cap. 4. Require enough data to calculate risk. Do not mark the safety check as passed when entry price, current quote, stop price, or another required value is unavailable. 5. For market orders, require a fresh reference quote and apply a conservative slippage buffer before calculating exposure. 6. Represent checks individually in output, including the calculated risk, configured cap, formula used, and pass/fail result. 7. Add boundary tests for values below, equal to, and above the cap. 8. Update documentation so the term “risk cap” precisely matches the implemented calculation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/build_trade_checklist.py:139
Finding
Unescaped User-Controlled Fields Permit Markdown Checklist Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/build_trade_checklist.py:139-180, 222-223` **Vulnerability Type**: Markdown injection / output integrity violation **Risk Level**: Medium ### Vulnerable Code ```python account_value = args.account_id if args.include_sensitive else mask_identifier(args.account_id) payload = { "created_at_utc": created_at, "account_id": account_value, "account_id_masked": mask_identifier(args.account_id), "environment": args.environment, "special_safety_check": { "status": "pass", "policy_acknowledged": True, "user_authorized": args.confirm_user_authorized, "manual_execution_confirmed": args.confirm_manual_execution, "no_secrets_shared_confirmed": args.confirm_no_secrets_shared, "data_age_seconds": args.data_age_seconds, "max_data_age_seconds": args.max_data_age_seconds, "observed_price_drift_pct": args.observed_price_drift_pct, "max_price_drift_pct": args.max_price_drift_pct, "risk_cap_usd": args.risk_cap_usd, }, "symbol": args.symbol.upper(), "side": args.side, "quantity": args.quantity, "order_type": args.order_type, "time_in_force": args.tif, "limit_price": args.limit_price, "stop_price": args.stop_price, "estimated_notional": estimated_notional, "risk_cap_usd": args.risk_cap_usd, "notes": args.notes, } lines = [ "# Trade Checklist", "", "## Ticket", f"- Created (UTC): {created_at}", f"- Environment: {args.environment.upper()}", f"- Account: {account_value}", f"- Symbol: {args.symbol.upper()}", f"- Side: {args.side.upper()}", f"- Quantity: {args.quantity}", f"- Order type: {args.order_type}", f"- Time in force: {args.tif.upper()}", ``` ```python if args.notes: lines.extend(["", "## Notes", args.notes]) ``` ### Technical Analysis The command-line values `account_id`, `symbol`, and `notes` are inserted directly into a Mar ...[truncated 2349 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict symbols to a documented allowlist grammar suitable for supported exchanges. For example: ```python import re SYMBOL_RE = re.compile(r"^[A-Z0-9][A-Z0-9.\-]{0,14}$") def validate_symbol(value: str) -> str: symbol = value.strip().upper() if not SYMBOL_RE.fullmatch(symbol): raise argparse.ArgumentTypeError("Invalid ticker symbol.") return symbol ``` 2. Reject carriage returns, line feeds, null bytes, and other control characters in account identifiers and all single-line fields. 3. Escape Markdown metacharacters before interpolation into list items. 4. Treat notes as explicitly untrusted content and place them in a clearly delimited fenced block, while safely handling embedded fence sequences. 5. Consider generating the human-readable view from a structured, validated data model rather than concatenating raw strings. 6. Keep the JSON ticket authoritative and display a warning if human-readable and structured values cannot be generated from the same validated object. 7. Add tests using newline injection, headings, checkbox syntax, links, HTML, and code-fence termination attempts. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • 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
Findings (5)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
This is a second description-versus-behavior issue with the same root problem: the skill presents itself as supporting execution and broker workflow operations without direct integration or enforceable logic for those actions. In a high-stakes trading setting, overstated capabilities can cause operators to skip manual verification, misunderstand the system's limits, or believe fills and order details were validated when they were not.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
This is a second description-versus-behavior issue with the same root problem: the skill presents itself as supporting execution and broker workflow operations without direct integration or enforceable logic for those actions. In a high-stakes trading setting, overstated capabilities can cause operators to skip manual verification, misunderstand the system's limits, or believe fills and order details were validated when they were not.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
1. Need a quote/monitoring snapshot across symbols: run `scripts/market_snapshot.py`.
2. Need a trade-ready plan with risk controls: run `scripts/build_trade_checklist.py` with Special Safety Check flags.
3. Need to place a trade in Questrade Web: follow `references/questrade-browser-playbook.md`.
4. Need field-level input/output rules: load `references/data-contracts.md`.
5. Need policy gating details: load `references/openclaw-policy-compliance.md`.

## Standard Execution Flow
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill advertises scripted workflow steps that imply file access and network use, but it does not declare an explicit tool or permission scope. That creates unnecessary ambiguity about what capabilities the agent may invoke, which is risky in a trading context because undeclared file/network access can lead to unexpected data handling or outbound requests.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
## Privacy Rules (OpenClaw.ai)
- Keep all credentials, MFA tokens, session cookies, API keys, and passwords user-side only.
- Never ask the user to paste secrets into chat, files, or logs.
- Use masked identifiers in generated artifacts by default (account IDs, order IDs, personal details).
- Only include raw sensitive identifiers when the user explicitly asks and confirms local-only usage.
- Store outputs locally and avoid sharing raw broker exports unless redacted.
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Static analysis

No suspicious patterns detected.