Feb 20, 2026·6 min read·8 visits
OpenClaw tried to stop AI agents from reading sensitive files by checking if files existed before running commands. Ironically, this check created a side-channel: if the system blocked the command, the attacker knew the file existed. If it didn't, the file was missing.
A logic flaw in the OpenClaw AI agent framework's command validation layer created a boolean side-channel, allowing attackers to probe the host filesystem. By attempting to prevent agents from accessing sensitive files via 'safe' binaries, the validation logic inadvertently revealed the existence of those files through error message discrepancies.
In the brave new world of Agentic AI, we want our digital minions to be capable but not too capable. We give them tools—specifically, shell access. But giving an LLM raw bash access is usually a résumé-generating event for security teams. So, frameworks like OpenClaw implement a concept called safeBins.
The idea is simple: allow the agent to run benign utilities like grep, sort, jq, and sed, but wrap them in a warm blanket of validation logic to ensure they aren't used to exfiltrate /etc/shadow or nuke the filesystem. It’s the 'sandbox' approach to shell execution.
However, in OpenClaw versions prior to 2026.2.19, this safety blanket had a hole in it. The mechanism designed to prevent the agent from touching sensitive files actually gave the agent a flashlight to find them. It turns out that when your security check involves asking the operating system, "Does this secret file exist?" and then acting differently based on the answer, you've just built a classic Oracle.
The vulnerability lies in src/infra/exec-approvals-allowlist.ts. The developers wanted to ensure that when an agent calls a binary like grep, it isn't pointing it at a sensitive file on the disk. To achieve this, they implemented a check that iterates through every token in the command's argument list (argv).
Here is where the logic went sideways. The validator used a helper function, essentially a wrapper around fs.existsSync, to check if an argument resolved to a real path on the host system. If the token resolved to an existing file, the validator would throw a specific error (allowlist miss) and block the execution.
Do you see the problem? It’s a boolean side-channel.
If I ask the agent to run grep pattern /etc/passwd and the system rejects it because "Wait, that's a real file!", I have confirmed the file exists. If I ask for grep pattern /etc/unicorn_tears and the system says "Command failed" (because grep couldn't find the file) or allows it to proceed, I know the file does not exist. The security mechanism itself became the very thing it was trying to prevent: a filesystem enumeration tool.
Let's look at the smoking gun. The vulnerable logic was relying on the filesystem state to make security decisions. This is a cardinal sin in secure coding—validation should be syntactic, not semantic regarding the environment state.
The Vulnerable Logic:
// Inside isSafeBinUsage loop
function defaultFileExists(filePath: string): boolean {
try {
return fs.existsSync(filePath);
} catch {
return false;
}
}
// ... later in the validation loop ...
if (exists(path.resolve(cwd, token))) {
// THE LEAK: The decision to return false (deny) depends on the disk state.
return false;
}The Fix (Commit bafdbb6f112409a65decd3d4e7350fbd637c7754):
The patch, authored by Peter Steinberger, rips out the fs.existsSync dependency entirely. Instead of asking the disk, it parses the command arguments deterministically. It introduces SAFE_BIN_PROFILES—strict definitions for each binary that dictate exactly which flags are allowed (e.g., blocking -f in grep or -o in sort) and how many positional arguments can be passed.
// The new approach: Deterministic parsing
const profile = SAFE_BIN_PROFILES[binaryName];
// Blocked flags are rejected regardless of what follows them
if (profile.blockedFlags.has(arg)) {
return false;
}
// Positional arguments are counted, not checked against the disk
if (!arg.startsWith("-")) {
positionalCount++;
if (positionalCount > profile.maxPositional) {
return false;
}
}By moving to a syntax-based allowlist, the response is now constant regardless of whether /secret/config.json actually exists.
Exploiting this is trivially easy for anyone controlling the agent's input or instructions. We don't need memory corruption; we just need to observe the error messages. Let's assume we want to map out the server's directory structure.
Step 1: Calibration First, we establish our baseline responses.
Probe: sort /etc/passwd (We know this exists on Linux)
Response: Error: Execution denied: allowlist miss (The Oracle confirms existence)
Probe: sort /etc/does_not_exist_12345
Response: Error: Command failed (or a generic execution error because sort ran and failed)
Step 2: The Attack Now we script the agent to brute-force interesting paths.
const targets = [
"/root/.ssh/id_rsa",
"/home/user/.aws/credentials",
"/var/run/docker.sock",
"/proc/1/environ"
];
for (const target of targets) {
const result = await agent.exec(`grep -l foo ${target}`);
if (result.error.includes("allowlist miss")) {
console.log(`[+] FOUND: ${target}`);
} else {
console.log(`[-] MISS: ${target}`);
}
}This allows an attacker to perform reconnaissance without ever successfully executing a command against a file. It's akin to a blind SQL injection, but for the filesystem.
You might look at the CVSS score of 4.3 and think, "Meh, it's just information disclosure." And sure, on its own, knowing a file exists isn't the same as reading it. But in the context of an attack chain, reconnaissance is king.
Knowing exactly where the config.json lives, or confirming the presence of a specific Docker socket, or determining which version of a library is installed by probing for version-specific files, allows an attacker to tailor their next stage payload with precision.
Furthermore, this vulnerability exposes the presence of security tools, logs, or other artifacts that an attacker might want to avoid or target. In a containerized environment, mapping the filesystem is often step one in a container breakout. The oracle provided by OpenClaw was a free map.
The mitigation is straightforward: Update OpenClaw to version 2026.2.19 or later. The fix changes the validation philosophy from "Check the world" to "Check the syntax."
If you are building your own command execution validators, take this as a lesson: Never use environmental state (file existence, network reachability, user presence) as a condition for input validation if the failure state is visible to the user. It will always leak information.
Instead, use strict allowlists for arguments. If grep supports a -f flag that reads a file, don't check if the file exists—just ban the -f flag entirely if you don't want files being read. Syntax is predictable; the filesystem is not.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
openclaw OpenClaw | <= 2026.2.17 | 2026.2.19 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-203 |
| Attack Vector | Network (Agent Session) |
| CVSS | 4.3 (Medium) |
| Risk | Filesystem Enumeration |
| Impact | Information Disclosure |
| Exploit Status | PoC Available |
Observable Discrepancy