Back to skill

Security audit

IBKR Investing with Permission Gate

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed IBKR trading skill, but it needs Review because live-order execution relies on agent-readable local tokens and mutable setup components that handle broker credentials.

Install only if you are comfortable reviewing a trading-capable skill. Use paper mode first, pin and verify the Docker gateway and Python dependencies before supplying IBKR credentials, keep symbol and notional caps tight, avoid broad scheduled execution, and treat any live-mode use as capable of placing real broker orders.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • 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
Findings (8)

T03 · Remote Payload Retrieval and Execution

Error
Location
README.md:21
Finding

Unpinned Remote Installer Is Piped Directly into a Privileged Shell

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Error
Location
requirements.txt:1
Finding

Unpinned Third-Party Gateway and Python Dependencies Create Supply-Chain Exposure

Content
View full analysis
=1.0.1 pandas>=2.0 numpy>=1.24 ``` ### Technical Analysis The gateway repository is cloned from its mutable default branch without a pinned commit, release tag, or integrity verification. Code and container configuration fetched at installation time may therefore differ from what was reviewed. This is especially sensitive because the gateway receives the user's IBKR username and password and is deliberately configured with `READ_ONLY_API=no`. The Python dependencies likewise use open-ended version ranges and no hashes. A future installation may retrieve materially different code from the package index. Compromise of an upstream package, maintainer account, release process, or transitive dependency can result in arbitrary code executing in the Aeon environment. The third-party names observed in the project are not themselves evidence of typosquatting or malicious intent. The vulnerability is the absence of reproducible version and integrity controls around components with access to broker operations and sensitive account data. ### Attack Path 1. An upstream repository or package release is compromised, or a future release introduces malicious code. 2. A user follows the README and clones the current default branch or runs `pip install` using the open-ended requirements. 3. The unreviewed d ...[truncated 873 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ibkr_execute_trade.py:54
Finding

Human Confirmation Is an Instructional Convention Rather Than an Enforced Authorization Boundary

Content
View full analysis
tuple[str, str]: _ensure_dirs() pid = new_id() token = _new_token() record = { "id": pid, "confirmation_token": token, "kind": kind, "payload": payload, "created_at": now_iso(), "expires_at_epoch": int(time.time()) + ttl_seconds, } path = _path(pid) path.write_text(json.dumps(record, indent=2)) os.chmod(path, 0o600) return pid, token ``` Execution verifies possession of that token, but not an independently recorded user confirmation: ```python p.add_argument("--id", required=True, help="Proposal id from ibkr_propose_trade.py") p.add_argument( "--confirmation-token", required=True, help="Read this from ~/.aeon/ibkr/pending/.json — DO NOT fabricate.", ) args = p.parse_args() try: record = load_pending(args.id) validate_token(record, args.confirmation_token) except PendingError as e: print(f"REFUSED: {e}", file=sys.stderr) return 3 # ... ib = connect(readonly=False) try: contract = _build_contract(payload["contract"]) qualified = ib.qualifyContracts(contract) # ... trade = ib.placeOrder(contract, order) ``` ### Technical Analysis The Skill claims that an order cannot execute until the user explicitly replies `YES `. The implementation, however, does not represent or validate that user action. The confirmation token exists before the user confirms and is stored in a location readable by the same agent context that can invoke the executor. Consequently, the token proves only access to the pending file. It does not prove that a h ...[truncated 1338 chars]
Remediation
View remediation
` event. - Require the executor to verify a short-lived, signed approval assertion containing the proposal ID, payload hash, user identity, timestamp, and session identity. - Ensure scheduled contexts cannot request or access approval assertions. - Bind approval to a hash of the complete contract and order payload so it cannot authorize altered parameters. - Do not pass bearer tokens on command lines. Use a protected file descriptor, standard input, or an authenticated local IPC channel. - Preserve the existing expiry, symbol allow-list, and notional checks as defense in depth rather than treating them as substitutes for confirmation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/_pending.py:45
Finding

Unsanitized Proposal IDs Permit Path Traversal Outside the Pending Directory

Content
View full analysis
Path: return PENDING_DIR / f"{pid}.json" ``` The path is used without validating that it remains under `PENDING_DIR`: ```python def load_pending(pid: str) -> dict: path = _path(pid) if not path.exists(): raise PendingError(f"No pending proposal with id '{pid}' (expired or never existed)") record = json.loads(path.read_text()) if int(time.time()) > record["expires_at_epoch"]: try: path.unlink() except FileNotFoundError: pass raise PendingError(f"Proposal '{pid}' has expired") return record ``` ```python def delete_pending(pid: str) -> None: path = _path(pid) if path.exists(): path.unlink() ``` The cancellation command accepts the ID directly: ```python p = argparse.ArgumentParser() p.add_argument("--id", required=True) args = p.parse_args() try: record = load_pending(args.id) except PendingError as e: print(str(e), file=sys.stderr) return 1 delete_pending(args.id) ``` ### Technical Analysis `--id` is not restricted to the generated eight-character hexadecimal format. Components such as `../` are preserved when constructing the path. For example, an ID like `../../target` causes the program to access a path equivalent to `~/.aeon/target.json`, rather than a file inside `~/.aeon/ibkr/pending`. The target generally must be JSON containing an `expires_at_epoch` field to pass `load_pending`. An expired JSON record may be deleted during loading, while a valid unexpired record may be deleted by the cancellation path. The execution path could also consume an externally located, attacker-crafted pending record if it has the required schema and matching token. The forced `.json` suffix l ...[truncated 1096 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/_guardrails.py:50
Finding

