Back to skill

Security audit

miaoda-app-builder

Security checks for vulnerabilities and agentic risk

Overview

This looks like a real Miaoda app-building integration, but it can trigger billable generation and public production publishing without built-in confirmation safeguards.

Install only if you intend agents to use your Miaoda account to create, modify, generate, download, and publish apps. Keep the API key in a secure environment mechanism rather than a skill-directory .env file, require a human confirmation before generation or publishing, and be especially careful because publish targets production by default.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/miaoda_api.py:965
Finding

Production Publishing Does Not Enforce Explicit User Authorization

Content
View full analysis

Vulnerability Details

File Location: scripts/miaoda_api.py, lines 965–981 and 1542–1556
Vulnerability Type: Missing authorization confirmation for a consequential operation
Risk Level: Medium

Complete Code Snippet

python
def publish_app(
    base_url: str,
    api_key: str,
    app_id: str,
    app_env: str = "PRODUCE",
    enable_weapp_mini_program: bool = True,
    is_release_app_square: bool = False,
) -> dict:
    """POST /api/v1/app/release — triggers a release, returns releaseId."""
    url = f"{base_url.rstrip('/')}/api/v1/app/release"
    payload = {
        "appId": app_id,
        "appEnv": app_env,
        "enableWeappMiniProgram": enable_weapp_mini_program,
        "isReleaseAppSquare": is_release_app_square,
    }
    resp = requests.post(url, json=payload, headers=auth_headers(api_key), timeout=120)
    resp.raise_for_status()
    body = resp.json()
    if body.get("status") != 0:
        msg = body.get("msg") or body.get("message") or ""
        _raise_if_insufficient_credits(msg)
        raise RuntimeError(f"Publish failed (status={body.get('status')}): {msg}")
    return body
python
elif args.command == "publish":
    if args.wait:
        result = publish_and_wait(
            args.base_url, args.api_key, args.app_id, args.env,
        )
        print(json.dumps(result, ensure_ascii=False, indent=2))
        emit_mcp_app_marker(args.base_url, args.api_key, args.app_id, result)
    else:
        result = publish_app(
            args.base_url, args.api_key, args.app_id, args.env,
        )
        print(json.dumps(result, ensure_ascii=False, indent=2))
        emit_mcp_app_marker(
            args.base_url, args.api_key, args.app_id, result, fetch_detail=False,
        )

Technical Analysis

The Skill documentation states that publishing must occur only after an explicit user request and must never be performed automatically after generation. The executable does not technically enforce that authorization ...[truncated 1881 chars]

Remediation
View remediation

Remediation Suggestions

  1. Require an explicit confirmation control for production publishing, such as:

    bash
    publish --app-id <id> --confirm-publish
    

    Reject the operation when the flag is absent.

  2. Prefer a short-lived confirmation token generated only after the user approves the exact application ID and target environment. Bind the token to:

    • The authenticated account.
    • The application ID.
    • The target environment.
    • A short expiration period.
  3. Require interactive confirmation when running in a terminal unless a validated confirmation token is supplied.

  4. Change the default environment from PRODUCE to a non-production environment, or require the environment to be specified explicitly.

  5. Fetch application details before release and enforce valid lifecycle states. Treat this as an additional readiness check, not a replacement for user authorization.

  6. Record the approved application ID, environment, timestamp, and confirmation source in an audit log without recording credentials.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/miaoda_api.py:541
Finding

Direct App Generation Bypasses the Documented Readiness and Confirmation Gate

Content
View full analysis

Vulnerability Details

File Location: scripts/miaoda_api.py, lines 541–585 and 1481–1489
Vulnerability Type: Missing server-state authorization validation for a billable operation
Risk Level: Medium

Complete Code Snippet

python
def generate_app_confirmation(
    base_url: str,
    api_key: str,
    context_id: str = "",
    app_id: Optional[str] = None,
    query_mode: str = "deep_mode",
    watch: bool = False,
    poll_interval: float = 2.0,
    fetch_timeout: int = 10,
) -> dict:
    """POST /api/v1/conversation/chat with userConfirmation: {type: "generateApp"}.

    Submits the app-generation confirmation and returns immediately with the
    server's ``state=submitted`` response.  The resolved ``appId`` and
    ``conversationId`` are printed as a JSON line so callers can capture them.
    """
    _INVALID_CONTEXT = {"", "null", "undefined", "none"}
    if app_id and context_id.strip().lower() in _INVALID_CONTEXT:
        raise RuntimeError(
            f"--app-id ({app_id}) was provided but --context-id is missing or invalid "
            f"(got: {context_id!r}).\n"
            "The platform will NOT modify the existing app — it will create a brand-new one.\n"
            "Provide --context-id <conversationId> to confirm generation for the correct app."
        )

    chat_url = f"{base_url.rstrip('/')}/api/v1/conversation/chat"
    payload = _build_chat_payload(
        text="生成应用",
        context_id=context_id,
        app_id=app_id,
        query_mode=query_mode,
        user_confirmation={"type": "generateApp"},
    )
    resp = requests.post(chat_url, json=payload, headers=auth_headers(api_key), timeout=60)
    resp.raise_for_status()
    result = resp.json()
    _check_chat_response_for_error(result)
