Back to skill

Security audit

Moomoo Trading

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent for moomoo/Futu trading, but live trading can send the unlock password to any configured OpenD host and the setup check exposes raw financial account identifiers.

Install only if you understand that this skill can trade real securities when explicitly invoked. Keep live trading pointed at a trusted local OpenD instance, avoid remote --host values unless you have a secure authenticated tunnel, protect the unlock-password environment variable, and avoid sharing setup_check.py output because it may include financial account identifiers.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/trade.py:28
Finding
Live trading password can be transmitted to an arbitrary OpenD host<![CDATA[ ## Vulnerability Details **File Location**: `scripts/trade.py`, lines 28-43 **Vulnerability Type**: Credential exposure through an unrestricted network destination **Risk Level**: High ### Vulnerable Code ```python def open_trade_context(api, args, *, ticker=None): return api.OpenSecTradeContext( host=args.host, port=args.port, filter_trdmarket=get_trade_market(api, ticker=ticker, market_code=args.market), ) def unlock_if_needed(ctx, api, args): if args.env != "real": return False password = env_password(args.unlock_password_env) ret, data = ctx.unlock_trade(password=password) if ret != api.RET_OK: raise RuntimeError(f"unlock_trade failed: {data}") return True ``` The destination is exposed as an unrestricted command-line option: ```python parser.add_argument( "--host", default=DEFAULT_HOST, help=f"OpenD host (default: {DEFAULT_HOST})" ) ``` ### Technical Analysis For real trading, the script reads the trading unlock password from an environment variable and passes it to `ctx.unlock_trade`. The same context is constructed using the user-controlled `--host` and `--port` arguments. The safe default is the loopback address `127.0.0.1`, but the implementation does not enforce that default for password-bearing live-trading operations. It does not require a destination allowlist, explicit remote-host confirmation, authenticated transport, or server identity verification at the skill layer. Consequently, a user or agent that is induced to specify an attacker-controlled host can cause the unlock credential to be submitted to that endpoint. The audit cannot establish from the project code whether the third-party SDK encrypts or authenticates its protocol, so passive plaintext interception is not asserted. Nevertheless, an attacker-controlled endpoint is the active recipient supplied to the SDK and may emulate or proxy the expected OpenD service. Allowing remote OpenD dep ...[truncated 1746 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce loopback destinations for real-trading operations by default: ```python import ipaddress import socket def require_local_live_host(host): addresses = { ipaddress.ip_address(item[4][0]) for item in socket.getaddrinfo(host, None) } if not addresses or not all(address.is_loopback for address in addresses): raise ValueError("Real trading requires a loopback OpenD host.") ``` 2. If remote OpenD access is a required feature, place it behind a separate explicit option such as `--allow-remote-live-opend`, with a prominent warning and an additional interactive confirmation. 3. Require an authenticated and encrypted tunnel, such as mutually authenticated TLS, SSH port forwarding, or a VPN. Prefer connecting the script to a local forwarded port so the password-bearing SDK session still targets loopback. 4. Maintain an allowlist of approved OpenD host identities rather than accepting an arbitrary hostname for live operations. 5. Resolve and validate hostnames carefully to reduce DNS rebinding and multi-address resolution risks. Validate every resolved address and pin the destination during connection establishment. 6. Document that `--confirm` confirms financial mutation but does not establish trust in a remote OpenD endpoint. 7. Keep password values out of logs and exception messages. Continue using an environment variable or, preferably, integrate an operating-system credential store with short-lived retrieval. 8. Add automated tests proving that real trading is rejected for non-loopback hosts unless the dedicated secure-remote override is explicitly enabled. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup_check.py:24
Finding
Setup check exposes raw financial account and card identifiers on standard output<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup_check.py`, lines 24-33 **Vulnerability Type**: Excessive disclosure of sensitive financial identifiers **Risk Level**: Medium ### Vulnerable Code ```python def show_accounts(ctx, api): """Show available trading accounts.""" ret, data = ctx.get_acc_list() if ret != api.RET_OK: print(f"\n⚠️ Could not fetch trading accounts: {data}") return if data.empty: print("\n⚠️ No trading accounts returned by OpenD.") return cols = ["acc_id", "trd_env", "acc_type", "card_num", "uni_card_num", "sim_acc_type"] available = [column for column in cols if column in data.columns] print("\nTrading accounts") print("-" * 60) print((data[available] if available else data).to_string(index=False)) ``` ### Technical Analysis The setup check unconditionally requests the account list and prints `acc_id`, `card_num`, and `uni_card_num` when those columns are available. If none of the expected columns are found, it prints the entire returned data frame, which could expose additional SDK fields not anticipated by the script. Account discovery is part of the documented setup-check functionality. However, showing complete card and universal-card identifiers is not necessary to establish connectivity or verify simulated account access. Printing these values violates data-minimization principles and exposes them to terminal history capture, CI logs, agent transcripts, support bundles, screen sharing, and other stdout collectors. The issue does not independently grant trading access, but the disclosed identifiers can improve account targeting and may be useful when combined with credential theft or social engineering. ### Attack Path 1. A user or automated agent runs `python3 scripts/setup_check.py` as recommended by the setup guide. 2. The script connects to the configured OpenD service and invokes `get_acc_list`. 3. Returned account, card, and univer ...[truncated 969 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `card_num` and `uni_card_num` from the default output. 2. Mask account identifiers unless the user explicitly requests full values: ```python def mask_identifier(value): text = str(value) if len(text) <= 4: return "*" * len(text) return "*" * (len(text) - 4) + text[-4:] ``` 3. Default to a minimal status summary, such as the number of discovered simulated and real accounts, account type, and trading environment. 4. Add an explicit option such as `--show-account-identifiers` for workflows that genuinely need account selection. Display a warning before emitting sensitive fields. 5. Never fall back to printing the complete data frame. Use a fixed allowlist of safe columns and report unknown columns only by name: ```python safe_columns = ["trd_env", "acc_type", "sim_acc_type"] available = [column for column in safe_columns if column in data.columns] print(data[available].to_string(index=False)) ``` 6. When an identifier is required for subsequent commands, show only a masked value plus the account index, allowing users to select accounts without revealing full identifiers. 7. Document that diagnostic output may contain financial metadata and should not be pasted into public issues or retained in shared logs. 8. Add tests asserting that default output never contains raw `card_num`, `uni_card_num`, or full account identifiers. ]]>
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (3)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill invokes scripts that rely on environment variables, including a live-trading unlock password, but the manifest does not declare any explicit tool scope or permission boundary for environment access. In an agent setting, undeclared env access can expose secrets or enable unintended use of sensitive credentials, which is especially risky here because the skill can place real stock orders.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
)

    try:
        return getattr(api.TrdMarket, resolved_market)
    except AttributeError as exc:
        raise ValueError(f"Trade market '{resolved_market}' is not supported by the SDK.") from exc
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
"K_YEAR",
    ]
    ktype_map = {
        name: getattr(api.KLType, name)
        for name in ktype_names
        if hasattr(api.KLType, name)
    }
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Static analysis

No suspicious patterns detected.