Back to skill

Security audit

openfin-enable-banking

Security checks for vulnerabilities and agentic risk

Overview

This banking skill appears purpose-built, but it needs Review because its callback and local storage handling could expose or corrupt sensitive financial data.

Review before installing. Only run this in a trusted environment, do not expose the callback server publicly without fixing state validation and path handling, avoid forwarding authorization links through casual messaging, and treat stored mandant/data JSON files as sensitive financial records.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/callback_server.py:72
Finding
Path Traversal Through Unvalidated Tenant and OAuth State Identifiers<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib/auth.py:134-145`, `scripts/onboard.py:172`, `scripts/callback_server.py:72-91`, `scripts/fetch.py:205-213` **Vulnerability Type**: Path traversal and arbitrary file access **Risk Level**: High ### Vulnerable Code `scripts/lib/auth.py:134-145`: ```python def load_mandant(mandant_id: str) -> dict: """Load mandanten/{mandant_id}.json.""" MANDANTEN_DIR.mkdir(parents=True, exist_ok=True) path = MANDANTEN_DIR / f"{mandant_id}.json" if not path.exists(): print(f"❌ Mandant not found: {path}", file=sys.stderr) sys.exit(1) with open(path) as f: return json.load(f) def save_mandant(mandant_id: str, data: dict) -> None: """Save mandanten/{mandant_id}.json (chmod 600).""" MANDANTEN_DIR.mkdir(parents=True, exist_ok=True) path = MANDANTEN_DIR / f"{mandant_id}.json" ``` `scripts/callback_server.py:72-91`: ```python if parsed.path == "/callback": params = parse_qs(parsed.query) code = params.get("code", [None])[0] state = params.get("state", [None])[0] if code and state: # Save callback PENDING_DIR.mkdir(parents=True, exist_ok=True) callback_data = { "code": code, "state": state, "receivedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } path = PENDING_DIR / f"{state}.json" with open(path, "w") as f: json.dump(callback_data, f, indent=2) os.chmod(path, 0o600) ``` `scripts/fetch.py:205-213`: ```python # Save to data directory data_dir = output_dir or DATA_DIR mandant_data_dir = data_dir / mandant_id mandant_data_dir.mkdir(parents=True, exist_ok=True) today = datetime.now().strftime("%Y-%m-%d") data_file = mandant_data_dir / f"{today}.json" with open(data_file, "w") as f: json.dump(result, f, indent=2, ensure_ascii=False) os.chmod(data_file, 0o600) ``` ### Technical Analysis The code interpolates attacker-controlled ` ...[truncated 2045 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate tenant IDs and callback states before using them in paths. For tenant IDs, use a strict allowlist such as: ```python SAFE_ID = re.compile(r"^[A-Za-z0-9_-]{1,64}$") ``` 2. Do not use an OAuth state received from the network as an unrestricted filename. Store pending states under server-generated opaque identifiers. 3. Resolve every destination and verify containment: ```python candidate = (base / f"{identifier}.json").resolve() if candidate.parent != base.resolve(): raise ValueError("Invalid identifier") ``` 4. Perform the same validation in `load_mandant`, `save_mandant`, onboarding existence checks, callback handling, renewal, and fetch output handling. 5. Use exclusive or atomic file creation where appropriate to prevent replacement races. 6. Run the callback service under a dedicated low-privilege account with write access only to its pending-callback directory. 7. Add tests covering `../`, absolute paths, path separators, encoded separators, excessive lengths, and platform-specific traversal forms. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/onboard.py:52
Finding
Predictable OAuth State and Acceptance of Unsolicited Callbacks<![CDATA[ ## Vulnerability Details **File Location**: `scripts/onboard.py:52-68`, `scripts/renew.py:34-50`, `scripts/callback_server.py:72-91` **Vulnerability Type**: OAuth state weakness and callback injection **Risk Level**: High ### Vulnerable Code `scripts/onboard.py:52-68`: ```python def start_auth(config: dict, token: str, bank_name: str, country: str, psu_type: str, callback_url: str) -> dict | None: """POST /auth to get redirect URL and state.""" payload = { "aspsp": { "name": bank_name, "country": country, }, "state": f"openclaw-{int(time.time())}", "redirect_url": callback_url, "psu_type": psu_type, "app": { "name": "FinRobotics", "description": "Bank data integration for tax advisors", }, "accounts": [{"iban": None}], # Request all accounts "scopes": ["aiia.accounts", "aiia.balances", "aiia.transactions"], } return api_request("POST", "/auth", token, json=payload) ``` `scripts/callback_server.py:72-91`: ```python if parsed.path == "/callback": params = parse_qs(parsed.query) code = params.get("code", [None])[0] state = params.get("state", [None])[0] if code and state: # Save callback PENDING_DIR.mkdir(parents=True, exist_ok=True) callback_data = { "code": code, "state": state, "receivedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } path = PENDING_DIR / f"{state}.json" with open(path, "w") as f: json.dump(callback_data, f, indent=2) os.chmod(path, 0o600) ``` ### Technical Analysis OAuth state is intended to bind an authorization response to the browser session and authorization request that initiated it. Here, state is based solely on the current Unix timestamp: ```python f"openclaw-{int(time.time())}" ``` The renewal flow uses the same construction with an `openclaw-renew-` pre ...[truncated 1800 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate state using a cryptographically secure random source: ```python import secrets state = secrets.token_urlsafe(32) ``` 2. Register the generated state locally before sending the authorization request. 3. Store only pending, server-generated states and reject callbacks whose state is not already registered. 4. Associate each state with an expiration time, expected tenant, operation type, and redirect URI. 5. Atomically mark state as consumed before processing the authorization code, and reject replayed callbacks. 6. Use constant-time comparison when comparing sensitive state values. 7. Avoid making raw state values filenames; use a keyed hash or a database lookup. 8. Add rate limiting and request-size limits to the callback service. 9. If supported by the provider, use PKCE in addition to state so an intercepted or injected authorization code cannot be redeemed without the verifier. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/callback_server.py:103
Finding
Reflected HTML Injection in OAuth Callback Error Response<![CDATA[ ## Vulnerability Details **File Location**: `scripts/callback_server.py:103-115` **Vulnerability Type**: Reflected cross-site scripting **Risk Level**: Medium ### Vulnerable Code ```python else: error = params.get("error", ["unknown"])[0] print(f"❌ Callback error: {error}", file=sys.stderr) self.send_response(400) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(f"""<!DOCTYPE html> <html><body style="font-family:sans-serif;text-align:center;padding:60px"> <h2>&#10007; Autorisierung fehlgeschlagen</h2> <p>Fehler: {error}</p> </body></html>""".encode()) ``` ### Technical Analysis The `error` query parameter is attacker-controlled and is inserted directly into an HTML document without output encoding. A value containing HTML markup can therefore alter the response document. Script-capable markup may execute in the callback service's origin when a user opens the crafted URL. No Content Security Policy is set to limit script execution. The fact that the response has HTTP status 400 does not prevent the browser from interpreting its body as HTML. ### Attack Path 1. An attacker constructs a callback URL whose `error` parameter contains encoded HTML or script-capable markup. 2. The attacker sends the URL to a user or causes the user's browser to navigate to it during a deceptive authorization flow. 3. The callback server parses the parameter and interpolates it into the HTML response. 4. The browser interprets the injected markup in the callback origin. 5. The payload may alter the displayed authorization result, issue same-origin requests, or execute browser-side code where browser policy permits. ### Impact Assessment Successful exploitation can execute attacker-controlled content in the callback server's browser origin and can display a fraudulent authorization result. Potential consequences include user deception and interaction with same-origin callback endpoints. The callb ...[truncated 262 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer a fixed error message that does not include provider- or user-supplied text. 2. If displaying the value is necessary, escape it before interpolation: ```python from html import escape safe_error = escape(error, quote=True) ``` 3. Add a restrictive Content Security Policy, for example: ```text Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none' ``` 4. Add `X-Content-Type-Options: nosniff` and an appropriate `Referrer-Policy`. 5. Limit query parameter lengths and reject malformed input. 6. Avoid logging unrestricted control characters from the error parameter to reduce terminal and log-injection risk. ]]>

T08 · Insecure Dependencies

Note
Location
requirements.txt:1
Finding
Unbounded Dependency Version Ranges Make Installations Non-Reproducible<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt:1-3` **Vulnerability Type**: Unpinned third-party dependencies **Risk Level**: Low ### Vulnerable Code ```text PyJWT>=2.0.0 cryptography>=3.0 requests>=2.25.0 ``` ### Technical Analysis Each dependency has only a minimum version and no upper bound or exact reviewed version. A later installation can therefore resolve package releases that did not exist when the Skill was audited. This prevents reproducible builds and may introduce incompatible behavior or a compromised future release without a corresponding change to the Skill repository. The package names are legitimate and no typosquatting, dependency confusion, malicious package, or unsafe package index was identified. The risk arises from uncontrolled future resolution rather than a confirmed malicious dependency. ### Attack Path 1. The project is installed at a later date using `pip install -r requirements.txt`. 2. The package resolver selects newer versions than those used during review. 3. A selected release contains a security regression, compromised code, or behavior incompatible with the Skill. 4. The dependency executes in the context of the banking scripts and inherits their access to private-key material, API tokens, session records, and financial data. ### Impact Assessment A compromised dependency would run with the privileges of the invoking user and could access all data available to the Python process, including the private key loaded for JWT signing, generated bearer tokens, tenant session identifiers, and fetched financial records. No such compromise was found in the audited files. This is a preventative supply-chain and reproducibility weakness. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin dependencies to exact versions that have been tested and reviewed. 2. Generate and verify hashes using a lock-file workflow such as `pip-tools`: ```text package==reviewed.version --hash=sha256:... ``` 3. Install with `pip install --require-hashes`. 4. Use a controlled package index and disable unintended extra indexes in production. 5. Run automated vulnerability scanning against the lock file. 6. Review and update pinned versions on a scheduled basis rather than allowing implicit upgrades during deployment. ]]>
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
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (20)

Missing User Warnings

High
Confidence
98% confidence
Finding
The onboarding flow says the authorization URL may be sent via WhatsApp or email without warning that the link is sensitive and may grant or facilitate account access if intercepted. Because this is a banking OAuth-style authorization step, transmitting the link over insecure or misaddressed channels materially raises the risk of account compromise or unauthorized session creation.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill documents capabilities that perform network access, shell execution, and file writes, but it does not declare any explicit tool scope or permission boundaries. In an agent environment, that makes the effective privilege surface ambiguous and increases the risk of overbroad execution, unintended data access, or unsafe tool use when handling banking workflows.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill handles highly sensitive financial information, including bank authorization flows, account identifiers, balances, and transactions, but the documentation does not include an explicit privacy or sensitivity warning. This omission can lead operators or downstream agents to mishandle regulated PSD2 and financial data, store it insecurely, or disclose it without informed consent.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
| `generate_jwt(config)` | RS256 JWT for API auth |
| `api_request(method, endpoint, token, **kwargs)` | Authenticated API call with retry (429, timeout) |
| `load_mandant(mandant_id)` | Load `mandanten/{id}.json` |
| `save_mandant(mandant_id, data)` | Save mandant file (chmod 600) |
| `list_mandanten()` | List all mandant IDs |

### `scripts/callback_server.py` — HTTPS Callback Server
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
| `generate_jwt(config)` | RS256 JWT for API auth |
| `api_request(method, endpoint, token, **kwargs)` | Authenticated API call with retry (429, timeout) |
| `load_mandant(mandant_id)` | Load `mandanten/{id}.json` |
| `save_mandant(mandant_id, data)` | Save mandant file (chmod 600) |
| `list_mandanten()` | List all mandant IDs |

### `scripts/callback_server.py` — HTTPS Callback Server
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This markdown file documents production use for linked bank accounts and explicitly mentions reading real client data, which can affect user privacy and expose highly sensitive financial information. The section explains how to enable access but does not include any warning or disclosure about consent, secure handling, or privacy implications.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The manifest describes PSD2/Open Banking integration for fetching balances and transactions, but this file also provisions local TLS material by invoking the external `openssl` binary. Running subprocesses is a separate host-level capability that is not an obvious requirement of the stated banking data integration scope, especially since certificate generation is ancillary infrastructure rather than core banking functionality.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return
    print(f"🔐 Generating self-signed certificate...", file=sys.stderr)
    KEYS_DIR.mkdir(parents=True, exist_ok=True)
    subprocess.run(
        [
            "openssl", "req", "-x509", "-newkey", "rsa:2048",
            "-keyout", str(KEY_FILE),
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The success and failure HTML responses display German-only text such as 'Autorisierung erfolgreich' and 'Autorisierung fehlgeschlagen'. This imposes a specific language on all users with no opt-in, fallback, or documentation indicating a justified region-specific constraint.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script persists highly sensitive banking data, including account identifiers, balances, and transactions, to local JSON files by default. Although it applies restrictive file permissions after writing, there is no explicit consent prompt, storage warning, encryption at rest, or retention control, which increases the risk of unintended exposure on multi-user systems, backups, synced folders, or compromised hosts.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The onboarding flow persists sensitive banking session metadata and account details to a local mandant JSON file via save_mandant() without any visible consent prompt, warning, encryption, or storage-scope controls in this script. In a tax advisory automation context, this data can expose account identifiers, session identifiers, and account metadata to other local users, backups, logs, or misconfigured storage, increasing privacy and unauthorized-access risk.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
Several descriptive labels are presented in German, such as balance and transaction field explanations, without indicating that the skill is intentionally region-specific or giving users an alternative language. This creates a locale policy concern because the documentation implicitly forces one language in otherwise general API reference material.

Natural-Language Policy Violations

Low
Confidence
87% confidence
Finding
The transaction field table uses German labels such as 'Buchungsdatum', 'Wertstellungsdatum', and 'Verwendungszweck' in a general-purpose reference. Because no language selection or regional limitation is stated, this appears to enforce a specific locale in violation of the language-choice policy.

Unpinned Dependencies

Low
Category
Supply Chain
Content
PyJWT>=2.0.0
cryptography>=3.0
requests>=2.25.0
Confidence
94% confidence
Finding
The dependency specification uses a lower-bound only constraint for PyJWT, which makes builds non-reproducible and can pull in different versions over time. In a banking integration that handles authentication tokens, an unexpected vulnerable or behavior-changing PyJWT release could weaken JWT validation and create authentication or SSRF-related exposure depending on usage.

Unverifiable Dependency: PyJWT has 16 known advisory(ies) (CVE-2026-32597 (PyJWT accepts unknown `crit` header extensions); CVE-2024-53861 (PyJWT Issuer field partial matches allowed); CVE-2026-48522 (PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token ) +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
88% confidence
Finding
PyJWT has multiple known advisories, and because the manifest does not pin a version, there is no way to verify that deployed environments avoid affected releases. Given this skill's PSD2/open banking context, any JWT parsing or validation weakness is more dangerous because it may affect authentication flows, token trust decisions, or access delegation.

Unpinned Dependencies

Low
Category
Supply Chain
Content
PyJWT>=2.0.0
cryptography>=3.0
requests>=2.25.0
Confidence
93% confidence
Finding
The cryptography package is not pinned, so deployments may resolve to different versions across environments or at different times. For software processing sensitive banking data, drifting cryptographic library versions can introduce known OpenSSL-related flaws or cryptographic weaknesses without visibility or change control.

Unverifiable Dependency: cryptography has 16 known advisory(ies) (GHSA-39hc-v87j-747x (Vulnerable OpenSSL included in cryptography wheels); CVE-2023-50782 (Python Cryptography package vulnerable to Bleichenbacher timing oracle attack); GHSA-537c-gmf6-5ccf (Vulnerable OpenSSL included in cryptography wheels) +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
87% confidence
Finding
cryptography has known security advisories, but the unpinned manifest prevents verification that the installed version is patched. In a banking automation skill, cryptographic assurance is core to protecting secrets and transport security, so uncertainty around the exact crypto package version materially increases risk.

Unpinned Dependencies

Low
Category
Supply Chain
Content
PyJWT>=2.0.0
cryptography>=3.0
requests>=2.25.0
Confidence
93% confidence
Finding
The requests dependency is specified with only a minimum version, allowing uncontrolled upgrades and unverifiable resolved versions. In a skill that communicates with banking APIs, this is risky because requests has had issues affecting redirect handling, credential leakage, and TLS/session behavior, which could impact confidentiality of tokens or account data.

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
89% confidence
Finding
requests has several published advisories, and the absence of version pinning means the installed version may be vulnerable without operators realizing it. Because this skill likely sends authenticated requests to bank APIs, any issue involving credential leakage, redirect handling, or certificate/session verification could expose highly sensitive financial data or tokens.

Natural-Language Policy Violations

Low
Confidence
80% confidence
Finding
The CLI argument for country silently defaults to Germany (DE), which imposes a locale-specific behavior unless the user notices and overrides it. The file does not explain why DE is the default or present it as an explicit user choice.

Static analysis

No suspicious patterns detected.