Back to skill

Security audit

Personal Assistant

Security checks for vulnerabilities and agentic risk

Overview

This OpenMarlin skill is purpose-aligned, but it needs review because its helpers can expose platform API keys and send them to insufficiently constrained custom server URLs.

Review before installing. Use only the default https://api.openmarlin.ai origin or a trusted HTTPS deployment, avoid putting OPENMARLIN_PLATFORM_API_KEY in ordinary skill config, do not run bootstrap where terminal output is logged unless the key is rotated afterward, and avoid custom --agent-id values until path validation is added.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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/openclaw_skill_config.py:129
Finding
Platform credentials and sensitive request data may be transmitted over unencrypted HTTP## Vulnerability Details **File Location**: `scripts/openclaw_skill_config.py:129-143`, with authenticated request sinks in `scripts/platform_request.py:722-739` and `scripts/billing.py:698-699, 764-765, 840-841, 895-896, 943-944` **Vulnerability Type**: Insufficient API-origin and transport-security validation **Risk Level**: High ### Vulnerable Code ```python def require_server_url(raw: str) -> str: server_url = raw.strip().rstrip("/") if not server_url: raise SystemExit(build_server_url_setup_message()) parsed = urllib.parse.urlparse(server_url) if parsed.scheme in {"http", "https"} and parsed.netloc.lower() == "openmarlin.ai": raise SystemExit( build_server_url_setup_message( resolved_value=server_url, reason=( "OPENMARLIN_SERVER_URL points at the OpenMarlin website frontend. " f"Use the API origin instead: {DEFAULT_SERVER_URL}" ), ) ) return server_url ``` The resulting URL is subsequently used for authenticated requests: ```python status, payload, response_headers = request( url=f"{server_url}/v1/executions", method="POST", headers={ "Authorization": f"Bearer {api_key}", }, payload=body, ) ``` ```python status, payload, response_headers = request( url=f"{server_url}/v1/tasks", method="POST", headers={ "Authorization": f"Bearer {api_key}", }, payload=body, ) ``` Billing requests use the same trust decision: ```python server_url = require_server_url(args.server_url) api_key, api_key_source = resolve_api_key_or_exit( args.api_key, args.profile_id, args.agent_id, ) auth_headers = {"Authorization": f"Bearer {api_key}"} ``` ### Technical Analysis `require_server_url()` rejects the browser-facing `openmarlin.ai` hostname, but ...[truncated 2218 chars]
Remediation
## Remediation Suggestions 1. Require `https` for all non-loopback destinations. 2. Permit plain HTTP only when the parsed hostname is a validated loopback address such as `127.0.0.1`, `::1`, or `localhost`, preferably behind an explicit development option. 3. Require the URL to be a bare origin with: - an allowed scheme; - a nonempty hostname; - no user information; - no query string or fragment; - no preconfigured API path. 4. Resolve and normalize the hostname before applying loopback checks to avoid textual bypasses. 5. Record the issuing API origin in the credential profile and refuse to send that credential to another origin without explicit user approval. 6. Disable or tightly constrain cross-origin redirects for authenticated requests. Never forward an authorization header to a different origin. 7. Clearly warn users whenever a custom deployment is selected, and display the exact origin before transmitting credentials.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/registration_session.py:412
Finding
API-key bootstrap exposes the complete secret through standard and JSON output## Vulnerability Details **File Location**: `scripts/registration_session.py:412-437, 568-583` **Vulnerability Type**: Plaintext credential disclosure **Risk Level**: High ### Vulnerable Code ```python def print_api_key_issue( result: dict[str, Any], session: dict[str, Any] | None = None, ) -> None: if session is not None: print( f"Session ID: " f"{session.get('registration_session_id', '<unknown>')}" ) completion = session.get("completion") if isinstance(completion, dict): print( f"Account ID: " f"{completion.get('account_id', '<unknown>')}" ) workspace = completion.get("workspace") if isinstance(workspace, dict): print( f"Workspace: " f"{workspace.get('display_name', '<unknown>')} " f"({workspace.get('slug', '<unknown>')})" ) api_key = result.get("api_key") if not isinstance(api_key, dict): raise SystemExit( "API key bootstrap response is missing api_key metadata." ) print(f"Key ID: {api_key.get('key_id', '<unknown>')}") print(f"Workspace ID: {api_key.get('workspace_id', '<unknown>')}") print(f"Status: {api_key.get('status', '<unknown>')}") print(f"Label: {api_key.get('label', '<unknown>')}") print(f"Created at: {api_key.get('created_at', '<unknown>')}") print(f"Last used at: {api_key.get('last_used_at', '<unknown>')}") print(f"Secret: {result.get('secret', '<missing>')}") print("Export:") print( f" export OPENMARLIN_PLATFORM_API_KEY=" f"'{result.get('secret', '')}'" ) ``` The output dispatcher exposes the issuance response even when `--store` is used: ```python if arg ...[truncated 2632 chars]
Remediation
## Remediation Suggestions 1. Never print the secret when `--store` is active. 2. Redact the `secret` field from both normal and JSON output by default. 3. Return only non-sensitive metadata such as key ID, workspace ID, status, storage path, profile ID, and agent ID. 4. If manual retrieval is genuinely necessary, require a separate explicit `--show-secret` option and display a strong warning before use. 5. Avoid generating shell export snippets. If one must be supported, use robust shell quoting rather than directly interpolating server-controlled text. 6. Add tests asserting that neither standard nor JSON output contains the secret during the recommended `bootstrap --store` workflow. 7. Document credential rotation steps for users whose bootstrap output may already have been logged.

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/openclaw_platform_auth.py:20
Finding
Unvalidated agent identifiers permit filesystem writes outside the intended OpenClaw agent directory## Vulnerability Details **File Location**: `scripts/openclaw_platform_auth.py:20-42, 60-78`; related billing-state path use in `scripts/openclaw_billing_state.py:21-57` **Vulnerability Type**: Path traversal and unauthorized file placement **Risk Level**: Medium ### Vulnerable Code ```python def resolve_openclaw_state_dir() -> Path: override = os.environ.get("OPENCLAW_STATE_DIR", "").strip() if override: return Path(override).expanduser() return Path.home() / ".openclaw" def resolve_agent_dir(agent_id: str = DEFAULT_AGENT_ID) -> Path: return resolve_openclaw_state_dir() / "agents" / agent_id / "agent" def resolve_auth_store_path(agent_id: str = DEFAULT_AGENT_ID) -> Path: return resolve_agent_dir(agent_id) / "auth-profiles.json" def ensure_auth_store( agent_id: str = DEFAULT_AGENT_ID, ) -> tuple[Path, dict[str, Any]]: agent_dir = resolve_agent_dir(agent_id) agent_dir.mkdir(parents=True, exist_ok=True) try: agent_dir.chmod(0o700) except OSError: pass auth_path = resolve_auth_store_path(agent_id) if not auth_path.exists(): store: dict[str, Any] = {"version": 1, "profiles": {}} save_auth_store(auth_path, store) return auth_path, store ``` The eventual save operation atomically replaces the resolved target: ```python def save_auth_store(path: Path, store: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) fd, tmp_name = tempfile.mkstemp( prefix="auth-profiles-", suffix=".json", dir=str(path.parent), ) tmp_path = Path(tmp_name) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(store, handle, indent=2, sort_keys=True) handle.write("\n") try: tmp_path.chmod(0o600) except OSError: pass os.replace(tmp_path, path ...[truncated 3007 chars]
Remediation
## Remediation Suggestions 1. Restrict `agent_id` to a conservative grammar, such as `^[A-Za-z0-9_-]+$`. 2. Explicitly reject empty identifiers, `.`, `..`, absolute paths, path separators, and platform-specific alternate separators. 3. Resolve the candidate destination and verify that it remains beneath the resolved OpenClaw agents root using `Path.relative_to()` or an equivalent containment check. 4. Apply the same containment validation to both auth-profile and billing-state paths. 5. Validate `OPENCLAW_STATE_DIR` separately and avoid accepting it from untrusted Skill-level input. 6. Refuse to replace destinations that are symbolic links or whose parent hierarchy contains unexpected symbolic links. 7. Add regression tests covering `../`, absolute paths, repeated separators, encoded separators, and platform-specific path syntax.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/registration_session.py:224
Finding
Server-controlled authorization URLs are automatically opened without scheme validation## Vulnerability Details **File Location**: `scripts/registration_session.py:224-263, 343-351` **Vulnerability Type**: Unsafe external URI handling **Risk Level**: Medium ### Vulnerable Code ```python def build_browser_url(session: dict[str, Any]) -> str | None: handoff = session.get("handoff") if not isinstance(handoff, dict): return None authorization_url = handoff.get("authorization_url") if ( not isinstance(authorization_url, str) or not authorization_url.strip() ): return None return authorization_url.strip() ``` The unvalidated value is passed directly to an operating-system URI handler: ```python def auto_open_browser_url(browser_url: str) -> tuple[bool, str]: system = platform.system().lower() try: if system == "darwin": completed = subprocess.run( ["open", browser_url], check=False, capture_output=True, text=True, ) elif system == "windows": os.startfile(browser_url) # type: ignore[attr-defined] return ( True, "Opened the authorization page in your default browser.", ) else: completed = subprocess.run( ["xdg-open", browser_url], check=False, capture_output=True, text=True, ) except FileNotFoundError as error: return ( False, "Could not auto-open the browser because the opener is " f"missing: {error.filename}", ) except OSError as error: return False, f"Could not auto-open the browser: {error}" if completed.returncode == 0: return ( True, "Opened the authorization page in your default browser.", ) ...[truncated 2535 chars]
Remediation
## Remediation Suggestions 1. Parse the authorization URL and permit only HTTPS. 2. If development environments require HTTP, allow it only for validated loopback hosts. 3. Reject URL user information, fragments where unnecessary, local-file schemes, data URLs, scripting schemes, and arbitrary custom protocols. 4. For custom API deployments, display the authorization hostname and require explicit confirmation before opening it. 5. Consider an allowlist of expected identity-provider or deployment-controlled authorization origins. 6. Provide a `--no-open` mode and consider making it the default for noninteractive or Agent-managed environments. 7. Continue using argument arrays rather than a shell command; this existing safeguard prevents ordinary shell metacharacter injection.
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
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (18)

