Back to skill

Security audit

Robinhood Mcp

Security checks for vulnerabilities and agentic risk

Overview

This skill can keep long-term access to a Robinhood account and place trades, but its safeguards are too broad for that level of financial authority.

Review carefully before installing. Use it only if you are comfortable granting an agent ongoing Robinhood account access that can include trading. Keep ROBINHOOD_MCP_HOME on private local storage, avoid shared or synced folders, do not set ROBINHOOD_MCP_URL except to a trusted Robinhood endpoint, require explicit human approval for every order, and revoke/logout immediately if the credential directory may have been exposed.

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

Error
Location
scripts/robinhood_mcp.py:276
Finding

Stored Trading Credentials Are Not Bound to a Trusted Server Origin

Content
View full analysis
str: return os.environ.get("ROBINHOOD_MCP_URL", DEFAULT_SERVER_URL) ``` ### Technical Analysis The OAuth provider loads credentials from the same `credentials.json` file regardless of the configured MCP server origin. At the same time, `ROBINHOOD_MCP_URL` can replace the trusted default endpoint with an arbitrary URL. OAuth access and refresh tokens are security-sensitive bearer credentials and must be bound to their intended resource server and authorization context. Reusing a common credential store after changing the endpoint can cause the authenticated client to present an existing token during communication with a substituted server. The implementation neither restricts custom endpoints to trusted Robinhood origins nor associates persisted credentials with the normalized server origin for which they were issued. The custom endpoint feature is not required for ordinary Robinhood trading functionality and therefore expands the trust boundary beyond the minimum privilege necessary for the declared functionality. ### Attack Path 1. A local attacker, compromised launcher, or unsafe deployment configuration sets `ROBINHOOD_MCP_URL` to an attacker-controlled endpoint. 2. The victim has previously authenticated, leaving valid Robinhood access and refresh tokens in `credentials.json`. 3. The victim or agent runs `status`, `tools`, or `call`. 4. `build_provider()` loads the shared credential store while using the attacker-controlled URL as the OAuth/MCP server URL. 5. Authentication or MCP tr ...[truncated 664 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/robinhood_mcp.py:91
Finding

Credential Files Are Persisted When Permission Hardening Fails

Content
View full analysis
None: self.home.mkdir(parents=True, exist_ok=True) try: os.chmod(self.home, 0o700) except OSError: pass @staticmethod def _write_private(path: Path, text: str) -> None: tmp = path.with_suffix(path.suffix + ".tmp") tmp.write_text(text) try: os.chmod(tmp, 0o600) except OSError: pass os.replace(tmp, path) ``` ### Technical Analysis The implementation documents `credentials.json` as a `0600` secret, but the security property is not enforced. Both directory and file permission failures are silently ignored, and the credential file is persisted even when restrictive permissions cannot be established. Additionally, `Path.write_text()` creates or truncates the predictable temporary path before `chmod(0600)` is attempted. Its initial permissions depend on the process umask and existing file state. On a permissive umask, shared directory, unsuitable mounted volume, or filesystem that does not implement Unix permissions, token data may exist with broader access than intended. A pre-existing temporary file may also retain unsafe ownership or permission characteristics before replacement. Because `credentials.json` contains access and refresh tokens capable of authorizing trading operations, permission hardening must fail closed rather than operating on a best-effort basis. ### Attack Path 1. The Skill is configured with `ROBINHOOD_MCP_HOME` on a shared, permissive, incorrectly owned, or non-POSIX filesystem. 2. The directory or temporary file cannot be restricted to `0700` or `0600`, or the process has a permissive umask. 3. `_ensure_home()` and `_write_private()` suppress the resulting `OSError`. 4. Login or token refresh continues and writes OAu ...[truncated 659 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:29
Finding

Security-Critical MCP Dependency Is Not Reproducibly Pinned

