Back to skill

Security audit

Questrade

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed Questrade API skill, but it needs review because it can submit or cancel brokerage orders with partner credentials and its safeguards for financial write actions are weak.

Install only if you understand that this skill can access sensitive Questrade account data and, with partner-level credentials, may place or cancel real orders. Keep QUESTRADE_READ_ONLY=true unless you intentionally need trading access, avoid using --force for live accounts, protect the ~/.openclaw credential files, and prefer a pinned dependency set.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/questrade_cli.py:154
Finding
OAuth token files are written without explicitly restrictive permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/questrade_cli.py:154-155` and `scripts/questrade_cli.py:205-209` **Vulnerability Type**: Insecure storage of sensitive authentication tokens **Risk Level**: Medium ### Vulnerable Code ```python with open(CREDENTIALS_FILE, "w") as f: json.dump(config, f, indent=2) ``` ```python with open(TOKEN_CACHE_FILE, "w") as f: json.dump( {"access_token": access_token, "api_server": api_server, "expires_at": expires_at.isoformat()}, f, indent=2, ) ``` ### Technical Analysis The application persists both a rotating OAuth refresh token and a bearer access token using the default permissions derived from the process umask. It does not explicitly create these files with mode `0600`, restrict the parent directories to `0700`, or correct the permissions of existing files. On a system with a permissive umask or previously created broadly accessible files, another local account may be able to read the tokens. The files also are not updated atomically, which can leave partially written credential state if the process is interrupted. The refresh token has greater security significance because it can be exchanged for new access tokens. The access-token cache additionally stores the API server to which the bearer token will subsequently be sent. ### Attack Path 1. A user executes the Questrade CLI on a multi-user host under a permissive umask. 2. The CLI refreshes its OAuth credentials. 3. The rotated refresh token is written to `~/.openclaw/credentials/questrade.json`, and the bearer access token is written to `~/.openclaw/data/questrade-token-cache.json`. 4. The resulting permissions allow another local user or compromised process to read one or both files. 5. The attacker exchanges the refresh token or directly uses the cached bearer token against the Questrade API. 6. The attacker accesses the API resources authorized by the token. ### Impact Assessment A stolen personal to ...[truncated 492 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create credential and cache directories with mode `0700`. - Create token files atomically with mode `0600`, for example by using `os.open()` with `O_CREAT | O_WRONLY` and an explicit mode. - Apply `chmod(0o600)` to existing token files before reading or updating them. - Write updates to a securely created temporary file in the same directory, flush and synchronize it, and atomically replace the destination. - Reject symbolic links and verify that credential files are regular files owned by the current user. - Avoid copying the rotated refresh token into `os.environ`, because environment values may be exposed to debugging or process-inspection mechanisms. - Document the required local permissions and provide a migration step that repairs existing installations. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/questrade_cli.py:80
Finding
Trading protections fail open and can be bypassed with the force option<![CDATA[ ## Vulnerability Details **File Location**: `scripts/questrade_cli.py:80-82`, `scripts/questrade_cli.py:150-153`, `scripts/questrade_cli.py:550-552`, `scripts/questrade_cli.py:569-608`, and `scripts/questrade_cli.py:755-756` **Vulnerability Type**: Unsafe authorization default and bypassable transaction safeguards **Risk Level**: High ### Vulnerable Code The read-only configuration defaults to disabled: ```python def is_read_only() -> bool: if os.environ.get("QUESTRADE_READ_ONLY", "").lower() == "true": return True return _load_config().get("readOnly", False) ``` New credential files also explicitly receive a write-enabled default: ```python if "readOnly" not in config: config["readOnly"] = False ``` The order command only blocks trading when read-only mode has already been explicitly enabled: ```python if is_read_only(): print("🔒 Read-only mode is enabled — order placement is blocked.") print(" To allow trades, set QUESTRADE_READ_ONLY=false or remove") print(' "readOnly": true from ~/.openclaw/credentials/questrade.json') sys.exit(1) ``` Price warnings and final confirmation are skipped when `--force` is supplied: ```python if o_type in ("Limit", "StopLimit") and args.limit_price: if side == "Buy" and ask > 0 and args.limit_price > ask and not args.force: print(f"⚠️ Warning: Buy limit ${args.limit_price:.4f} is ABOVE current ask ${ask:.4f}") if input(" Proceed anyway? (y/n): ").strip().lower() != "y": print("Order cancelled.") return elif side == "Sell" and bid > 0 and args.limit_price < bid and not args.force: print(f"⚠️ Warning: Sell limit ${args.limit_price:.4f} is BELOW current bid ${bid:.4f}") if input(" Proceed anyway? (y/n): ").strip().lower() != "y": print("Order cancelled.") return ``` ```python if not args.force: if input(" Confirm order? (y/n): ").strip().lower() != "y": print("Order ...[truncated 2615 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Change read-only behavior to fail closed: ```python return _load_config().get("readOnly", True) ``` - Save `"readOnly": true` when creating or rotating a credential file unless write access was explicitly configured through a separate, protected mechanism. - Require explicit per-invocation write authorization rather than treating the absence of a read-only setting as authorization. - Remove `--force` from Agent-facing operation, or restrict it to a separately authenticated administrative workflow. - Require confirmation that includes the account, side, symbol, quantity, order type, limit or stop price, and estimated notional amount. - Add configurable account allowlists, symbol allowlists, quantity limits, and maximum order-notional limits. - Reject non-positive quantities and prices, and validate the required price fields for each order type. - Require confirmation before cancellation and display the order details before sending the delete request. - Consider requiring an out-of-band approval token for any live financial mutation. ]]>

T08 · Insecure Dependencies

Note
Location
requirements.txt:2
Finding
Third-party dependency is not reproducibly pinned<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt:2` **Vulnerability Type**: Unbounded dependency version and missing integrity verification **Risk Level**: Low ### Vulnerable Code ```text requests>=2.28.0 ``` ### Technical Analysis The dependency declaration accepts any current or future release of `requests` newer than version 2.28.0. No lock file or package hash is included. Consequently, separate installations of the same Skill version can resolve to different dependency versions. No evidence of typosquatting, dependency confusion, or a currently malicious package was found; `requests` is a legitimate dependency obtained through normal Python package resolution. The weakness is the lack of reproducibility and integrity enforcement. A future compromised, malicious, or incompatible release could be installed without a corresponding change to the audited project. ### Attack Path 1. An operator installs dependencies using `pip install -r requirements.txt`. 2. Package resolution selects the newest available version satisfying `requests>=2.28.0`. 3. A future compromised release, malicious package-index response, or incompatible version is selected. 4. Package installation or subsequent import executes or loads the affected dependency under the user's privileges. 5. Because the CLI handles OAuth tokens and authenticated API traffic, a compromised dependency could access credentials, alter requests, or transmit sensitive account data. ### Impact Assessment The dependency executes with the same operating-system privileges as the CLI and is directly involved in all authentication and API traffic. A compromised package could read the local token files, capture bearer and refresh tokens, modify Questrade transactions, or access any other data available to the invoking user. The practical likelihood is lower than the other findings because exploitation depends on a compromised or unsafe package release or package source; no such active com ...[truncated 54 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin `requests` to a specific reviewed version. - Generate and commit a lock file that includes all transitive dependencies. - Use package hashes and install with `pip --require-hashes`. - Install only from an explicitly configured trusted package index over TLS. - Run dependency vulnerability and provenance checks as part of release automation. - Regularly update the pinned version through a controlled review and testing process rather than accepting arbitrary future releases automatically. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
Findings (18)

Tainted flow: 'refresh_token' from os.environ.get (line 124, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
# Questrade docs show this as a GET with query params; POST with params= is
    # equivalent (params become the query string either way) and aligns with
    # RFC 6749 which recommends POST for token endpoints.
    resp = requests.post(
        login_url,
        params={"grant_type": "refresh_token", "refresh_token": refresh_token},
        timeout=15,
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Credential Access

High
Category
Privilege Escalation
Content
```

> **Important:** Questrade refresh tokens *rotate* — every time the token is
> used to obtain a new access token, Questrade issues a new refresh token.
> This script automatically saves the new refresh token back to the credentials
> file, so you only need to paste the initial token once.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```

> **Important:** Questrade refresh tokens *rotate* — every time the token is
> used to obtain a new access token, Questrade issues a new refresh token.
> This script automatically saves the new refresh token back to the credentials
> file, so you only need to paste the initial token once.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
## Safety Notes

- **Personal API tokens cannot place trades.** Questrade restricts order placement
  and cancellation (`POST`/`DELETE /accounts/{id}/orders`) to partner-level API
  access only. Attempting either with a personal token returns `403 Forbidden`.
- The `order` and `cancel-order` commands are included for completeness but will
  not work unless you have Questrade partner API access.
Confidence
83% confidence
Finding
The skill includes support for order cancellation and placement against brokerage endpoints, which are high-risk state-changing operations in a financial context. Although the documentation notes personal tokens cannot use these endpoints, the capability becomes dangerous if partner credentials are present, since prompt abuse or operator error could trigger destructive trading actions.

Credential Access

High
Category
Privilege Escalation
Content
- The `order` and `cancel-order` commands are included for completeness but will
  not work unless you have Questrade partner API access.
- The refresh token rotates on every use; do **not** share the credentials file.
- Access tokens expire in ~30 minutes; the script caches and refreshes them automatically.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
---

### DELETE /v1/accounts/{id}/orders/{orderId} *(partner access)*

Cancel an order. Returns updated order detail.
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
Delete the token cache file and retry — the CLI will refresh automatically:

```bash
rm ~/.openclaw/data/questrade-token-cache.json
```

If the refresh token itself is expired (7-day window), generate a new one from
Confidence
97% confidence
Finding
The explicit `rm ~/.openclaw/data/questrade-token-cache.json` command is a direct local file-deletion action that could be executed by a user or agent without understanding its consequences. In a skill context, this is dangerous because it mixes executable host-level remediation with passive API documentation, creating a path for unintended local state manipulation.

Credential Access

High
Category
Privilege Escalation
Content
# Questrade refresh tokens rotate — save the new one
    _save_refresh_token(new_refresh, practice)

    # Cache access token (subtract 60 s as buffer)
    TOKEN_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
    expires_at = datetime.now(timezone.utc) + timedelta(seconds=max(expires_in - 60, 0))
    with open(TOKEN_CACHE_FILE, "w") as f:
Confidence
84% confidence
Finding
The script caches live access tokens to a JSON file on disk, and no permission hardening is visible around file creation. Access tokens grant API access for their lifetime, so if another local user, process, backup system, or malware can read the file, they may gain temporary unauthorized access to account and trading data, and potentially trading actions depending on scope.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
# Stop-limit order
python3 scripts/questrade_cli.py order sell 12345678 8049 5 StopLimit --stop-price 140.00 --limit-price 139.50

# Skip confirmation prompts
python3 scripts/questrade_cli.py order buy 12345678 8049 10 Market --force
```
Confidence
86% confidence
Finding
The skill documents a `--force` flag that bypasses interactive confirmation for order placement, enabling fully autonomous trade execution if the underlying account has partner-level trading permissions. In a financial skill, removing human approval on a state-changing action materially increases the risk of unintended or prompt-induced transactions.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The reference documents order placement and cancellation endpoints without a prominent warning that these actions can affect real brokerage accounts and financial positions. In an agent or automation context, omission of that warning increases the chance of accidental live trading or cancellation when users assume the examples are informational only.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The document includes a destructive local shell command to delete an authentication cache file, which is unrelated to the remote API specification itself and encourages users or downstream agents to modify local state. In an agent setting, copying such remediation text into automated execution paths could cause unintended deletion of credentials or authentication state on the host machine.

Session Persistence

Medium
Category
Rogue Agent
Content
def _invalidate_config_cache() -> None:
    """Clear the config cache after a write so the next read is fresh."""
    global _config_cache
    _config_cache = None
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.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The script automatically persists rotated refresh tokens to disk without an explicit user-facing warning or consent at write time. Because refresh tokens are long-lived credentials, silent storage increases the risk of credential theft from local compromise, backups, shared home directories, or permissive file permissions, especially in an agent-skill environment that may run unattended.

External Transmission

Medium
Category
Data Exfiltration
Content
def api_post(path: str, body: Optional[dict] = None) -> dict:
    base, headers = _auth()
    resp = requests.post(
        f"{base}{path}",
        headers={**headers, "Content-Type": "application/json"},
        json=body,
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
Order cancellation is a destructive financial action, yet the cancel-order command executes immediately once invoked, with no confirmation prompt, dry-run mode, or explicit warning. In an agent or automation context, a mistaken command, argument mix-up, or prompt-injection-driven invocation could cancel legitimate open orders and materially affect trading outcomes.

Missing User Warnings

Low
Confidence
92% confidence
Finding
The token-cache deletion instruction omits a warning that it removes local authentication state and may disrupt access until credentials are refreshed. While not inherently malicious, this can mislead users or agents into performing unnecessary destructive local actions during routine error handling.

Unpinned Dependencies

Low
Category
Supply Chain
Content
# Python 3.9+ required (uses zoneinfo, f-strings, typing.Optional)
requests>=2.28.0
Confidence
95% confidence
Finding
The dependency specification uses a lower-bound range (`requests>=2.28.0`) instead of pinning to an exact version, which makes builds non-reproducible and can unexpectedly pull in different package versions over time. In a security context this increases supply-chain risk and makes it harder to verify whether known-vulnerable or regressed releases are being installed.

Unverifiable Dependency: requests has 16 known advisory(ies) (CVE-2014-1830 (Exposure of Sensitive Information to an Unauthorized Actor in Requests); CVE-2024-47081 (Requests vulnerable to .netrc credentials leak via malicious URLs); CVE-2024-35195 (Requests `Session` object does not verify requests after making first request wi) +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
90% confidence
Finding
Because `requests` is not pinned, the manifest does not prove which concrete release will be installed, so you cannot determine whether deployment will include a version affected by one of the known advisories. This is dangerous primarily as an uncertainty and supply-chain hygiene issue: depending on environment and install time, a vulnerable version could be selected without visibility.

Static analysis

No suspicious patterns detected.