Daily Notional Cap Check and Accounting Are Non-Atomic

Content
View full analysis
None: cap = _max_daily() used = today_notional_usd() if used + notional_usd > cap: raise GuardrailError( f"Today's executed notional is ${used:.2f}; adding ${notional_usd:.2f} would exceed " f"IBKR_MAX_DAILY_NOTIONAL_USD=${cap:.2f}. Try again tomorrow or raise the cap." ) def check_all(symbol: str, notional_usd: float) -> None: check_symbol(symbol) check_notional(notional_usd) check_daily_room(notional_usd) ``` The executor checks the cap, places the order, and only later appends usage: ```python try: check_all(payload["symbol"], float(payload["notional_usd"])) except GuardrailError as e: # ... return 3 ib = connect(readonly=False) try: # ... trade = ib.placeOrder(contract, order) ib.sleep(args.wait_fill_seconds) # ... append_notional_log({ "id": record["id"], "ord_id": ord_id, "symbol": payload["symbol"], "side": payload["side"], "notional_usd": actual_notional, }) ``` ### Technical Analysis The daily-cap operation is implemented as an unlocked read-check-place-write sequence. Two executor processes can read the same `used` value before either appends to `notional_log.jsonl`. Both then pass validation and place their orders, even if the combined notional exceeds the configured daily maximum. Appending each JSON line is not equivalent to locking the preceding cap decision. There is also no broker-side reconciliation to account for orders submitted by concurrent Skill processes or other IBKR clients. ### Attack Path 1. The account has enough remaining daily capacity for one proposal but not two. 2. Two valid pending propos ...[truncated 591 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ibkr_propose_trade.py:27
Finding

Market Orders Can Exceed Notional Caps Because Checks Use a Stale Reference Price

