Back to skill

Security audit

SnapTrade

Security checks for vulnerabilities and agentic risk

Overview

This skill is a real SnapTrade portfolio helper, but it also enables live trading, persistent scheduled financial reports, and local credential storage with insufficient guardrails.

Review carefully before installing. Use it only if you intentionally want SnapTrade scripts that can store brokerage credentials locally, read account data, create scheduled reports, and potentially place live trades. Avoid enabling trading scope or cron-based WhatsApp delivery unless you have verified the exact account, destination, permissions, and removal steps.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • 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 (6)

T06 · System Persistence

Error
Location
SKILL.md:111
Finding
Recurring Cron Job Creates Cross-Session Persistence<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:111-113` **Vulnerability Type**: Scheduled-task persistence **Risk Level**: Critical ### Vulnerable Code ```markdown ### 4) Schedule daily report Use cron to call `snaptrade_total.py`, format a concise WhatsApp message, then send it to the user. Only the total value is required. ``` ### Technical Analysis The Skill explicitly instructs the user or Agent to create a cron job that repeatedly executes `snaptrade_total.py`. This scheduled execution survives the original Skill run and continues accessing financial account information in future sessions. The instruction does not require confirmation immediately before creating the task, establish an expiration date, restrict the reporting destination, or provide instructions for inspecting and removing the persistent job. ### Attack Path 1. The Agent loads and follows the workflow in `SKILL.md`. 2. A cron entry is created to run `snaptrade_total.py` daily. 3. Each execution loads persistent SnapTrade credentials and retrieves brokerage balances. 4. The resulting financial total is formatted and sent through WhatsApp. 5. The task continues running after the original interaction ends and may remain active until manually discovered and removed. ### Impact Assessment The scheduled job obtains recurring access to the user's brokerage portfolio information using stored SnapTrade credentials. Its scope includes repeated retrieval of balances and recurring disclosure of portfolio totals to an external messaging destination. An incorrectly configured, forgotten, or unauthorized schedule could expose financial information indefinitely. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Do not direct an Agent to create scheduled tasks automatically. - Require explicit, informed user consent immediately before scheduler creation. - Display the exact command, frequency, execution identity, credential source, and reporting destination. - Prefer a scheduler entry with a defined expiration date or bounded execution count. - Avoid embedding secrets in cron commands or environment variables. - Record the created job identifier and provide verified inspection and removal commands. - Require separate confirmation before sending financial information through WhatsApp or another external channel. - Default to an on-demand report when persistent scheduling is not strictly necessary. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/snaptrade_common.py:24
Finding
Credential File Permissions Are Applied After Writing and Fail Open<![CDATA[ ## Vulnerability Details **File Location**: `scripts/snaptrade_common.py:24-31` **Vulnerability Type**: Insecure storage of API credentials **Risk Level**: High ### Vulnerable Code ```python def save_config(data: Dict[str, Any], path: str = DEFAULT_CONFIG_PATH) -> None: p = Path(path) p.parent.mkdir(parents=True, exist_ok=True) p.write_text(json.dumps(data, indent=2)) try: os.chmod(p, 0o600) except Exception: pass ``` ### Technical Analysis The configuration file can contain `client_id`, `consumer_key`, `user_id`, and `user_secret`. The code writes these values before applying mode `0600`. For a newly created file, its initial permissions therefore depend on the process umask. For an existing file, writing it does not first verify that its current permissions are restrictive. Any failure from `os.chmod` is silently ignored. The program can consequently continue operating while the credential file remains readable by unintended local users or processes. Writing directly to the destination also lacks atomic replacement and can expose a partially written file if execution is interrupted. ### Attack Path 1. The program runs under a permissive umask, or the destination already has permissive permissions. 2. `p.write_text(...)` writes SnapTrade credentials to the file. 3. The subsequent `chmod` operation fails because of ownership, filesystem, or permission conditions. 4. The exception is suppressed without alerting the user. 5. Another local user or compromised process reads the exposed SnapTrade credentials. 6. The credentials are used to access the associated SnapTrade user and connected brokerage data, subject to the permissions granted to that integration. ### Impact Assessment Successful exploitation can disclose the SnapTrade consumer key and user secret. These credentials may permit unauthorized access to portfolio information and other API operations available to the configured integration. The immediat ...[truncated 186 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create new credential files with mode `0600` at creation time, such as through `os.open` with explicit flags and permissions. - Write through a securely created temporary file in the same directory, flush and synchronize it, set verified permissions, and atomically replace the destination. - Verify the final file owner and permission bits before returning success. - Fail closed if secure permissions cannot be established; do not suppress permission errors. - Create the parent secrets directory with restrictive permissions such as `0700`, and verify existing directory permissions. - Consider using an operating-system secret manager instead of a plaintext JSON file. - Never include credential values in exception messages or logs. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/snaptrade_reconnect.py:37
Finding
Reconnect Workflow Selects Unintended Connections and Requests Trading Scope<![CDATA[ ## Vulnerability Details **File Location**: `scripts/snaptrade_reconnect.py:37-57` **Vulnerability Type**: Excessive permissions and unsafe authorization selection **Risk Level**: High ### Vulnerable Code ```python # If a brokerage name was provided, try to match it from all connections target = None if len(sys.argv) > 1: wanted = sys.argv[1].lower() for d in connections: if d["name"] and wanted in d["name"].lower(): target = d break if target is None: # fallback to first disabled, else first connection target = disabled[0] if disabled else connections[0] resp = client.authentication.login_snap_trade_user( user_id=cfg["user_id"], user_secret=cfg["user_secret"], reconnect=target["id"], connection_type="trade", connection_portal_version="v4", immediate_redirect=False, show_close_button=True, ) ``` ### Technical Analysis The reconnect command searches all connections rather than limiting selection to disabled connections. It uses substring matching and accepts the first match. If no requested match is found, it silently falls back to the first disabled connection or, if none are disabled, the first active connection. The generated portal also requests `connection_type="trade"`, even though portfolio reporting only requires read access and the initial connection workflow uses read scope. This violates least privilege and can lead the user to authorize trading capability while intending only to restore portfolio connectivity. ### Attack Path 1. The script is invoked without a brokerage name, with an ambiguous substring, or with a name that does not match. 2. The selection logic silently chooses the first disabled or active connection. 3. The script generates a portal URL for that potentially unintended connection. 4. The portal requests trading scope rather than read-only scope. 5. The user ope ...[truncated 634 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Restrict reconnect candidates to connections explicitly marked as disabled. - Abort when no disabled connection exists rather than falling back to an active connection. - Require an exact connection identifier or exact normalized brokerage-name match. - Reject ambiguous matches and show the candidate list for explicit selection. - Display the selected brokerage, connection identifier, and requested permission scope before generating the URL. - Default to `connection_type="read"` for portfolio reporting. - Put trading authorization in a separate, clearly named workflow that requires explicit confirmation. - Revalidate the selected connection immediately before requesting the portal URL. ]]>

other

Warning
Location
scripts/snaptrade_accounts.py:12
Finding
Brokerage Account Numbers Are Disclosed Through Standard Output<![CDATA[ ## Vulnerability Details **File Location**: `scripts/snaptrade_accounts.py:12-30` **Vulnerability Type**: Sensitive financial information exposure **Risk Level**: Medium ### Vulnerable Code ```python out = [] for a in accounts: if isinstance(a, dict): out.append({ 'id': a.get('id'), 'name': a.get('name'), 'number': a.get('number') or a.get('account_number'), 'type': a.get('type'), 'institution_name': a.get('institution_name') or a.get('brokerage_name') }) else: out.append({ 'id': getattr(a, 'id', None), 'name': getattr(a, 'name', None), 'number': getattr(a, 'number', None), 'type': getattr(a, 'type', None), 'institution_name': getattr(a, 'institution_name', None) }) print(json.dumps(out, indent=2)) ``` ### Technical Analysis The account-listing script includes the complete brokerage account number in its output. Standard output may be retained in terminal history, Agent transcripts, orchestration logs, scheduled-task output, monitoring systems, or support diagnostics. Full account numbers are not required for the documented portfolio-total workflow. Returning them by default violates data minimization and unnecessarily increases the sensitivity of every system that captures the command output. ### Attack Path 1. A user or Agent invokes `snaptrade_accounts.py`. 2. SnapTrade returns account metadata containing an account number. 3. The script copies the complete number into the output object. 4. The JSON is printed to standard output. 5. A transcript, logger, scheduler, or monitoring service retains the output. 6. A person with access to those records obtains the financial identifier and may use it for targeted phishing, impersonation, or account-recovery abuse. ### Impact Assessment The direct impact is disclos ...[truncated 245 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove account numbers from default output. - When an account must be distinguished, return only a masked suffix such as `****1234`. - Require an explicit option and user confirmation before revealing a complete number. - Keep machine-facing account IDs separate from human-readable financial identifiers. - Document that command output may contain sensitive data and must not be logged. - Add automated tests confirming that full account numbers never appear in ordinary output. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/snaptrade_order.py:9
Finding
Trade Orders Are Submitted Without Adequate Numeric Validation or Confirmation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/snaptrade_order.py:9-48` **Vulnerability Type**: Unsafe financial transaction handling **Risk Level**: High ### Vulnerable Code ```python def parse_args(): p = argparse.ArgumentParser(description="Place a SnapTrade order and optionally monitor for fill.") p.add_argument("action", choices=["buy", "sell"], help="buy or sell") p.add_argument("symbol", help="Ticker symbol, e.g., AAPL or VXC.TO") p.add_argument("units", type=float, help="Number of units (shares). Use whole numbers when required by broker.") p.add_argument("--account-id", required=True, help="SnapTrade account id") p.add_argument("--order-type", default="market", choices=["market", "limit"], help="market (default) or limit") p.add_argument("--limit-price", type=float, help="Limit price (required if order-type=limit)") p.add_argument("--tif", default="Day", choices=["Day", "GTC", "IOC", "FOK"], help="Time in force") p.add_argument("--watch", action="store_true", help="Monitor for fill after placing") p.add_argument("--watch-interval", type=int, default=10, help="Seconds between checks (default 10)") p.add_argument("--watch-seconds", type=int, default=120, help="Max seconds to watch (default 120)") return p.parse_args() def normalize_order_type(o): return "Market" if o.lower() == "market" else "Limit" def main(): args = parse_args() if args.order_type == "limit" and args.limit_price is None: raise SystemExit("--limit-price is required for limit orders") cfg = load_config() client = get_client(cfg) user_id = cfg["user_id"] user_secret = cfg["user_secret"] resp = client.trading.place_force_order( user_id=user_id, user_secret=user_secret, account_id=args.account_id, action=args.action.upper(), order_type=normalize_order_type(args.order_type), time_in_force=args.tif, symbol=args.symbol, ...[truncated 1743 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Reject quantities and prices unless `math.isfinite(value)` is true and the value is strictly positive. - Use `decimal.Decimal` rather than binary floating point for financial values. - Enforce broker-supported quantity and price precision. - Define reasonable configurable upper limits for quantity and estimated notional value. - Normalize and validate ticker symbols. - Verify that the account belongs to the configured user and is authorized for trading. - Add an order-preview stage showing account, action, symbol, units, order type, time in force, price, and estimated notional value. - Require explicit confirmation after the preview and immediately before submission. - For Agent-driven execution, use a structured confirmation token tied to the exact previewed order so parameters cannot change between approval and placement. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/snaptrade_total.py:203
Finding
Balances in Different Currencies Are Added Without Conversion<![CDATA[ ## Vulnerability Details **File Location**: `scripts/snaptrade_total.py:203-214` **Vulnerability Type**: Incorrect financial aggregation **Risk Level**: Medium ### Vulnerable Code ```python if amount is None: continue total_value += float(amount) if cur and not currency: currency = cur currency = currency or cfg.get("currency", "") # print as JSON for easy parsing result = {"total_value": round(total_value, 2), "currency": currency} if disabled_connections: result["disabled_connections"] = sorted(set(disabled_connections)) print(result) ``` The behavior is also acknowledged in `SKILL.md:121-122`: ```markdown - If multiple currencies are present, the script uses the first currency encountered. ``` ### Technical Analysis The script adds every account's numeric balance to one total without checking whether `cur` matches the currency already selected. The output is then labeled with the first currency encountered. No exchange-rate conversion is performed. For example, a balance of `1000 USD` and a balance of `1000 CAD` produce an output of `2000` labeled either USD or CAD depending on account order. The result is not a valid monetary total and may vary semantically when account enumeration order changes. ### Attack Path 1. The SnapTrade user has non-closed accounts denominated in multiple currencies. 2. The script obtains each account's balance and currency. 3. Raw numeric amounts are added without conversion. 4. Only the first observed currency is retained as the output label. 5. The invalid aggregate is emitted as a single portfolio value. 6. A scheduled report or downstream consumer treats the value as an accurate single-currency total and may use it for financial decisions. ### Impact Assessment The vulnerability does not grant additional privileges, but it compromises the integrity of financial reporting. Users may receive materially incorrect portfolio valuation ...[truncated 181 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Group balances by currency and return a separate total for each currency by default. - If a consolidated total is required, convert every balance into an explicitly selected base currency. - Use a trusted exchange-rate source and include the rate timestamp and source in the report. - Use `decimal.Decimal` for conversion and aggregation. - Reject or clearly flag stale or unavailable exchange rates. - Never label an unconverted mixed-currency sum as a single currency. - Add tests covering multiple currencies, missing currency identifiers, account-order changes, and conversion rounding. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (14)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The documented ability to watch and track brokerage orders is omitted from the stated purpose. In a financial context, undisclosed order-status polling expands the operational scope from passive reporting into active trade workflow support, which can expose sensitive trading activity and enable actions the user did not reasonably expect.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The documented ability to watch and track brokerage orders is omitted from the stated purpose. In a financial context, undisclosed order-status polling expands the operational scope from passive reporting into active trade workflow support, which can expose sensitive trading activity and enable actions the user did not reasonably expect.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The skill presents itself as generating portfolio reports, yet the primary workflow also includes placing buy/sell orders. This mismatch is particularly dangerous in an investment context because a user may grant or trigger the skill expecting read-only reporting while the skill supports irreversible market activity.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The documented ability to place securities orders is not justified by the stated purpose of connectivity and portfolio summaries. Unnecessary high-risk capability increases blast radius: if the skill is selected for benign account-linking or reporting tasks, it still exposes trade execution paths that could be misused or accidentally invoked.

Missing User Warnings

High
Confidence
97% confidence
Finding
The documentation describes live buy/sell orders with a default market order and no explicit warning about financial loss, slippage, or irreversibility. Defaulting to market orders in particular can produce unintended execution prices, making accidental or misunderstood use especially harmful.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
This script enables live order placement and post-trade monitoring, which materially exceeds the skill's declared scope of brokerage connectivity and portfolio reporting. Scope expansion into trading is dangerous because an agent or user expecting read-oriented account access could instead trigger irreversible financial transactions.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The code directly calls a force-order trading API to buy or sell securities, but the stated skill purpose only covers account connectivity, portal links, registration, and portfolio summaries. In this context, live trading capability is unjustified and increases the risk of unauthorized or unexpected financial actions through the agent.

Missing User Warnings

High
Confidence
99% confidence
Finding
Order submission occurs immediately from command-line inputs with no confirmation prompt, no human-in-the-loop warning, and no secondary validation for a destructive financial action. A mistaken symbol, units value, account selection, or automated invocation could therefore result in real trades and direct monetary loss.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill declares no explicit tool scope or permissions even though its documented workflows require shell execution, local file reads/writes, and access to secrets. In a financial-account skill, missing scope boundaries materially increases the chance the agent can use broader capabilities than intended, including handling credentials and executing trading-related scripts without clear confinement.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The workflow instructs sending portfolio values via WhatsApp without warning that sensitive financial data is being transmitted through a third-party messaging platform. Even if only total value is sent, this can expose private financial information to external services, compromised devices, or misdirected recipients.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- Request signing is handled by the SDK via `request_after_hook` using `consumer_key`.
- The total is computed by summing each account’s `account.balance.total` from holdings; this avoids errors when accounts are added or removed.
- If multiple currencies are present, the script uses the first currency encountered.
- Keep secrets in the local config file with `chmod 600` permissions.
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This function writes credential-bearing configuration data to disk silently, which increases the chance that API secrets are persisted without the operator fully realizing it. In a brokerage-integration skill handling SnapTrade client credentials, undisclosed local secret storage raises the risk of credential exposure through backups, shared workspaces, misconfigured permissions, or later file disclosure.

Tainted flow: 'data' from pathlib.Path.read_text (line 20, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
def save_config(data: Dict[str, Any], path: str = DEFAULT_CONFIG_PATH) -> None:
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(json.dumps(data, indent=2))
    try:
        os.chmod(p, 0o600)
    except Exception:
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The code persists `user_secret` directly into the local config via `save_config(cfg)` without any indication of encryption, access control hardening, or user disclosure. Because this secret is later used to authenticate SnapTrade user actions, storing it in plaintext or weakly protected config increases the risk of credential theft from disk, logs, backups, or multi-user environments.

Static analysis

No suspicious patterns detected.