Back to skill

Security audit

Revolut Business

Security checks for vulnerabilities and agentic risk

Overview

This Revolut Business skill appears purpose-built rather than deceptive, but it handles real financial authority with weak guardrails and insecure local credential handling.

Install only if you are comfortable giving this skill access to Revolut Business financial data and money-moving operations. Before use, fix or mitigate the setup command-injection issue, store keys and tokens with strict owner-only permissions or a secure secret store, avoid --yes, require confirmation for FX and transfers, use pinned dependencies, and ensure Revolut-side permissions and payment limits are tightly scoped.

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/setup.py:44
Finding

Shell Command Injection Through Certificate Subject Fields

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/revolut.py:73
Finding

OAuth Tokens Are Stored Without Explicit Owner-Only Permissions

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
README.md:22
Finding

Security-Sensitive Dependencies Are Installed Without Version or Integrity Pinning

Content
View full analysis
Remediation
View remediation
cryptography== ``` - Generate and publish SHA-256 hashes for all direct and transitive artifacts. - Require hash verification during installation: ```bash python3 -m pip install --require-hashes -r requirements.txt ``` - Use a lock file generated for supported Python versions and platforms. - Periodically update dependencies through a controlled review process with automated vulnerability scanning. - Recommend installation in an isolated virtual environment. - State the expected package index explicitly and avoid untrusted extra indexes. - Include provenance or signature verification where supported by the distribution workflow. ]]>
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
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (26)

Tainted flow: 'req' from os.environ.get (line 190, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

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

python
headers={"Content-Type": "application/x-www-form-urlencoded"},
    )
    try:
        resp = urllib.request.urlopen(req)
        result = json.loads(resp.read())
        result["obtained_at"] = int(time.time())
        result["client_id"] = CLIENT_ID

Tainted flow: 'req' from os.environ.get (line 190, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

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

python
headers={"Content-Type": "application/x-www-form-urlencoded"},
    )
    try:
        resp = urllib.request.urlopen(req)
        result = json.loads(resp.read())
        result["obtained_at"] = int(time.time())
        result["client_id"] = CLIENT_ID

Tainted flow: 'req' from os.environ.get (line 190, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

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

python
headers={"Content-Type": "application/x-www-form-urlencoded"},
    )
    try:
        resp = urllib.request.urlopen(req)
        result = json.loads(resp.read())
        result["obtained_at"] = int(time.time())
        result["client_id"] = CLIENT_ID

Tainted flow: 'req' from os.environ.get (line 190, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

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

python
headers={"Content-Type": "application/x-www-form-urlencoded"},
    )
    try:
        resp = urllib.request.urlopen(req)
        result = json.loads(resp.read())
        result["obtained_at"] = int(time.time())
        result["client_id"] = CLIENT_ID

Tainted flow: 'req' from os.environ.get (line 254, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/setup.py (reported line 261)May include surrounding context.

python
)

    try:
        resp = urllib.request.urlopen(req)
        tokens = json.loads(resp.read())
        tokens["obtained_at"] = now
        tokens["client_id"] = client_id

Tainted flow: 'req' from os.environ.get (line 254, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/setup.py (reported line 293)May include surrounding context.

python
)

    try:
        resp = urllib.request.urlopen(req)
        tokens = json.loads(resp.read())
        tokens["obtained_at"] = now
        tokens["client_id"] = client_id

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 · README.md (reported line 207)May include surrounding context.

md
- Private key and tokens stored in `~/.clawdbot/revolut/` — treat as sensitive
- Payments require explicit confirmation (use `--yes` to skip, `--draft` for approval in app)
- Never share your private key, tokens, or client assertion JWT
- Access tokens expire after ~40 minutes and auto-refresh

## License

Credential Access

High
Category
Privilege Escalation
Confidence
92% confidence
Finding

The skill stores and automatically uses long-lived credentials, including refresh tokens and an RSA private key, to obtain access tokens without further user interaction. In the context of a business banking API, compromise of these artifacts could permit ongoing unauthorized access to financial data and payment capabilities.

Content

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

text

## Token Auto-Refresh
- Access tokens expire after ~40 minutes
- Automatically refreshed using the refresh token before API calls
- No manual intervention needed after initial auth

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

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

Content

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

python
def load_env():
    """Load .env from clawd workspace if vars not set."""
    global CLIENT_ID, ISS_DOMAIN
    if CLIENT_ID and ISS_DOMAIN:
        return

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

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

Content

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

python
if CLIENT_ID and ISS_DOMAIN:
        return
    env_paths = [
        Path.home() / "clawd" / ".env",
        Path(__file__).resolve().parent.parent.parent.parent / ".env",
    ]
    for env_path in env_paths:

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

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

Content

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

python
if CLIENT_ID and ISS_DOMAIN:
        return
    env_paths = [
        Path.home() / "clawd" / ".env",
        Path(__file__).resolve().parent.parent.parent.parent / ".env",
    ]
    for env_path in env_paths:

Credential Access

High
Category
Privilege Escalation
Confidence
88% confidence
Finding

The tool persists OAuth tokens, including the refresh token, to a JSON file in the user's home directory without setting restrictive permissions or any encryption. A stolen refresh token can enable long-lived unauthorized access to Revolut business data and potentially payment-capable operations depending on token scope.

Content

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

python
def refresh_access_token():
    """Refresh the access token using refresh_token."""
    tokens = load_tokens()
    if not tokens or "refresh_token" not in tokens:
        print("ERROR: No refresh token. Run 'revolut auth' first.", file=sys.stderr)

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
97% confidence
Finding

Using shell=True with a generic run(cmd) helper enables tool-parameter abuse because later calls interpolate user-supplied values into shell command strings. In this setup context, users are explicitly prompted for organization, country, common name, and redirect URI, so a maliciously crafted value could inject arbitrary shell commands and execute code on the host running setup.

Content

Scanner excerpt · scripts/setup.py (reported line 47)May include surrounding context.

python
def run(cmd):
    return subprocess.run(cmd, shell=True, capture_output=True, text=True)


def main():

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/revolut.py (reported line 123)May include surrounding context.

python
1. Generate RSA key pair & X509 certificate
  2. Upload certificate to Revolut
  3. Configure OAuth redirect URI
  4. Authorize and get access token

{YELLOW}Requirements:{RESET}
  • Python 3.10+ with PyJWT & cryptography

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/setup.py (reported line 60)May include surrounding context.

python
1. Generate RSA key pair & X509 certificate
  2. Upload certificate to Revolut
  3. Configure OAuth redirect URI
  4. Authorize and get access token

{YELLOW}Requirements:{RESET}
  • Python 3.10+ with PyJWT & cryptography

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/setup.py (reported line 227)May include surrounding context.

python
1. Generate RSA key pair & X509 certificate
  2. Upload certificate to Revolut
  3. Configure OAuth redirect URI
  4. Authorize and get access token

{YELLOW}Requirements:{RESET}
  • Python 3.10+ with PyJWT & cryptography

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 12)May include surrounding context.

md
- 💰 **Accounts & Balances** — list all accounts, total EUR balance
- 📋 **Transactions** — filter by date, type, account; JSON output
- 👥 **Counterparties** — list, search by name
- 💸 **Payments** — send payments (with confirmation) or create drafts
- 💱 **FX Exchange** — exchange currencies between accounts
- 🔄 **Internal Transfers** — move funds between own accounts
- 📊 **CSV Export** — export transactions for bookkeeping

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
85% 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 · README.md (reported line 174)May include surrounding context.

md
# Payments
revolut pay -c "Lisa Dreischer" --amount 50.00 -r "Lunch"
revolut pay -c "Lisa Dreischer" --amount 50.00 --draft    # Draft (approve in app)
revolut pay -c "Lisa Dreischer" --amount 50.00 --yes      # Skip confirmation

# Currency Exchange
revolut fx --amount 100 --sell EUR --buy USD

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
85% 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/revolut.py (reported line 487)May include surrounding context.

python
# Payments
revolut pay -c "Lisa Dreischer" --amount 50.00 -r "Lunch"
revolut pay -c "Lisa Dreischer" --amount 50.00 --draft    # Draft (approve in app)
revolut pay -c "Lisa Dreischer" --amount 50.00 --yes      # Skip confirmation

# Currency Exchange
revolut fx --amount 100 --sell EUR --buy USD

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill exposes capabilities to read environment variables, read/write local files, invoke shell commands, and access the network, but it does not declare any explicit tool scope or permission boundaries. For a finance-related skill that handles private keys and OAuth tokens, this lack of least-privilege declaration makes accidental overreach or abusive use harder to constrain and audit.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
81% confidence
Finding

The ability to create persistent payment drafts and maintain authenticated session state via saved tokens enables actions to survive beyond the initiating interaction. In a financial workflow, that persistence can be abused to stage transactions for later execution or to retain access longer than a user expects.

Content

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

md
# Send payment (with confirmation prompt)
python3 {baseDir}/scripts/revolut.py pay -c "Lisa Dreischer" --amount 50.00 --currency EUR -r "Lunch"

# Create draft (no immediate send)
python3 {baseDir}/scripts/revolut.py pay -c "Lisa Dreischer" --amount 50.00 --draft -r "Lunch"

# Skip confirmation

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
90% confidence
Finding

The documented -y option allows sending payments without an interactive confirmation step. In a payment skill, removing human confirmation materially increases the risk of unintended or malicious fund transfers if an agent is tricked, misprompted, or operates autonomously.

Content

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

Create draft (no immediate send)

python3 {baseDir}/scripts/revolut.py pay -c "Lisa Dreischer" --amount 50.00 --draft -r "Lunch"

Skip confirmation

python3 {baseDir}/scripts/revolut.py pay -c "Lisa Dreischer" --amount 50.00 -y

text

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

Currency exchange executes immediately with no confirmation prompt, unlike the payment path which warns the user before sending funds. In an agent/CLI context, this increases the chance of unintended financial actions caused by operator error, prompt injection, or unsafe automation.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

Internal transfers are executed immediately without a user-facing confirmation or review step. Because this skill can move money between business accounts, accidental or injected commands could trigger irreversible or operationally disruptive fund movements.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
96% confidence
Finding

The helper wraps subprocess.run with shell=True and is later used with command strings containing user-influenced values such as certificate subject fields and URLs. This creates a command-injection path because shell metacharacters in those values can alter the executed command, and the script also invokes external tools like openssl, curl, and clipboard utilities through the shell.

Content

Scanner excerpt · scripts/setup.py (reported line 47)May include surrounding context.

python
def run(cmd):
    return subprocess.run(cmd, shell=True, capture_output=True, text=True)


def main():

Static analysis

No suspicious patterns detected.