Back to skill

Security audit

appium-android-adb

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed Android phone automation bridge, but it needs review because it exposes direct tap and typing controls on a connected phone without enforcing its own confirmation rule for sensitive actions.

Install only if you intentionally want an agent to control a USB-connected Android phone. Keep Appium bound to 127.0.0.1, use one-shot commands when possible, stop the daemon after use, and require explicit human confirmation before any booking, checkout, order submission, payment-adjacent tap, or typed sensitive information.

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

T09 · Insecure Skill Coding Practices

Warning
Location
bridge_daemon.py:648
Finding

Device-Control Entry Points Do Not Enforce the Documented User-Confirmation Boundary

Content
View full analysis

Vulnerability Details

File Location: SKILL.md:267-273; ocr_bridge.py:323-334; bridge_daemon.py:648-658, 731-753
Vulnerability Type: Missing authorization enforcement for state-changing device actions
Risk Level: Medium

Relevant Code

The Skill documentation requires confirmation before state-changing actions:

text
- **Confirm before irreversible actions**: before submitting an order, a
  checkout step, or any tap that changes live app state, tell the user what
  will happen and wait for confirmation. Payment credentials (PIN,
  fingerprint) are ALWAYS entered by the user — scripts never request or
  store them.

However, the primary OCR entry point immediately performs requested taps without requiring evidence of confirmation:

python
elif cmd == "tap-text":
    text = rest[0]
    tap_text(text, idx=int(opt("--idx", 0)),
             x0=opt("--x0"), x1=opt("--x1"),
             y0=opt("--y0"), y1=opt("--y1"))
    for x in shot():
        print(x)
elif cmd == "tap":
    u2.connect().click(int(rest[0]), int(rest[1]))
    time.sleep(2.5)
    for x in shot():
        print(x)

The Appium backend similarly exposes state-changing handlers and executes them directly:

python
ACTIONS = {
    "dump": cmd_dump, "tap": cmd_tap, "tap_bounds": cmd_tap_bounds,
    "tap_coords": cmd_tap_coords, "find": cmd_find,
    "scroll": cmd_scroll, "js_scroll": cmd_js_scroll,
    "screenshot": cmd_screenshot,
    "type": cmd_type, "wait": cmd_wait,
    "ocr_tap": cmd_ocr_tap,
    "find_book_button": cmd_find_book_button,
}
python
action = sys.argv[1]
args = {}
if len(sys.argv) > 2:
    try:
        args = json.loads(sys.argv[2])
    except json.JSONDecodeError:
        print(json.dumps({"ok": False, "error": "invalid json args"}))
        sys.exit(1)

if _daemon_alive():
    # Send command to daemon
    cmd = json.dumps({"action": action, "args": args})
    _safe_write(CMD_FILE, cmd)

Technical Analysis

The documented confirmat ...[truncated 2360 chars]

Remediation
View remediation

Remediation Suggestions

  1. Require an explicit confirmation artifact for state-changing commands, rather than relying solely on agent instructions.
  2. Introduce a short-lived, single-use confirmation token bound to:
    • the intended action;
    • the target text, bounds, or coordinates;
    • the active package and activity;
    • an expiration time.
  3. Reject sensitive tap commands unless the caller provides a valid token generated after presenting the exact action summary to the user.
  4. Separate read-only and state-changing interfaces. Keep dump, find, screenshot, and equivalent inspection commands available without confirmation, while gating click and typing operations.
  5. Add a dry-run mode that resolves the target and returns the proposed coordinates and application context without sending input.
  6. Re-check the active package, activity, and target immediately before execution so a confirmation cannot be reused after the screen changes.
  7. Ensure dependent Skills cannot bypass the gate by invoking lower-level helpers such as u2.connect().click() through an exposed wrapper.
  8. Add automated tests verifying that all click-capable handlers reject execution when confirmation is absent, expired, mismatched, or already consumed.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (25)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The declared description says the skill’s primary backend is direct uiautomator2 + RapidOCR via ocr_bridge.py, with Appium/bridge_daemon.py retained only as a fallback. The supplied code chunk, however, is entirely focused on setting up and validating the Appium fallback path: it checks ADB connectivity, starts Appium, launches bridge_daemon.py, and tests daemon-based dumping. There is no OCR, RapidOCR, screenshot processing, click/swipe re-verification loop, or direct uiautomator2 logic in this chunk. While this behavior is related to the broader Android automation domain and is explicitly mentioned in the description as a fallback component, the actual code’s primary purpose is materially different from the declared primary emphasis. So this is a description-behavior mismatch for this specific code chunk, though not a malicious or unrelated one.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · bridge_daemon.py (reported line 139)May include surrounding context.