Content
View full analysis
=1.9.0 ``` ### Technical Analysis The Skill permits installation of any current or future `mcp` release at or above version 1.9.0. No upper bound, exact pin, lockfile, or integrity hash is provided. This dependency executes inside the process that reads Robinhood access and refresh tokens, performs OAuth exchanges, communicates with the remote MCP service, and submits tool calls that may place trades. Consequently, dependency updates have direct access to high-value credentials and financial operations. An unreviewed, compromised, or behaviorally incompatible future release can alter authentication handling, exfiltrate credentials, redirect requests, or manipulate tool arguments and responses. This finding does not establish that the currently available `mcp` package is malicious. The vulnerability is the lack of reproducible and reviewable dependency resolution for a security-critical component. ### Attack Path 1. A future `mcp` package release is compromised, malicious, or contains a security regression. 2. A new installation resolves `mcp>=1.9.0` to that release automatically. 3. The package executes when `robinhood_mcp.py` imports and initializes the MCP client. 4. The compromised dependency accesses OAuth tokens in memory or on disk, modifies authentication traffic, or changes trading tool requests. 5. Credentials or financial operations are exposed without any source change in this Skill repository. ### Impact Assessment A compromised dependency executes with the full privileges of the Skill process. It can read Robinhood credentials, access files available to the process, make network requests, observe account information, and manipulate or initiate MCP tool calls. The financial impact may include unauthorized trades, while the bro ...[truncated 74 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (22)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
| File                          | Contents                                              |
| ----------------------------- | ----------------------------------------------------- |
| `$ROBINHOOD_MCP_HOME/client.json`      | The dynamically-registered OAuth client.     |
| `$ROBINHOOD_MCP_HOME/credentials.json` | Access token, refresh token, expiry (0600).  |

On every non-`login` command the skill loads these, and if the access token
is expired it uses the **refresh token** to mint a new one silently. The user

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 30)May include surrounding context.

md
`redirect_handler` shows/opens it; the user approves; our
   `callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 52)May include surrounding context.