Content
View full analysis
float: """Use the user's limit if present; else delayed-frozen last/close.""" if ord_type == "LMT" and lmt_price: return float(lmt_price) ib.reqMarketDataType(4) # delayed-frozen t = ib.reqMktData(contract, "", False, False) ib.sleep(2.5) px = t.last or t.close or t.bid or t.ask ib.cancelMktData(contract) if not px or px != px: # NaN check raise RuntimeError(f"Could not fetch reference price for {contract.symbol}") return float(px) ``` ```python if args.shares is not None: shares = float(args.shares) else: shares = float(args.quote_sz) / ref_px # ... notional = shares * ref_px try: check_all(args.symbol, notional) except GuardrailError as e: print(f"REFUSED: {e}", file=sys.stderr) return 3 ``` At execution, the same stored notional is rechecked before placing an unbounded market order: ```python try: check_all(payload["symbol"], float(payload["notional_usd"])) except GuardrailError as e: # ... return 3 # ... trade = ib.placeOrder(contract, order) ``` ### Technical Analysis For `MKT` orders, share quantity and notional are calculated from delayed-frozen market data at proposal time. Pending proposals remain valid for ten minutes. At execution, the executor does not obtain a fresh quote or calculate worst-case exposure; it only rechecks the stored proposal-time notional. A market buy can therefore fill materially above the reference price because of price movement, stale delayed data, a wide spread, a trading halt reopening, or limited liquidity. The real notional can exceed both per-trade and remaining daily caps even though the store ...[truncated 803 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/_audit.py:21
Finding

Sensitive Portfolio and Trade Records Are Written Without Enforced Restrictive Permissions