python
# this it binds broadly and exposes a full Android-control API to the
        # local network — exactly what the ClawHub review flagged as [T05].
        ["appium", "--address", APPIUM_BIND, "--log", str(IPC_DIR / "appium.log")],
        env={**os.environ, "ANDROID_HOME": android_home,
             "ANDROID_SDK_ROOT": android_home},
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    )

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
95% confidence
Finding

Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Content

Scanner excerpt · start_bridge.sh (reported line 16)May include surrounding context.

sh
[ "$pid" = "$$" ] && continue
    kill "$pid" 2>/dev/null || true
done
rm -f ~/.cache/appium-bridge/bridge_cmd ~/.cache/appium-bridge/bridge_resp ~/.cache/appium-bridge/bridge.lock
sleep 1

# 2. Verify ADB connection

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
95% confidence
Finding

Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Content

Scanner excerpt · start_bridge.sh (reported line 53)May include surrounding context.

sh
[ "$pid" = "$$" ] && continue
    kill "$pid" 2>/dev/null || true
done
rm -f ~/.cache/appium-bridge/bridge_cmd ~/.cache/appium-bridge/bridge_resp ~/.cache/appium-bridge/bridge.lock
sleep 1

# 2. Verify ADB connection

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding

The skill advertises powerful capabilities including shell, file read/write, environment access, and network-facing components, but it does not declare any explicit tool scope or permission boundary. That makes review and enforcement harder and increases the chance that dependent skills or operators invoke it with broader access than intended, especially because it can control a connected phone and launch auxiliary processes.

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

md
The bare `$PY ocr_bridge.py …` form above works here because you're in this skill's directory.
**Dependent skills invoke it as `$PY $BASE/ocr_bridge.py …`** instead — `$BASE` being this skill's
path — so their commands run from any working directory. Use `$BASE` in anything you write that has
to work outside this directory.

## The Proven Loop

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

The module header describes a persistent Appium session bridge, but the file exposes a broader set of automation actions including screenshot capture, OCR-driven tapping, JavaScript execution in WebViews, freeform typing, and direct uiautomator2 access. That mismatch increases the risk that downstream users or orchestrators grant it more trust or permission than its documented scope warrants, enabling unintended device control and data exposure.

Content

No source excerpt is available for this finding.

Unsafe Defaults

Medium
Category
Tool Misuse
Confidence
70% confidence
Finding

Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Content

Scanner excerpt · bridge_daemon.py (reported line 51)May include surrounding context.

python
def _safe_read(path: Path) -> str | None:
    """Read an IPC file only if it is owned by the current user and not
    group/world-writable (rejects foreign or tamperable files)."""
    try:
        st = path.stat()
    except FileNotFoundError:

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 · bridge_daemon.py (reported line 83)May include surrounding context.

python
import subprocess
    try:
        with open(dest, "wb") as fh:
            subprocess.run(["adb", "exec-out", "screencap", "-p"],
                           stdout=fh, timeout=10, check=False)
        if dest.stat().st_size > 0:
            return 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 · bridge_daemon.py (reported line 91)May include surrounding context.

python
pass
    device_path = "/sdcard/bridge_scr.png"
    try:
        subprocess.run(["adb", "shell", "screencap", "-p", device_path],
                       capture_output=True, timeout=10)
        subprocess.run(["adb", "pull", device_path, str(dest)],
                       capture_output=True, timeout=10)

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 · bridge_daemon.py (reported line 93)May include surrounding context.