md
`redirect_handler` shows/opens it; the user approves; our
   `callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 31)May include surrounding context.

md
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 80)May include surrounding context.

md
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 86)May include surrounding context.

md
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/robinhood_mcp.py (reported line 375)May include surrounding context.

python
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/robinhood_mcp.py (reported line 392)May include surrounding context.

python
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/robinhood_mcp.py (reported line 427)May include surrounding context.

python
`callback_handler` captures the `code` (via a loopback server, or pasted
   stdin in `--manual` mode).
5. The SDK exchanges the code for an **access token + refresh token**, stored
   in `credentials.json`.

All five steps happen inside the single `login` connection. `tools`, `status`,
and `call` skip straight to using/refreshing the stored tokens.

Credential Access

High
Category
Privilege Escalation
Confidence
79% confidence
Finding

Persisting access and refresh tokens in credentials.json across sessions creates a high-value local secret that can grant ongoing Robinhood trading access if the file is stolen from disk, backups, mounted volumes, or other shared host storage. In this skill's context, the danger is elevated because the tokens authorize financial-account actions, including trading, not just low-risk profile access.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 48)May include surrounding context.

md
`~/.config/robinhood-mcp`). As long as that directory persists across
sessions, later sessions never prompt:

- `credentials.json` holds `access_token`, `refresh_token`, and an absolute
  `_expires_at` timestamp.
- On load, `FileTokenStorage.get_tokens()` checks `_expires_at`. If the access
  token is within `EXPIRY_SKEW_SECONDS` of expiry, it returns the token with

Credential Access

High
Category
Privilege Escalation
Confidence
82% confidence
Finding

Documenting that credentials.json contains both access_token and refresh_token identifies a concrete persistent secret store whose compromise would allow account access and potentially silent reauthentication. Because this skill can act on a Robinhood account, theft of the refresh token could enable unauthorized portfolio access and trading until revoked.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 50)May include surrounding context.

md
- `credentials.json` holds `access_token`, `refresh_token`, and an absolute
  `_expires_at` timestamp.
- On load, `FileTokenStorage.get_tokens()` checks `_expires_at`. If the access
  token is within `EXPIRY_SKEW_SECONDS` of expiry, it returns the token with
  the **access token blanked but the refresh token intact**. This is the key
  trick: the MCP SDK loads tokens from storage but does **not** restore the

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
token is within `EXPIRY_SKEW_SECONDS` of expiry, it returns the token with
  the **access token blanked but the refresh token intact**. This is the key
  trick: the MCP SDK loads tokens from storage but does **not** restore the
  expiry clock across processes, so without this it would trust a stale access
  token, hit a `401`, and fall back to a full interactive re-auth. Blanking the
  access token steers the SDK into its silent `refresh_token` grant instead.
- When a refresh response omits a new `refresh_token`, the previous one is

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 54)May include surrounding context.

md
token is within `EXPIRY_SKEW_SECONDS` of expiry, it returns the token with
  the **access token blanked but the refresh token intact**. This is the key
  trick: the MCP SDK loads tokens from storage but does **not** restore the
  expiry clock across processes, so without this it would trust a stale access
  token, hit a `401`, and fall back to a full interactive re-auth. Blanking the
  access token steers the SDK into its silent `refresh_token` grant instead.
- When a refresh response omits a new `refresh_token`, the previous one is

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/robinhood_mcp.py (reported line 84)May include surrounding context.

python
token is within `EXPIRY_SKEW_SECONDS` of expiry, it returns the token with
  the **access token blanked but the refresh token intact**. This is the key
  trick: the MCP SDK loads tokens from storage but does **not** restore the
  expiry clock across processes, so without this it would trust a stale access
  token, hit a `401`, and fall back to a full interactive re-auth. Blanking the
  access token steers the SDK into its silent `refresh_token` grant instead.
- When a refresh response omits a new `refresh_token`, the previous one is

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/robinhood_mcp.py (reported line 87)May include surrounding context.

python
token is within `EXPIRY_SKEW_SECONDS` of expiry, it returns the token with
  the **access token blanked but the refresh token intact**. This is the key
  trick: the MCP SDK loads tokens from storage but does **not** restore the
  expiry clock across processes, so without this it would trust a stale access
  token, hit a `401`, and fall back to a full interactive re-auth. Blanking the
  access token steers the SDK into its silent `refresh_token` grant instead.
- When a refresh response omits a new `refresh_token`, the previous one is

Credential Access

High
Category
Privilege Escalation
Confidence
76% confidence
Finding

Preserving the previous refresh token when the server omits a new one extends long-lived credential continuity, which is operationally useful but increases the value of any stolen token because access can persist across sessions without fresh user interaction. In a trading skill, durable refresh-token retention materially raises the impact of local credential compromise.

Content

Scanner excerpt · references/oauth_and_persistence.md (reported line 56)May include surrounding context.

md
trick: the MCP SDK loads tokens from storage but does **not** restore the
  expiry clock across processes, so without this it would trust a stale access
  token, hit a `401`, and fall back to a full interactive re-auth. Blanking the
  access token steers the SDK into its silent `refresh_token` grant instead.
- When a refresh response omits a new `refresh_token`, the previous one is
  preserved so the chain doesn't break.

Credential Access

High
Category
Privilege Escalation
Confidence
90% confidence
Finding

This code intentionally persists OAuth access and refresh tokens plus client registration to disk outside the session boundary. Although file permissions are tightened, theft of the credential files by another local process, compromised account, backup leak, or misconfigured ROBINHOOD_MCP_HOME could grant continued access to the user's Robinhood account and silent reauthentication.

Content

Scanner excerpt · scripts/robinhood_mcp.py (reported line 93)May include surrounding context.

python
def __init__(self, home: Path):
        self.home = home
        self.tokens_path = home / "credentials.json"
        self.client_path = home / "client.json"

    def _ensure_home(self) -> None:

Missing User Warnings

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The call command will invoke any Robinhood MCP tool using previously persisted OAuth credentials with no local confirmation, policy gate, or allowlist. In the context of a trading skill, this can directly enable high-risk financial actions such as placing orders on a user's brokerage account if an upstream agent, prompt injection, or workflow invokes the command unexpectedly.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill declares broad capabilities including shell, network, file read/write, environment access, and MCP connectivity, but does not define any explicit tool/permission scope. For a skill that can authenticate to a brokerage and place trades, the absence of least-privilege boundaries increases the blast radius if the skill is misused, modified, or invoked unexpectedly.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.