Credential Access

High
Category
Privilege Escalation
Content
The persisted OpenClaw config path is:

```text
skills.entries["openmarlin"].env
```

So OpenClaw can remember values such as:
Confidence
75% confidence
Finding
The README documents persisting skill environment values in `~/.openclaw/openclaw.json` under `skills.entries["openmarlin"].env`, and the examples include server URL, provider, routing labels, plus nearby guidance references `OPENMARLIN_PLATFORM_API_KEY` as a resolvable value. Storing credentials in general skill config increases the chance that secrets are exposed through local file reads, backups, logs, sync tooling, or other skills/components that can access the config, especially because the same document separately says API keys should be stored in auth-profile storage instead.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill explicitly handles issuance and storage of platform/workspace API keys, which is a sensitive credential-management capability. If that capability is not prominently disclosed in the summary and permission model, users and reviewers may underestimate the risk of secret creation, persistence, and later misuse; in this context, hidden credential handling is materially more dangerous than ordinary task execution because it can create durable access to an external account.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill explicitly handles issuance and storage of platform/workspace API keys, which is a sensitive credential-management capability. If that capability is not prominently disclosed in the summary and permission model, users and reviewers may underestimate the risk of secret creation, persistence, and later misuse; in this context, hidden credential handling is materially more dangerous than ordinary task execution because it can create durable access to an external account.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill explicitly handles issuance and storage of platform/workspace API keys, which is a sensitive credential-management capability. If that capability is not prominently disclosed in the summary and permission model, users and reviewers may underestimate the risk of secret creation, persistence, and later misuse; in this context, hidden credential handling is materially more dangerous than ordinary task execution because it can create durable access to an external account.

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
The skill explicitly handles issuance and storage of platform/workspace API keys, which is a sensitive credential-management capability. If that capability is not prominently disclosed in the summary and permission model, users and reviewers may underestimate the risk of secret creation, persistence, and later misuse; in this context, hidden credential handling is materially more dangerous than ordinary task execution because it can create durable access to an external account.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill explicitly handles issuance and storage of platform/workspace API keys, which is a sensitive credential-management capability. If that capability is not prominently disclosed in the summary and permission model, users and reviewers may underestimate the risk of secret creation, persistence, and later misuse; in this context, hidden credential handling is materially more dangerous than ordinary task execution because it can create durable access to an external account.