python
try:
        subprocess.run(["adb", "shell", "screencap", "-p", device_path],
                       capture_output=True, timeout=10)
        subprocess.run(["adb", "pull", device_path, str(dest)],
                       capture_output=True, timeout=10)
    finally:
        subprocess.run(["adb", "shell", "rm", "-f", device_path],

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 · bridge_daemon.py (reported line 96)May include surrounding context.

python
subprocess.run(["adb", "pull", device_path, str(dest)],
                       capture_output=True, timeout=10)
    finally:
        subprocess.run(["adb", "shell", "rm", "-f", device_path],
                       capture_output=True, timeout=10)
    return dest.exists() and dest.stat().st_size > 0

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 · bridge_daemon.py (reported line 130)May include surrounding context.

python
if not android_home:
        android_home = os.path.expanduser("~/android-sdk")  # Linux default

    proc = subprocess.Popen(
        # No insecure features needed: the daemon only uses standard
        # UiAutomator2 commands (find/click, mobile: clickGesture/scrollGesture).
        #

Tainted flow: 'android_home' from os.environ.get (line 117, credential/environment) → subprocess.Popen (code execution)

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 · bridge_daemon.py (reported line 130)May include surrounding context.

python
if not android_home:
        android_home = os.path.expanduser("~/android-sdk")  # Linux default

    proc = subprocess.Popen(
        # No insecure features needed: the daemon only uses standard
        # UiAutomator2 commands (find/click, mobile: clickGesture/scrollGesture).
        #

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · bridge_daemon.py (reported line 215)May include surrounding context.

python
if text == "预订" and clickable:
            entry = {"text": "预订", "bounds": bounds, "x": 0, "y": 0}
            m = __import__('re').findall(r'\d+', bounds)
            if len(m) >= 4:
                entry["x"] = (int(m[0]) + int(m[2])) // 2
                entry["y"] = (int(m[1]) + int(m[3])) // 2

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · bridge_daemon.py (reported line 225)May include surrounding context.

python
if text == "预订" and clickable:
            entry = {"text": "预订", "bounds": bounds, "x": 0, "y": 0}
            m = __import__('re').findall(r'\d+', bounds)
            if len(m) >= 4:
                entry["x"] = (int(m[0]) + int(m[2])) // 2
                entry["y"] = (int(m[1]) + int(m[3])) // 2

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · bridge_daemon.py (reported line 235)May include surrounding context.

python
if text == "预订" and clickable:
            entry = {"text": "预订", "bounds": bounds, "x": 0, "y": 0}
            m = __import__('re').findall(r'\d+', bounds)
            if len(m) >= 4:
                entry["x"] = (int(m[0]) + int(m[2])) // 2
                entry["y"] = (int(m[1]) + int(m[3])) // 2

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · bridge_daemon.py (reported line 373)May include surrounding context.

python
# Annotate collapsed bounds
    for r in results:
        m = __import__('re').findall(r'\d+', r.get("bounds", ""))
        if len(m) >= 4:
            h = int(m[3]) - int(m[1])
            r["height_px"] = h

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 · ocr_bridge.py (reported line 80)May include surrounding context.

python
"""
    try:
        with open(dest, "wb") as fh:
            subprocess.run(["adb", "exec-out", "screencap", "-p"],
                           stdout=fh, timeout=15, check=False)
        if os.path.getsize(dest) > 0:
            return os.path.getsize(dest)

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 · ocr_bridge.py (reported line 86)May include surrounding context.

python
return os.path.getsize(dest)
    except (OSError, subprocess.SubprocessError):
        pass
    subprocess.run(["adb", "shell", "screencap", "-p", DEVICE_SHOT], timeout=15)
    subprocess.run(["adb", "pull", DEVICE_SHOT, dest], timeout=15)
    subprocess.run(["adb", "shell", "rm", "-f", DEVICE_SHOT], timeout=15)
    return os.path.getsize(dest) if os.path.exists(dest) else 0

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 · ocr_bridge.py (reported line 87)May include surrounding context.

python
except (OSError, subprocess.SubprocessError):
        pass
    subprocess.run(["adb", "shell", "screencap", "-p", DEVICE_SHOT], timeout=15)
    subprocess.run(["adb", "pull", DEVICE_SHOT, dest], timeout=15)
    subprocess.run(["adb", "shell", "rm", "-f", DEVICE_SHOT], timeout=15)
    return os.path.getsize(dest) if os.path.exists(dest) else 0

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 · ocr_bridge.py (reported line 88)May include surrounding context.

python
pass
    subprocess.run(["adb", "shell", "screencap", "-p", DEVICE_SHOT], timeout=15)
    subprocess.run(["adb", "pull", DEVICE_SHOT, dest], timeout=15)
    subprocess.run(["adb", "shell", "rm", "-f", DEVICE_SHOT], timeout=15)
    return os.path.getsize(dest) if os.path.exists(dest) else 0

Session Persistence

Medium
Category
Rogue Agent
Confidence
65% 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 · start_bridge.sh (reported line 40)May include surrounding context.

sh
# --address 127.0.0.1: loopback only. Appium exposes full control of the
    # connected device; without this it binds broadly and any host on the LAN
    # could drive the phone.
    nohup appium --address 127.0.0.1 --log "$HOME/.cache/appium-bridge/appium.log" > /dev/null 2>&1 &
    sleep 4
fi

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 · start_bridge.sh (reported line 55)May include surrounding context.

sh
# 5. Start daemon (pre-warms session)
rm -f ~/.cache/appium-bridge/bridge_cmd ~/.cache/appium-bridge/bridge_resp
python3 ~/.openclaw/workspace/skills/appium-android-adb/bridge_daemon.py --daemon &
sleep 6  # Wait for daemon to create session

# 6. Test daemon with a quick dump
RESP=$(python3 ~/.openclaw/workspace/skills/appium-android-adb/bridge_daemon.py dump 2>/dev/null)

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
74% confidence
Finding

The docstring says 'Uses uiautomator2 (not Appium) to avoid stale sessions', but this function lives inside the Appium bridge daemon and its surrounding file is documented as an Appium session daemon. While the code really does call uiautomator2 directly, that intent-level documentation conflicts with the file's stated role and backend expectations, creating misleading documentation about what backend this daemon actually uses.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.