Back to skill

Security audit

revolut

Security checks for vulnerabilities and agentic risk

Overview

This Revolut banking skill is mostly purpose-aligned, but it needs review because it persists sensitive banking session/PIN data and contains an input-handling flaw that could expose authenticated financial data.

Review this before installing in any environment with real Revolut access. Avoid storing the PIN in config.json, keep the workspace private, run logout after use, do not pass portfolio IDs from untrusted sources, and prefer a version that fixes the in-page JavaScript interpolation, removes or gates invest-scan, pins dependencies, and enforces private permissions on session files.

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/revolut.py:1341
Finding

JavaScript Injection Through Unvalidated Portfolio Identifiers

Content
View full analysis
Any: """Execute fetch() inside the page context to get data directly.""" headers_json = json.dumps(extra_headers) method_json = json.dumps(method) body_json = json.dumps(body) try: print(f"[revolut] Fetching via JS: {method} {url}", file=sys.stderr) result = page.evaluate( f""" async () => {{ try {{ const extra = {headers_json}; const method = {method_json}; const body = {body_json}; const headers = {{ 'Accept': 'application/json, text/plain, */*', ...extra }}; const opts = {{ method, headers, credentials: 'include' }}; if (body !== null && body !== undefined) {{ headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }} const res = await fetch("{url}", opts); if (!res.ok) return {{ error: String(res.status) + " " + (res.statusText || "") }}; return await res.json(); }} catch (e) {{ return {{ error: e.toString() }}; }} }} """ ) ``` User-controlled portfolio identifiers reach this function without validation: ```python portfolio_id = getattr(args, "id", None) if portfolio_id: raw = _fetch_via_api_call( page, f"/api/retail/trading-acces ...[truncated 3128 chars]
Remediation
View remediation
{ const headers = { "Accept": "application/json, text/plain, */*", ...extra, }; const opts = { method, headers, credentials: "include", }; if (body !== null && body !== undefined) { headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(body); } const res = await fetch(url, opts); if (!res.ok) { return { error: `${res.status} ${res.statusText || ""}`, }; } return await res.json(); }""", { "url": url, "extra": extra_headers, "method": method, "body": body, }, ) ``` 2. Validate `--id` and `--portfolio-id` against the exact identifier syntax used by Revolut. If they are UUIDs, enforce a full UUID match and reject all other values. 3. URL-encode identifiers with `urllib.parse.quote(identifier, safe="")` before placing them into path components. 4. Restrict `_fetch_via_api_call()` to relative `/api/retail/` paths and reject absolute URLs, protocol-relative URLs, control characters, quotes, backslashes, and unexpected origins. 5. Add security tests using identifiers containing quotes, backticks, newlines, `${...}`, closing parentheses, and script statements to ensure they are treated strictly as data. 6. Prefer Playwright's request APIs or a fixed endpoint mapping over dynamically generated in-page JavaScript wherever practical. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/revolut.py:255
Finding

Sensitive Revolut Authentication State Stored Without Explicit Restrictive Permissions

Content
View full analysis
None: p.mkdir(parents=True, exist_ok=True) def _write_json(path: Path, payload: Any) -> None: _ensure_dir(path.parent) path.write_text( json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8", ) ``` The authenticated browser state is written to disk: ```python state_file = _state_file_from_args(args) _ensure_dir(state_file.parent) # ... time.sleep(2) try: context.storage_state(path=str(state_file)) print( f"[revolut] Login successful. State saved to: {state_file}", file=sys.stderr, ) except Exception as e: print( f"[revolut] WARNING: could not save storage state: {e}", file=sys.stderr, ) ``` A plaintext PIN may be loaded from the configuration: ```python user_name, user_cfg = _resolve_user(args.user) args._user_name = user_name args._pin = user_cfg.get("pin") ``` The documented configuration stores that PIN directly in JSON: ```json { "users": { "oliver": {}, "sylvia": { "pin": "123456" } } } ``` ### Technical Analysis The configuration may contain a banking PIN, while Playwright storage-state files and persistent Chromium profiles can contain cookies, local storage, and other reusable authentication material. The code creates these paths without explicitly applying modes such as `0700` for directories and `0600` for files. Consequently, confidentiality depends on the process umask, workspace permissions, inherited directory permissions, and deployment environment. A permissive umask or shared workspace can make the PIN or session d ...[truncated 1519 chars]
Remediation
View remediation
None: path.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(path, 0o700) ``` 2. Create sensitive files atomically with mode `0600`. After Playwright writes the storage state, explicitly enforce its mode: ```python context.storage_state(path=str(state_file)) os.chmod(state_file, 0o600) ``` 3. Validate that configuration and state files are owned by the current user and are not symbolic links before reading or writing them. 4. Reject configuration or state files that are group-readable or world-readable, or automatically correct their permissions after warning the user. 5. Store the PIN in an operating-system credential manager or secret-management facility rather than plaintext JSON. If that is unavailable, clearly document and enforce `0600` permissions on `config.json`. 6. Apply equivalent protections to persistent Chromium profile directories and debug payload files. 7. Preserve the existing logout cleanup, but also provide automatic cleanup on failure and optionally expire local state after a configurable period. 8. Warn users that `--debug` writes raw financial API responses and ensure those files also receive mode `0600`. ]]>

T08 · Insecure Dependencies

Note
Location
SETUP.md:8
Finding

Unpinned Playwright Package and Browser Installation

Content
View full analysis
Remediation
View remediation
" python3 -m playwright install chromium ``` 2. Maintain a lock file containing cryptographic hashes and install with hash verification, for example through a generated requirements file and `pip --require-hashes`. 3. Record and review the exact Chromium revision installed by the pinned Playwright release. 4. Pin any Playwright container image to both a versioned tag and an immutable image digest. 5. Install dependencies inside a dedicated virtual environment or locked container rather than into a privileged global Python environment. 6. Re-run security review and automated tests before updating the pinned package or browser revision. 7. Use a trusted package index and restrict dependency resolution to approved repositories. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (8)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The documented purpose understates the skill's real behavior: it handles investment data, stores browser session state, reads credentials/PIN from config, and includes endpoint enumeration behavior. In a financial automation context, this mismatch is dangerous because users may grant trust or permissions appropriate for simple account listing/transactions while the skill actually performs broader, more sensitive actions involving authentication secrets and persistent banking session material.

Content

No source excerpt is available for this finding.

Memory Manipulation

High
Category
Memory Poisoning
Confidence
80% confidence
Finding

Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Content

Scanner excerpt · scripts/revolut.py (reported line 1084)May include surrounding context.

python
state_file.unlink()
            print(f"[revolut] Deleted state: {state_file}", file=sys.stderr)
    except Exception as e:
        print(f"[revolut] Failed to delete state: {e}", file=sys.stderr)

    try:
        if profile_dir.exists():

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The setup guide explicitly instructs users to place a 6-digit Revolut app PIN in a plaintext JSON file under the workspace. Even though this is framed as optional convenience for automation, it normalizes insecure credential storage and creates a recoverable secret on disk that could be exposed through filesystem access, logs, backups, or other workspace-sharing mechanisms.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill declares no explicit tool scope or permission boundaries even though it clearly requires filesystem access, network access, and state persistence. For a banking automation skill, missing scope declarations are dangerous because operators and agents cannot reliably constrain or review what the skill is allowed to do before execution.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The skill description and module docstring materially understate what the code can do: beyond wallet login/accounts/transactions, it also accesses Revolut Invest portfolios and investment transactions. This creates a capability mismatch that can bypass user/operator expectations and approvals, especially in an automation context handling financial data.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The invest-scan command performs endpoint discovery by collecting and outputting observed Revolut API URLs, expanding the skill beyond its stated automation purpose. In a credentialed banking context, this reconnaissance capability can expose internal API structure and facilitate broader unauthorized data access or follow-on abuse.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The code reads a Revolut PIN from config.json and automatically enters it into the login flow, meaning a sensitive authentication secret is stored locally in plaintext and consumed without explicit runtime warning or confirmation. In a financial automation skill, this increases the blast radius of workspace compromise and normalizes unsafe credential handling.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The docstring presents the available commands as a closed list containing only login, logout, accounts, and transactions. Later CLI registration adds additional commands for portfolio, invest-transactions, and invest-scan, so the inline documentation actively misstates what the tool can do.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.