python
elif args.command == "generate-app":
    generate_app_confirmation(
        args.base_url, args.api_key,
        context_id=args.context_id,
        app_id=args.app_id,
        query_mode=args.query_mode,
        wa
...[truncated 2405 chars]
Remediation
View remediation

Remediation Suggestions

  1. Fetch the latest conversation trajectory inside generate_app_confirmation() before submitting the request.

  2. Require all of the following conditions:

    • The application and conversation identifiers are valid and correspond to one another.
    • The latest relevant artifact has type prdAgentAction.
    • Its action list contains an event whose name is exactly generateApp.
    • The application has not already been generated.
    • The workflow has current user approval for the generation operation.
  3. Reuse _has_generate_app_button() in the direct command path rather than limiting it to the guided chat flow.

  4. Reject generation when readiness cannot be established. Do not silently fall back to sending a synthesized confirmation.

  5. Add an explicit confirmation flag or short-lived user-approval token for non-interactive invocation.

  6. Return a clear error explaining whether the failure was caused by missing readiness, stale identifiers, an already-generated application, or missing user confirmation.

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
  • Output HandlingUnvalidated Output Injection, Cross-Context Output, Unbounded Output
  • Rogue AgentSelf-Modification, Session Persistence
Findings (26)

Tainted flow: 'url' from os.environ.get (line 96, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
92% confidence
Finding

The downloader fetches an arbitrary URL with requests.get and does not restrict scheme, host, or destination trust boundaries. Because the tool can obtain download URLs from remote API responses and also rewrites network behavior via environment-driven proxy settings elsewhere in the file, this creates SSRF-style risk and unintended outbound access to attacker-controlled endpoints; in an agent setting it can also be used to pull untrusted large content for local write.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 180)May include surrounding context.

python
def _download_url_to_file(url: str, output: Optional[str] = None, default_name: str = "download.bin") -> str:
    """Download a presigned URL atomically and return the final local path."""
    with requests.get(url, stream=True, timeout=300) as resp:
        resp.raise_for_status()
        local_path = os.path.abspath(_infer_output_path(url, resp, output, default_name))
        parent_dir = os.path.dirname(local_path)

Credential Access

High
Category
Privilege Escalation
Confidence
97% confidence
Finding

The skill advises storing MIAODA_API_KEY in a plaintext .env file inside the skill directory. Secrets placed in workspace-accessible files are more likely to be read, copied, committed, or exposed by other tools and agents, enabling credential theft and unauthorized use of the Miaoda account.

Content

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

  1. Or create a .env file in the skill directory:

    bash
    echo "MIAODA_API_KEY=sk_xxxxx" > ~/.openclaw/skills/miaoda-app-builder/.env
    

Context Leakage

High
Category
Data Exfiltration
Confidence
91% confidence
Finding

The get_context_id logic scans all trajectory events and extracts the most recent conversationId from message metadata. Conversation IDs are session/context identifiers; exposing or retrieving them broadly from historical trajectory data increases the risk of context leakage across users or app sessions, especially in an agent environment that may mix prior conversation artifacts and tool outputs.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 837)May include surrounding context.

