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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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
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.

Content

Scanner excerpt · README.md (reported line 106)May include surrounding context.

The persisted OpenClaw config path is:

text
skills.entries["openmarlin"].env

So OpenClaw can remember values such as:

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
If you install it manually, copy both `SKILL.md` and the sibling `scripts/`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
If you install it manually, copy both `SKILL.md` and the sibling `scripts/`

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

Install into that default location:

bash
mkdir -p "$HOME/.openclaw/workspace/skills/openmarlin"
rsync -a --delete \
  --exclude '.git' \
  /path/to/openmarlin-skill-directory/ \

Skill Enumeration

Medium
Category
Agent Snooping
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.

Content

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

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:

Autonomous Decision Making

Medium
Category
Excessive Agency
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.

Content

Scanner excerpt · README.md (reported line 160)May include surrounding context.

md
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`

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Autonomous Decision Making

Medium
Category
Excessive Agency
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.

Content

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

md
- 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

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/registration_session.py (reported line 241)May include surrounding context.

python
system = platform.system().lower()
    try:
        if system == "darwin":
            completed = subprocess.run(
                ["open", browser_url],
                check=False,
                capture_output=True,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/registration_session.py (reported line 251)May include surrounding context.

python
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,

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.