Ae1

High
Category
analysis-evasion
Content
If you install it manually, copy both `SKILL.md` and the sibling `scripts/`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
If you install it manually, copy both `SKILL.md` and the sibling `scripts/`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Session Persistence

Medium
Category
Rogue Agent
Content
Install into that default location:

```bash
mkdir -p "$HOME/.openclaw/workspace/skills/openmarlin"
rsync -a --delete \
  --exclude '.git' \
  /path/to/openmarlin-skill-directory/ \
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.

Skill Enumeration

Medium
Category
Agent Snooping
Content
After install, the main entrypoint should be:

```text
~/.openclaw/workspace/skills/openmarlin/SKILL.md
```

The helper scripts remain available relative to that installed skill directory:
Confidence
80% confidence
Finding
Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
7. Send your first routed execution with `python3 scripts/platform_request.py executions --body-json '{"instruction":"hello"}'`.
8. For long-running jobs such as video generation, default to `python3 scripts/platform_request.py tasks-submit --watch ...` so submission and polling happen in one flow.

Do not ask users to paste platform API keys into chat during setup. Registration
should issue and store the key through the bootstrap flow above.

If you plan to force a specific `provider_id` and also pass an exact `model`
Confidence
80% 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.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill declares broad operational behavior that relies on sensitive capabilities including network access, shell execution, environment-variable use, and local file read/write, but it does not declare any explicit tool scope or permission boundaries. That makes the effective privilege surface opaque to reviewers and increases the risk of overbroad execution, accidental misuse, or future prompt/content abuse through implicitly available tools.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
- store the key in the default OpenClaw auth profile when `--store` is used
- avoid echoing raw secrets unless the active command explicitly returns them
- when reporting success, show where the key was stored or loaded from
- never ask the user to paste a platform API key into chat as the normal
  registration path
- if `openmarlin.ai` appears blocked by browser or Cloudflare protection,
  verify that helpers are targeting `https://api.openmarlin.ai` before
