Skip to content

Arbitrary shell command execution via unrestricted WebView JavaScript bridge in runCommandAsync #136

Description

@3em0

Submission Target

The repository does not contain a .github/ISSUE_TEMPLATE bug-report template. It does contain SECURITY.md, which says not to open public GitHub issues for security vulnerabilities and to report them through GitHub Security Advisories:

https://github.com/AidanPark/openclaw-android/security/advisories/new

Affected Versions

  • Confirmed affected: OpenClaw Android 0.4.0, commit cfb0740fc0961f1dd1c2a22ecf133eae443fa96f
  • Other versions: Not verified
  • Fixed: Not available at the time of reporting

Description

The Android app exposes a native JavaScript bridge named window.OpenClaw to WebView content. The bridge includes runCommand and runCommandAsync, both of which accept a caller-controlled command string from JavaScript and pass it directly to the command runner.

The command runner executes the supplied string with sh -c, so shell metacharacters such as ;, &&, pipes, redirects, and command substitution are interpreted by the shell. There is no command allowlist, argument separation, origin-bound authorization check, or validation step before execution.

As a result, any attacker who obtains JavaScript execution in the app WebView can escalate that capability into arbitrary shell command execution under the app's Termux/OpenClaw runtime context.

Evidence

The WebView enables JavaScript, permits broad file URL access, registers the native bridge, and loads local WebView content:

# https://github.com/AidanPark/openclaw-android/blob/cfb0740fc0961f1dd1c2a22ecf133eae443fa96f/android/app/src/main/java/com/openclaw/android/MainActivity.kt#L130-L172
binding.webView.apply {
    clearCache(true)
    settings.javaScriptEnabled = true
    settings.domStorageEnabled = true
    settings.allowFileAccess = true
    @Suppress("DEPRECATION")
    settings.allowFileAccessFromFileURLs = true
    @Suppress("DEPRECATION")
    settings.allowUniversalAccessFromFileURLs = true
    settings.cacheMode = android.webkit.WebSettings.LOAD_NO_CACHE
    addJavascriptInterface(jsBridge, "OpenClaw")
    ...
}
...
binding.webView.loadUrl(url)

The bridge exposes runCommandAsync and passes the JavaScript-controlled cmd directly to CommandRunner.runStreaming:

# https://github.com/AidanPark/openclaw-android/blob/cfb0740fc0961f1dd1c2a22ecf133eae443fa96f/android/app/src/main/java/com/openclaw/android/JsBridge.kt#L447-L468
@JavascriptInterface
fun runCommandAsync(
    callbackId: String,
    cmd: String,
) {
    launchWithErrorHandling(
        errorEventType = "command_output",
        errorContext = mapOf("callbackId" to callbackId, "done" to true),
    ) {
        val env = EnvironmentBuilder.build(activity)
        CommandRunner.runStreaming(cmd, env, bootstrapManager.homeDir) { output ->
            eventBridge.emit(
                "command_output",
                mapOf("callbackId" to callbackId, "data" to output, "done" to false),
            )
        }
        eventBridge.emit(
            "command_output",
            mapOf("callbackId" to callbackId, "data" to "", "done" to true),
        )
    }
}

CommandRunner.runStreaming executes the string through sh -c:

# https://github.com/AidanPark/openclaw-android/blob/cfb0740fc0961f1dd1c2a22ecf133eae443fa96f/android/app/src/main/java/com/openclaw/android/CommandRunner.kt#L55-L77
suspend fun runStreaming(
    command: String,
    env: Map<String, String>,
    workDir: File,
    onOutput: (String) -> Unit,
) = withContext(Dispatchers.IO) {
    try {
        val shell = env["PREFIX"]?.let { "$it/bin/sh" } ?: "/system/bin/sh"
        val pb = ProcessBuilder(shell, "-c", command)
        pb.environment().clear()
        pb.environment().putAll(env)
        pb.directory(workDir)
        pb.redirectErrorStream(true)

        val process = pb.start()
        process.inputStream.bufferedReader().forEachLine { line ->
            onOutput(line)
        }
        process.waitFor()
    } catch (e: Exception) {
        onOutput("Error: ${e.message}")
    }
}

Steps to Reproduce

Use a controlled debug or instrumentation environment where JavaScript can be evaluated inside the app WebView.

  1. Check out the affected commit:
git checkout cfb0740fc0961f1dd1c2a22ecf133eae443fa96f
  1. Build and launch the Android app in a test device or emulator.

  2. Execute the following JavaScript in the app WebView context:

window.OpenClaw.runCommandAsync(
  "poc",
  "printf openclaw-poc > $HOME/openclaw-bridge-poc.txt"
)
  1. Verify from the same runtime or terminal that $HOME/openclaw-bridge-poc.txt was created with the content openclaw-poc.

  2. Repeat with a compound command to confirm shell metacharacter interpretation:

window.OpenClaw.runCommandAsync(
  "poc2",
  "printf first; printf second > $HOME/openclaw-bridge-poc-2.txt"
)

The second command after the semicolon is executed, demonstrating that the JavaScript-controlled command string is interpreted by the shell.

Impact Assessment

This vulnerability allows an attacker with WebView JavaScript execution to:

  • Execute arbitrary shell commands under the app's OpenClaw/Termux runtime context
  • Read, modify, or delete files available to that runtime, including local configuration and CLI credential files
  • Download or run additional scripts, alter update/setup scripts, or persist malicious changes inside the app runtime

This is not a zero-click remote RCE by itself. The primary exploitation precondition is JavaScript execution inside the OpenClaw WebView, such as compromised WebView assets, an XSS-like bug in the UI, or a compromised WebView update supply chain. However, WebView JavaScript execution alone normally does not imply shell command execution; this bridge turns that condition into native command execution.

Suggested Fix

Avoid exposing unrestricted shell execution to WebView JavaScript.

Recommended changes:

  1. Remove runCommand and runCommandAsync from the public JavaScript bridge if they are not strictly required.
  2. Replace arbitrary command strings with a small allowlist of named operations, such as getNodeVersion or getGitVersion.
  3. Avoid sh -c for WebView-controlled input. Execute fixed binaries with explicit argument arrays through ProcessBuilder.
  4. Validate command requests on the native side, not only in TypeScript or UI code.
  5. Reduce WebView bridge exposure and origin risk by limiting file URL access where possible and adding strict navigation/origin controls.
  6. Treat downloaded or OTA WebView assets as privileged code because they can reach the native bridge.

Reporter Notes

No public GitHub issue should be opened for this report because SECURITY.md explicitly asks reporters not to disclose security vulnerabilities through public issues.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions