T09 · Insecure Skill Coding Practices
- Location
scripts/setup.py:44- Finding
Shell Command Injection Through Certificate Subject Fields
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/setup.py:44Shell Command Injection Through Certificate Subject Fields
scripts/revolut.py:73OAuth Tokens Are Stored Without Explicit Owner-Only Permissions
README.md:22Security-Sensitive Dependencies Are Installed Without Version or Integrity Pinning
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
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
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
resp = urllib.request.urlopen(req)
tokens = json.loads(resp.read())
tokens["obtained_at"] = now
tokens["client_id"] = client_id
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
)
try:
resp = urllib.request.urlopen(req)
tokens = json.loads(resp.read())
tokens["obtained_at"] = now
tokens["client_id"] = client_id
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
- 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
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.
## 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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
def load_env():
"""Load .env from clawd workspace if vars not set."""
global CLIENT_ID, ISS_DOMAIN
if CLIENT_ID and ISS_DOMAIN:
return
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
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:
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
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:
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.
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)
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.
def run(cmd):
return subprocess.run(cmd, shell=True, capture_output=True, text=True)
def main():
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
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
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
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
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.
- 💰 **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
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.
# 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
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.
# 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
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.
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.
# 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
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.
python3 {baseDir}/scripts/revolut.py pay -c "Lisa Dreischer" --amount 50.00 --draft -r "Lunch"
python3 {baseDir}/scripts/revolut.py pay -c "Lisa Dreischer" --amount 50.00 -y
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.
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.
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.
def run(cmd):
return subprocess.run(cmd, shell=True, capture_output=True, text=True)
def main():
No suspicious patterns detected.