Confidence
80% 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.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The manifest describes answering questions, running tasks, and managing OpenMarlin account setup and billing flows. This file adds a distinct `referral-link` capability that fetches referral codes, invite links, and attribution summaries, which is not necessary to implement billing, top-up, or account setup behavior and therefore extends into a separate growth/referral feature area.

Description-Behavior Mismatch

Medium
Confidence
88% confidence
Finding
The skill manifest scopes this skill to OpenMarlin questions, tasks, account setup, and billing flows. The `referral-link` subcommand and related API call retrieve referral code and attribution summary data from `/v1/referrals/me`, which is semantically outside the stated billing-focused behavior in this file.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
system = platform.system().lower()
    try:
        if system == "darwin":
            completed = subprocess.run(
                ["open", browser_url],
                check=False,
                capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
os.startfile(browser_url)  # type: ignore[attr-defined]
            return True, "Opened the authorization page in your default browser."
        else:
            completed = subprocess.run(
                ["xdg-open", browser_url],
                check=False,
                capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The script prints the newly issued API key secret directly to stdout and even emits a ready-to-export shell command containing the secret. In agent, CI, terminal-logging, or shared-session contexts, stdout is commonly captured in logs, transcripts, scrollback, or telemetry, which can leak a long-lived credential and enable unauthorized access to the OpenMarlin/OpenClaw workspace.

Static analysis

No suspicious patterns detected.