Content
View full analysis
None: try: ROOT.mkdir(parents=True, exist_ok=True) record = { "ts_iso": datetime.now(timezone.utc).isoformat(), "ts_epoch": int(time.time()), "event": event, **fields, } with AUDIT_LOG.open("a") as f: f.write(json.dumps(record, default=str) + "\n") ``` The notional ledger is handled similarly: ```python def append_notional_log(entry: dict) -> None: _ensure_dirs() entry = dict(entry) entry.setdefault("ts_iso", now_iso()) entry.setdefault("date_utc", datetime.now(timezone.utc).strftime("%Y-%m-%d")) with NOTIONAL_LOG.open("a") as f: f.write(json.dumps(entry) + "\n") ``` Snapshots containing NAV, buying power, positions, average costs, values, and P&L are also written without an explicit mode: ```python if not args.no_write: SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True) (SNAPSHOT_DIR / f"{today_str}.json").write_text( json.dumps(snapshot, indent=2, default=str) ) ``` ### Technical Analysis Only pending proposal files and drawdown state are explicitly changed to mode `0600`. Audit logs, notional logs, and snapshots depend on the process umask. Under a permissive umask, they may be readable by other local users. This contradicts the README statement that files under `~/.aeon/ibkr/` are `0600`. Existing files are also not repaired if permissions become too broad. The state directories are created without an explicit restrictive mode, so directory traversal permissions li ...[truncated 798 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/ibkr_gateway_status.py:39
Finding

Routine Diagnostic and Balance Commands Disclose Full Broker Account Identifiers

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (20)

Chaining Abuse

High
Category
Tool Misuse
Confidence
98% confidence
Finding

The | sh construct is the dangerous sink that turns remote content into immediate shell execution, enabling command injection through any compromise of the fetched script or transport path. In this skill's context, host compromise could expose IBKR credentials, .env secrets, and trading infrastructure, amplifying the impact beyond a generic workstation setup issue.

Content

Scanner excerpt · README.md (reported line 25)May include surrounding context.

1. Install Docker

bash
curl -fsSL https://get.docker.com | sh
docker --version

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The description claims a full IBKR investing skill with human-in-the-loop trade confirmation and support for balances, quotes, trading, DCA, and portfolio snapshots. This code chunk instead only tests TCP connectivity and IB API handshake status for the gateway, printing troubleshooting guidance. While this could be a supporting utility within such a skill, the actual behavior of the provided code chunk does not match the declared purpose as stated. There is no trade logic, no confirmation gate logic, and none of the user-facing investing functions described.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding

The declared description presents a broad IBKR investing/trading skill centered on balances, quotes, portfolio actions, DCA, and especially trade execution with a strict confirmation gate. The supplied code chunk instead has a narrower and materially different purpose: retrieving historical price bars and optionally calculating SMA/RSI overlays. While market-data lookup is adjacent to 'look up quotes,' historical candles with indicators and generated trading signals are a distinct capability not stated in the description. More importantly, the code does not implement any of the signature declared behaviors like human-in-the-loop trade confirmation, trading mode controls, order placement, balance retrieval, or portfolio snapshots. This is a meaningful description-to-behavior mismatch for the provided chunk.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The description presents a broad IBKR trading skill with human-in-the-loop confirmation and explicit safeguards around live trading. The supplied code, however, only implements the daily snapshot/reporting portion: it reads account/position data, fetches delayed quotes and historical closes, computes changes, writes a local JSON snapshot, and prints a summary. There is no order placement, no confirmation gate, no DCA logic, and no explicit handling of paper/live mode selection in this file. While the snapshot behavior is mentioned in the declared description, the declared purpose substantially overstates the code chunk’s capabilities, and the code also performs local file persistence that is not disclosed in the description. Therefore the description does not accurately represent what this specific code chunk actually does.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · README.md (reported line 32)May include surrounding context.

2. Pull and configure IB Gateway Docker

bash
mkdir -p ~/.aeon/ibkr-runtime && cd ~/.aeon/ibkr-runtime
git clone https://github.com/gnzsnz/ib-gateway-docker.git
cd ib-gateway-docker

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 113)May include surrounding context.

export IBKR_MAX_DAILY_NOTIONAL_USD=1000

text

`chmod 600 ~/.aeon/.env`. Then either restart aeon or `source ~/.aeon/.env` if running interactively.

## Going live

Unsafe Defaults

Medium
Category
Tool Misuse
Confidence
70% confidence
Finding

Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Content

Scanner excerpt · README.md (reported line 143)May include surrounding context.

dca_dip_state.json # drawdown-trigger state

text

Files are 0600 so the confirmation token is not world-readable. Pending proposals expire automatically after 10 minutes.

## What the user types in chat

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
97% confidence
Finding

The skill explicitly instructs use of powerful capabilities including shell, file_read, file_write, and environment-dependent behavior, but it declares no tool scope or permission boundary. In a trading skill, missing least-privilege constraints increases the blast radius of prompt injection, misrouting, or operator error, because the agent can read local files, persist state, and invoke execution-oriented scripts without an explicit allowlist.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The activation criteria are broad enough to match generic finance-related requests such as balances, quotes, investing, DCA, and portfolio snapshots. In a skill with shell and file capabilities and potential trade execution, over-broad triggering raises the chance of accidental invocation, context confusion, or prompt-injection-assisted routing into a higher-risk toolchain than the user intended.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The skill first requires using the printed absolute pending-file path verbatim, then later tells the operator to read a guessed fixed path under ~/.aeon/ibkr/pending/.json. This inconsistency can cause the agent to read the wrong file, fail open on multi-user systems, or use stale/cross-session confirmation data, weakening the trade confirmation control in a sensitive trading workflow.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
79% confidence
Finding

The skill directs persistent storage of strategy state, snapshots, and audit data in the user's home directory across sessions. Persistence is not automatically unsafe, but in a financial context it creates privacy and integrity risk: stored portfolio history, trigger state, and audit trails may be exposed to other tools, reused out of context, or tampered with to influence future trading recommendations.

Content

Scanner excerpt · SKILL.md (reported line 148)May include surrounding context.

md
1. Decide what observations the strategy needs. Read from `ibkr_snapshot.py` (current state), `~/.aeon/ibkr/snapshots/*.json` (history), `~/.aeon/ibkr/audit.jsonl` (lifecycle events), and `ibkr_get_candles.py --indicators` (signal).
2. Define the trigger condition. Either a scheduled task that runs daily and checks the condition, or a passive observation surfaced in the snapshot.
3. The trigger NEVER places a trade itself — it produces a recommendation that the agent (or scheduled task) can pass to `ibkr_propose_trade.py`. User still YES-confirms each fill.
4. Write any required state to `~/.aeon/ibkr/<strategy_name>_state.json` (mode 600).
5. Append `audit.jsonl` events for transparency.

The `ibkr_dca_dip.py` script is the canonical example of this shape. Follow it for new strategies.

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
75% 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.

Content

Scanner excerpt · scripts/ibkr_cancel_pending.py (reported line 4)May include surrounding context.

python
#!/usr/bin/env python3
"""Discard a pending IBKR proposal (the 'NO <id>' path).

No confirmation token required — cancelling a proposal before any IBKR order
has been placed is always safe.
"""
from __future__ import annotations

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

This script prints sensitive financial data including account identifiers, net liquidation value, cash, positions value, buying power, and P&L directly to stdout with no masking, consent prompt, or output-scope control. In an agent setting, stdout may be surfaced to chat logs, stored in run history, or exposed to other tooling, creating a real confidentiality risk even though the script is read-only.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script emits directional trading guidance such as 'consider buy' and 'consider sell' based solely on RSI thresholds, without any user-facing disclaimer or context about the signal's limitations. In an investing skill that can be used to inform or initiate trades, this increases the risk that users or downstream agents treat a simplistic indicator as actionable advice, leading to poor investment decisions.

Content

No source excerpt is available for this finding.

External Script Fetching

Low
Category
Supply Chain
Confidence
97% confidence
Finding

Piping a remote script from get.docker.com directly into sh executes code fetched over the network without prior verification or review. If the remote content, TLS trust chain, or delivery path is compromised, the user could run arbitrary code on the host, which is especially risky in a trading skill environment that also manages credentials and can place orders.

Content

Scanner excerpt · README.md (reported line 25)May include surrounding context.

1. Install Docker

bash
curl -fsSL https://get.docker.com | sh
docker --version

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
95% confidence
Finding

The dependency specification uses a lower-bound only (ib_async>=1.0.1), which permits installation of any future release, including unreviewed or breaking versions. In a trading-related skill, supply-chain compromise or an unexpected behavioral change in the brokerage client library could affect market data access or trade execution paths.

Content

Scanner excerpt · requirements.txt (reported line 1)May include surrounding context.

text
ib_async>=1.0.1
pandas>=2.0
numpy>=1.24

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
98% confidence
Finding

pandas>=2.0 is unpinned, so builds are not reproducible and may pull in future releases with undiscovered vulnerabilities or incompatible behavior. Although pandas is not typically a direct remote-execution vector in normal use, unreviewed upgrades in a financial workflow can still introduce reliability or security issues around data handling.

Content

Scanner excerpt · requirements.txt (reported line 2)May include surrounding context.

text
ib_async>=1.0.1
pandas>=2.0
numpy>=1.24

Unverifiable Dependency: pandas has 1 known advisory(ies) (CVE-2020-13091 (** DISPUTED ** pandas through 1.0.3 can unserialize and execute commands from an)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
98% confidence
Finding

numpy>=1.24 allows arbitrary newer versions to be installed, weakening reproducibility and increasing exposure to supply-chain or compatibility issues. In this investing skill, incorrect numerical behavior or a compromised package could affect calculations used for portfolio analysis or trade sizing.

Content

Scanner excerpt · requirements.txt (reported line 3)May include surrounding context.

text
ib_async>=1.0.1
pandas>=2.0
numpy>=1.24

Unverifiable Dependency: numpy has 16 known advisory(ies) (CVE-2014-1859 (Numpy arbitrary file write via symlink attack); CVE-2021-41495 (NumPy NULL Pointer Dereference); CVE-2021-33430 (NumPy Buffer Overflow (Disputed)) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
63% confidence
Finding

Because numpy is unpinned, dependency resolution could select a version with known security issues depending on environment, mirrors, or transitive constraints. While the manifest's minimum version excludes some older CVEs, the lack of a fixed resolved version leaves security posture unverifiable and is undesirable in a finance-related skill where numerical integrity matters.

Content

No source excerpt is available for this finding.