python
app_id: str,
    fetch_timeout: int = 10,
) -> Optional[str]:
    """Extract the conversationId (contextId) for an existing app by reading its trajectory.

    Scans ALL trajectory events and returns the MOST RECENT (last) non-empty contextId
    found, checking two paths per event:

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill invokes a Python CLI that uses environment variables and makes networked calls to a third-party platform, but the manifest declares no explicit tool scope or allowed-tools boundary. Without an explicit permission model, an agent runner may overgrant capabilities or invoke the skill in contexts where network and secret access were not clearly intended, increasing the blast radius of misuse.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The description emphasizes creation and deployment but does not prominently warn that publishing pushes an application to a public production environment. Users or agents may therefore underestimate the consequence of invoking publish, leading to accidental exposure of unfinished or sensitive applications.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The activation criteria are broad enough to match ordinary software or content requests that may have nothing to do with Miaoda. Overbroad routing can cause accidental invocation of a skill with deployment and account-backed side effects, sending user intent and data to an external platform unnecessarily.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The routing keywords are generic phrases like 'build a website' and 'create a blog site' without negative conditions or confirmation steps. That increases the likelihood that unrelated requests are routed into a skill that can create projects, use credentials, and potentially publish to production.

Content

No source excerpt is available for this finding.

Unbounded Output

Medium
Category
Output Handling
Confidence
60% confidence
Finding

Output size or generation rate is not bounded. Unbounded output enables denial-of-service through resource exhaustion, log flooding, or context-window stuffing.

Content

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

Only show the last 3 turns

python scripts/miaoda_api.py conversation-history --app-id app-abc123xyz --limit 3

Show full content without truncation

python scripts/miaoda_api.py conversation-history --app-id app-abc123xyz --full

text

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

Complete Workflow Examples

Example 1: Create and Deploy a New App

bash
export MIAODA_API_KEY="sk_xxxxx"

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

Each chat call prints a JSON header line {"appId": "...", "conversationId": "..."}. The platform may return an updated conversationId in this response. Always capture it and use it for the next chat call, or the next round will create a brand-new app.

bash
export MIAODA_API_KEY="sk_xxxxx"

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The example workflow tells the agent to publish after modifications, contradicting the repeated rule that publishing must occur only on explicit user request. In practice, agents often follow concrete examples over prose rules, so this inconsistency can cause unintended deployment of modified apps to production.

Content

No source excerpt is available for this finding.

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

md
Used to:

* create a new application
* refine PRD specifications
* modify an existing generated application

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The tool downloads remote content and writes it to local disk automatically without an explicit user confirmation step or a clearly bounded safe directory. In an agent skill, silent local writes increase the risk of storing malicious or unexpected artifacts, consuming disk, or tricking downstream users into opening untrusted files.

Content

No source excerpt is available for this finding.

Tainted flow: 'tmp_path' from requests.get (line 187, network input) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 189)May include surrounding context.

python
tmp_path = f"{local_path}.tmp"
        try:
            with open(tmp_path, "wb") as fh:
                for chunk in resp.iter_content(chunk_size=1024 * 1024):
                    if chunk:
                        fh.write(chunk)

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 230)May include surrounding context.

python
"size": size,
        "sort": {"direction": "desc", "property": "updatedAt"},
    }
    resp = requests.post(url, json=payload, headers=auth_headers(api_key), timeout=30)
    result = raise_for_api_error(resp)
    data = result.get("data", {})
    # Accommodate both "items" and "list" response keys (API may vary)

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 422)May include surrounding context.

python
"size": size,
        "sort": {"direction": "desc", "property": "updatedAt"},
    }
    resp = requests.post(url, json=payload, headers=auth_headers(api_key), timeout=30)
    result = raise_for_api_error(resp)
    data = result.get("data", {})
    # Accommodate both "items" and "list" response keys (API may vary)

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 981)May include surrounding context.

python
"size": size,
        "sort": {"direction": "desc", "property": "updatedAt"},
    }
    resp = requests.post(url, json=payload, headers=auth_headers(api_key), timeout=30)
    result = raise_for_api_error(resp)
    data = result.get("data", {})
    # Accommodate both "items" and "list" response keys (API may vary)

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 1140)May include surrounding context.

python
"size": size,
        "sort": {"direction": "desc", "property": "updatedAt"},
    }
    resp = requests.post(url, json=payload, headers=auth_headers(api_key), timeout=30)
    result = raise_for_api_error(resp)
    data = result.get("data", {})
    # Accommodate both "items" and "list" response keys (API may vary)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

The chat path sends user-supplied text and, when provided, detailed UI-node metadata to a remote API via HTTP requests, but there is no explicit runtime notice that this data is being transmitted. The command help describes functionality, yet it does not warn that selected file paths, line ranges, tags, and styles may be sent to the external service.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 471)May include surrounding context.

python
chat_url = f"{base_url.rstrip('/')}/api/v1/conversation/chat"
    payload = _build_chat_payload(text, context_id, app_id, query_mode, input_field_type, node=node)

    resp = requests.post(chat_url, json=payload, headers=auth_headers(api_key), timeout=60)
    resp.raise_for_status()
    chat_result = resp.json()
    _check_chat_response_for_error(chat_result)

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 585)May include surrounding context.

python
chat_url = f"{base_url.rstrip('/')}/api/v1/conversation/chat"
    payload = _build_chat_payload(text, context_id, app_id, query_mode, input_field_type, node=node)

    resp = requests.post(chat_url, json=payload, headers=auth_headers(api_key), timeout=60)
    resp.raise_for_status()
    chat_result = resp.json()
    _check_chat_response_for_error(chat_result)

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The conversation-history feature exposes cmd_run_observation entries, which can reveal outputs of commands previously executed in the app-building workspace. In this skill context, those observations may contain secrets, filesystem details, tokens, or internal build data unrelated to the user's immediate need, so the leakage risk is real.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

The publish command can trigger an external release of an application with production-oriented defaults and no confirmation prompt. In an app-builder skill, publishing is a high-impact state-changing action that can accidentally expose unfinished apps, overwrite public versions, or incur operational/business consequences.

Content

No source excerpt is available for this finding.

Tainted flow: 'app_id' from requests.post (line 397, network input) → requests.post (network output)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/miaoda_api.py (reported line 1140)May include surrounding context.

python
"""POST /api/v1/app/operation/download — return full source ZIP download info."""
    url = f"{base_url.rstrip('/')}/api/v1/app/operation/download"
    payload = {"appId": app_id, "env": app_env}
    resp = requests.post(url, json=payload, headers=_app_context_headers(api_key, app_id, app_env), timeout=60)
    return raise_for_api_error(resp)

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The file contains user-facing output in Chinese and also defaults chat payload language to 'zh', which can force a specific language/locale experience without user opt-in. There is no visible CLI option or documented choice allowing users to select their preferred language.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.