Feb 18, 2026·7 min read·7 visits
OpenClaw versions prior to v2026.2.14 are vulnerable to Local Privilege Escalation and RCE via PATH manipulation. The software automatically trusted local `node_modules` binaries over system executables and allowed external agents to override the `PATH` environment variable during execution. Attackers can hijack commands to execute arbitrary code.
OpenClaw, an open-source platform for agent-based automation, suffered from a critical set of design flaws centered around how it handled the system `PATH` variable. By prioritizing local directories over system paths and allowing request-scoped environment overrides, the platform opened the door to classic binary hijacking and arbitrary code execution. This analysis covers the three distinct vectors: unsafe bootstrapping, relative command resolution, and environment injection.
We all love developer experience (DX). We love tools that "just work" without us having to fiddle with our shell configuration for three hours. But in the world of security, "it just works" usually translates to "it just executes whatever it finds." OpenClaw, a platform for building autonomous agents, fell into this classic trap.
At its core, OpenClaw is designed to orchestrate complex workflows. To do this, it often spawns its own CLI or other helper processes. The developers, likely tired of users complaining that the openclaw binary wasn't found, decided to write a helper function: ensureOpenClawCliOnPath. It sounds innocent enough. It's helpful. It's distinctively dangerous.
The vulnerability here isn't a buffer overflow or a complex heap grooming exercise. It's a logic flaw born from the desire to make things easy. The application prioritized untrusted, user-controlled directories over the system's trusted paths. It's the digital equivalent of a bank manager accepting a handwritten ID card because it was handed to them first.
The root cause splits into a Hydra with three heads, all stemming from PATH mismanagement. The first head was the bootstrapper. When OpenClaw started, it wanted to ensure its binaries were accessible. Instead of checking if they were already there, it aggressively prepended the current working directory's node_modules/.bin to the PATH environment variable.
In Unix (and Windows) land, the PATH is processed in order. The first match wins. By putting a local, potentially user-controlled directory at the front of the line, OpenClaw effectively said: "I trust whatever is lying around in this folder more than I trust /usr/bin."
The second head was in the Agent Client Protocol (ACP). When the client needed to spawn a server instance, it didn't look for the absolute path of the binary it was currently running. It just shouted spawn("openclaw", ...) into the void. Node.js, being a compliant servant, looked at that compromised PATH, found the local impostor first, and executed it.
The third head was the node-host implementation. This component executes tasks requested by agents. It allowed these requests to pass in environment variable overrides. While it sanitized some things, it completely forgot that PATH is the master key to command resolution. An attacker could simply say, "Please run this task, but look for binaries in /tmp/evil first."
Let's look at the smoking gun in src/acp/client.ts. This is how the code was spawning the internal server before the fix:
// VULNERABLE CODE
import { spawn } from 'child_process';
export class AcpClient {
startServer() {
// Relies on system PATH, which might be poisoned
// or prepended with local node_modules/.bin
return spawn("openclaw", ["server", "--start"], {
stdio: 'inherit'
});
}
}And here is the patch from commit 013e8f6b3be3333a229a066eef26a45fec47ffcc. The developers realized that relying on a global name is suicide. They switched to resolving the absolute path of the entry point relative to the executing script.
// FIXED CODE
import { spawn } from 'child_process';
import { resolveSelfEntryPath } from '../utils/path';
export class AcpClient {
startServer() {
// Resolve absolute path to the binary
const entryPath = resolveSelfEntryPath(import.meta.url);
// Use the absolute path to the Node runtime
return spawn(process.execPath, [entryPath, "server", "--start"], {
stdio: 'inherit'
});
}
}Meanwhile, in src/node-host/invoke.ts, the environment sanitization was virtually non-existent for PATH. The fix explicitly blocks sensitive keys. It's a simple if statement, but its absence was catastrophic.
// PATCH in src/node-host/invoke.ts
export function sanitizeEnv(overrides?: Record<string, string> | null) {
const blockedEnvKeys = new Set(['LD_PRELOAD', 'PYTHONPATH', 'NODE_OPTIONS']);
for (const [key, val] of Object.entries(overrides || {})) {
const upper = key.toUpperCase();
// THE FIX: Explicitly deny PATH manipulation
if (upper === "PATH" || blockedEnvKeys.has(upper)) {
continue;
}
// ... allow other env vars
}
}Exploiting this requires very little sophistication, which makes it all the more dangerous. Let's look at the Local Binary Hijacking vector. Imagine a developer pulls down a repo that includes a malicious postinstall script (or just a checked-in binary).
openclaw (or openclaw.cmd on Windows) inside node_modules/.bin/ or the project root.openclaw run-agent.ensureOpenClawCliOnPath utility runs and prepends the current directory to PATH.spawn("openclaw").Here is a visualization of the flow:
The Network Vector is even cleaner. If you can communicate with the node-host (the component that runs agent tasks), you can send a task payload with environment overrides. sending {"env": {"PATH": "/tmp/attacker_controlled"}} forces the host to look for standard binaries (like ls, git, or docker) in your controlled directory first.
Why does this matter? OpenClaw isn't a calculator app; it's an automation platform. It's designed to wield API keys, manage cloud infrastructure, and deploy code. It holds the keys to the kingdom.
If an attacker successfully hijacks the binary, they inherit the permissions of the automation agent. In a CI/CD context or a production server, this often means:
process.env to dump AWS keys, database connection strings, and Github tokens before passing execution back to the real binary to hide the tracks.This is a classic case of "High Impact, Low Complexity." It doesn't require a race condition or a specific memory layout. It just requires the software to function exactly as it was programmed.
The fix provided in version v2026.2.14 is robust because it stops guessing.
First, they removed the automatic prepending of local directories. If you want to use a local binary, you now have to opt-in via OPENCLAW_ALLOW_PROJECT_LOCAL_BIN, and even then, it appends to the PATH rather than prepending. This ensures system binaries always take precedence.
Second, and most importantly, they switched to Absolute Path Resolution. By using process.execPath (the absolute path to the Node.js executable) and resolving the entry script's full path on disk, they bypassed the PATH lookup mechanism entirely for internal processes. You can't hijack a path if the application isn't looking for it.
For the node-host, they implemented a strict blocklist. PATH, PYTHONPATH, and LD_PRELOAD are now forbidden in request-scoped overrides.
> [!NOTE]
> If you are a developer using OpenClaw, upgrade immediately. If you cannot upgrade, audit your environment to ensure no untrusted files exist in your node_modules or working directories before running the tool.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
openclaw/openclaw OpenClaw | < 2026.2.14 | 2026.2.14 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-427 / CWE-78 |
| Attack Vector | Local & Network |
| CVSS Score | 7.8 (High) |
| Exploit Maturity | Proof-of-Concept |
| Impact | Arbitrary Code Execution |
| Patch Status | Fixed in v2026.2.14 |
Uncontrolled Search Path Element