Mar 5, 2026·7 min read·2 visits
A blocklist bypass in zeptoclaw's Android shell tool allows execution of banned commands like 'rm -rf' via argument splitting (e.g., 'rm -r -f').
The `zeptoclaw` Rust framework contains a security bypass vulnerability in its Android device shell interface (`device_shell`). The vulnerability allows attackers to execute dangerous commands, specifically recursive file deletions (`rm -rf`), by circumventing a naive blocklist implementation. The original security control relied on literal substring matching, which fails to account for argument permutations, alternative flag syntax, or binary aliasing (e.g., `busybox rm`). This flaw permits malicious agents or attackers with access to the framework's shell tool to perform destructive actions on connected Android devices.
The zeptoclaw package functions as an AI agent framework, providing various tools for interacting with external systems. One such tool, device_shell, facilitates the execution of shell commands on connected Android devices via the Android Debug Bridge (ADB). To prevent accidental or malicious data loss, the developers implemented a blocklist intended to intercept and reject dangerous commands, most notably recursive force deletion (rm -rf).
The vulnerability resides in the implementation of this blocklist. Instead of parsing the command arguments to understand the intent, the system performed a simple literal string search. It checked if the command string contained specific banned substrings such as "rm -rf" or "rm -r". This approach is fundamentally flawed when applied to command-line interfaces, where the same operation can be invoked using a multitude of syntactically different but semantically equivalent command structures.
By crafting commands that avoid the exact blocked substrings while retaining the destructive flags, an attacker can bypass the filter entirely. This allows the execution of arbitrary removal commands on the target Android file system, potentially leading to critical data loss or device bricking.
The root cause of this vulnerability is an Incomplete List of Disallowed Inputs (CWE-184) combined with Improper Neutralization of Argument Delimiters (CWE-88). The security logic relied on a "deny-list" approach using naive string matching, which is insufficient for complex grammars like POSIX shell commands.
In standard Unix-like shells (including Android's mksh or toybox shell), command arguments are tokenized based on whitespace. Flags can often be combined (bundled), separated, or reordered without changing the program's behavior. For example, the rm binary accepts flags in any order relative to the target path. The vulnerable code in zeptoclaw failed to model this behavior.
Technical Flaws:
"rm -rf". It did not detect rm invoked with separated flags (e.g., rm -r -f).busybox rm or toybox rm, which effectively execute the same logic but change the command string signature./system/bin/rm instead of just rm would bypass a filter looking for the latter if not anchored correctly.The following analysis compares the vulnerable implementation with the patched logic introduced in commit 68916c3e4f3af107f11940b27854fc7ef517058b. The original code relied on a simple iterator over a static array of banned strings.
Vulnerable Implementation (Pre-Patch):
// src/tools/android/actions.rs
// Naive check uses .contains() on the raw command string
let blocked = [
"rm -rf",
"rm -r",
// ... other patterns
];
for pattern in &blocked {
// If the command is "rm -r -f /", this check fails
// because the string "rm -rf" is not present literally.
if lower.contains(pattern) {
return Err(ZeptoError::Tool(format!("Blocked dangerous command...")));
}
}Patched Implementation (Fixed):
The fix introduces a robust parsing strategy. It tokenizes the input string and inspects the arguments semantic meaning rather than their string representation. The new logic specifically looks for the rm binary (or its aliases) and then iterates through all arguments to detect if both recursive and force flags are present in any combination.
// src/tools/android/actions.rs
// 1. Tokenize the input command
let tokens: Vec<&str> = command.split_whitespace().collect();
// 2. Identify if the binary is 'rm', '/system/bin/rm', 'busybox rm', etc.
if let Some(rm_args) = rm_invocation_args(&tokens) {
let mut has_r = false;
let mut has_f = false;
// 3. Iterate through all arguments to find dangerous flags
for &tok in rm_args {
if tok == "--recursive" { has_r = true; }
else if tok == "--force" { has_f = true; }
// Handle combined flags like '-rf', '-fr', '-vfr', etc.
else if tok.starts_with('-') && !tok.starts_with("--") {
let flags = &tok[1..];
if flags.contains('r') { has_r = true; }
if flags.contains('f') { has_f = true; }
}
}
// 4. Block only if both flags are present
if has_r && has_f {
return Err(ZeptoError::Tool("Recursive force deletion is blocked".into()));
}
}This approach correctly identifies rm -r -f, rm -fr, and rm --recursive --force as semantically identical dangerous commands.
An attacker can exploit this vulnerability by submitting specific command strings to the device_shell tool. The goal is to construct a command that instructs the underlying shell to perform a recursive deletion while ensuring the command string does not strictly match the blocked patterns.
Bypass Vectors:
Flag Splitting:
rm -rf /sdcardrm -r -f /sdcard-rf. By separating the flags with a space, the substring check fails, but the rm binary still interprets both flags.Flag Reordering:
rm -f -r /sdcard-r followed by -f.Long Flags:
rm --recursive --force /sdcard-rf), the verbose GNU/POSIX standard flags will bypass detection.Binary Aliasing:
busybox rm -rf /sdcardrm, invoking it via busybox (common on Android) changes the command signature, evading detection.Proof of Concept:
The following Rust test case, adapted from the patch, demonstrates the successful detection of previously allowed commands:
#[tokio::test]
async fn test_bypass_permutations() {
// These commands previously executed successfully
let payloads = [
"rm -r -f /sdcard",
"rm --recursive --force /sdcard",
"/system/bin/rm -fr /data/local/tmp",
"busybox rm -rf /sdcard"
];
for cmd in payloads {
// In the patched version, these now return errors
assert!(device_shell(&adb, cmd).await.is_err());
}
}The impact of this vulnerability is categorized as High. It allows for the circumvention of safety guardrails designed to prevent catastrophic data loss on connected hardware.
Operational Impact:
/sdcard) or, if the device is rooted, the system partition (/system, /data). This effectively factory resets or "bricks" the device, requiring manual recovery or reflashing.Scope:
This affects any deployment of the zeptoclaw framework where untrusted agents or users can supply commands to the device_shell interface. It is particularly critical for automated testing environments or agentic workflows where the AI might hallucinate or be tricked into generating destructive commands.
The vulnerability is addressed in zeptoclaw by moving from string-matching to semantic argument parsing. Users and developers using this crate must upgrade to the patched version immediately.
Remediation Steps:
zeptoclaw to the version containing commit 68916c3e4f3af107f11940b27854fc7ef517058b (typically v1.1.0 or later).device_shell implementation includes the is_rm_recursive_force check or equivalent token-based validation.Defense-in-Depth:
While the patch fixes the specific rm -rf bypass, developers should consider additional layers of security for shell execution:
root unless strictly required.| Product | Affected Versions | Fixed Version |
|---|---|---|
zeptoclaw qhkm | < 1.1.0 | 1.1.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-184 (Incomplete List of Disallowed Inputs) |
| Attack Vector | Local / Remote (via Agent Interface) |
| Severity | High |
| Exploit Status | Functional PoC |
| Affected Component | device_shell() function |
| Patch Commit | 68916c3e4f3af107f11940b27854fc7ef517058b |