Mar 4, 2026·6 min read·19 visits
OpenClaw's webhook transform loader failed to resolve symbolic links, allowing attackers to bypass directory restrictions. By linking a valid path to an external malicious file, attackers could achieve Remote Code Execution. The issue was patched by enforcing `realpath` verification on module paths.
A critical path traversal vulnerability exists in the OpenClaw infrastructure, specifically within the webhook transform module loader. The vulnerability arises from improper resolution of symbolic links when validating module paths against a restricted directory allowlist. By creating a symbolic link within the allowed directory that points to a file outside of it, an attacker can bypass the containment check and force the application to load and execute arbitrary JavaScript or TypeScript files from the filesystem. This flaw allows for Remote Code Execution (RCE) if an attacker can introduce a symbolic link into the configured transforms directory.
The OpenClaw platform provides a webhook gateway that allows users to define "transform" modules. These modules are JavaScript or TypeScript files used to process incoming webhook payloads before they are forwarded to their final destination. To ensure security, the system enforces a containment policy, requiring that all transform modules reside within a specific directory, typically defined in hooks.transformsDir.
The vulnerability, identified as GHSA-659F-22XC-98F2, resides in the logic responsible for enforcing this containment. The validation mechanism relied solely on lexical path analysis to determine if a requested module path was inside the authorized directory. Lexical analysis treats paths as strings and does not query the filesystem to determine the actual physical location of the file.
Consequently, the system failed to detect symbolic links (symlinks) that traverse out of the allowed directory. If an attacker can create a symlink within the transformsDir that points to a file elsewhere on the system (such as /tmp/malicious.js or system configuration files), the lexical check would pass because the symlink's path string appears valid. When the application subsequently loads the module using Node.js's dynamic import() or require(), the runtime follows the symlink and executes the target code, resulting in arbitrary code execution or information disclosure.
The root cause of this vulnerability is the use of path.relative() for security boundary checks without resolving the canonical path of the input. This is a classic instance of CWE-59: Improper Link Resolution Before File Access.
The vulnerable code essentially performed the following check:
...While this logic correctly handles directory traversal characters (e.g., ../../etc/passwd) in the string itself, it ignores the filesystem layer. In Unix-like systems, a file at /app/hooks/transforms/link.js can be a symbolic link to /etc/passwd.
From the perspective of path.resolve and path.relative, /app/hooks/transforms/link.js is strictly inside /app/hooks/transforms. The standard library's path manipulation functions operate on the abstract string representation of the path. They do not stat the file or check for inode redirection. The security check therefore validated the pointer (the symlink name) rather than the destination (the actual file). When the runtime later accessed the file to load the code, the operating system resolved the link, effectively bypassing the application's intended sandbox.
The remediation involved moving from a lexical check to a physical filesystem check using fs.realpath. The following analysis highlights the critical changes in src/gateway/hooks-mapping.ts.
The original implementation relied on path.resolve and path.relative. Note the absence of filesystem calls (fs.*) in the validation phase:
// VULNERABLE IMPLEMENTATION
function resolveContainedPath(baseDir: string, target: string, label: string): string {
const base = path.resolve(baseDir);
const resolved = resolvePath(base, target);
// BUG: Lexical check only. If 'resolved' is a symlink,
// path.relative calculates the distance to the link, not the target.
const relative = path.relative(base, resolved);
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new Error(`${label} module path must be within ${base}`);
}
return resolved;
}The fix introduces safeRealpathSync to resolve the canonical path (stripping away symlinks) before performing the containment check. It also includes resolveExistingAncestor to handle cases where the full path might not exist yet but the traversal occurs in a parent directory.
// PATCHED IMPLEMENTATION
function resolveContainedPath(baseDir: string, target: string, label: string): string {
const base = path.resolve(baseDir);
// ... (initial resolution)
// 1. Resolve the real physical path of the base directory
const baseRealpath = safeRealpathSync(base);
// 2. Resolve the real physical path of the target (or its nearest existing ancestor)
const existingAncestor = resolveExistingAncestor(resolved);
const existingAncestorRealpath = existingAncestor ? safeRealpathSync(existingAncestor) : null;
// 3. Verify the physical path is contained within the physical base
if (
baseRealpath &&
existingAncestorRealpath &&
escapesBase(baseRealpath, existingAncestorRealpath) // Uses path.relative on realpaths
) {
throw new Error(`${label} module path must be within ${base}: ${target}`);
}
return resolved;
}The function escapesBase (helper) re-implements the path.relative check, but because it is now fed the output of fs.realpathSync, the check validates the true location of the file on the disk.
To exploit this vulnerability, an attacker requires the ability to create a symbolic link within the configured transformsDir. This could be achieved via a separate file upload vulnerability, compromised developer credentials, or a misconfigured NFS/shared mount.
The following steps demonstrate the attack vector using the provided test case data:
/tmp/pwn.mjs containing export default () => process.exit(1) or a reverse shell payload.openclaw/hooks/transforms/innocent.mjs) pointing to /tmp/pwn.mjs.innocent.mjs.innocent.mjs. The lexical check confirms it is inside transforms. The loader imports innocent.mjs, forcing Node.js to read and execute /tmp/pwn.mjs.Official PoC (TypeScript):
// Source: src/gateway/hooks-mapping.test.ts
it.runIf(process.platform !== "win32")(
"rejects transform module symlink escape outside transformsDir",
() => {
// Setup directories
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-config-"));
const transformsRoot = path.join(configDir, "hooks", "transforms");
// Create malicious module outside allowed root
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-outside-"));
const outsideModule = path.join(outsideDir, "evil.mjs");
fs.writeFileSync(outsideModule, 'export default () => ({ kind: "wake" });');
// Create Symlink inside allowed root -> outside module
fs.symlinkSync(outsideModule, path.join(transformsRoot, "linked.mjs"));
// Attempt to load
expect(() =>
resolveHookMappings(
{ /* ... config using "linked.mjs" ... */ },
{ configDir },
),
).toThrow(/must be within/);
},
);The primary impact of this vulnerability is Remote Code Execution (RCE). By successfully loading a module outside the intended directory, an attacker can execute arbitrary code within the context of the OpenClaw application process. This allows for full compromise of the application, access to secrets (database credentials, API keys), and potentially lateral movement within the hosting infrastructure.
While the vulnerability requires the prerequisite of file system manipulation (creating the symlink), this is a common capability in shared development environments or systems allowing user-uploaded content.
Risk Breakdown:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
OpenClaw OpenClaw | < 2026-02-22 | 2026-02-22 Patch |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-59 (Improper Link Resolution) |
| Attack Vector | Local / Network (via Configuration) |
| CVSS (Est.) | 8.1 (High) |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | PoC Available |
| Platform | Node.js / TypeScript |
An uncontrolled resource consumption vulnerability exists in the Scala-based http4s-blaze-server package of the http4s/blaze library. The vulnerability allows remote, unauthenticated attackers to cause an Out of Memory Error (OOM) and JVM crash by streaming a continuous sequence of small or empty WebSocket continuation frames with the FIN bit set to 0. This bypasses typical payload size checks because of the JVM's per-object allocation overhead, leading to rapid heap exhaustion with minimal network bandwidth.
A critical path traversal vulnerability has been identified in the OpenList Go-based backend package. The vulnerability exists within the batch rename handler because the application does not validate the source filename parameter before constructing filesystems paths. This omission allows authenticated users to escape their designated directory and rename files in sibling paths.
OpenList version 4.2.3 and prior is vulnerable to an authorization bypass and metadata leakage. When configured with the Bleve search engine backend, OpenList fails to perform separator-aware path matching when validating tenant containment. This allows authenticated users to access sibling directories sharing similar name prefixes. Furthermore, the search backend returns unfiltered global result counts, leaking existence verification data of unauthorized files via side-channel analysis.
An authorization bypass vulnerability in OpenList version 4.2.3 and below allows authenticated users to read arbitrary files outside of their designated base directories due to an insecure path prefix check using Go's standard strings.HasPrefix function.
A security policy bypass vulnerability exists in the AWS API MCP Server (awslabs-aws-api-mcp-server) from version 0.2.13 through 1.3.46. When the server fails to load the read-only operations index during startup (due to transient network failures, file permission issues, or other exceptions), it logs a warning but continues running in an insecure, degraded state. Under this condition, the security policy engine fails open, silently skipping all subsequent security checks and consent prompts for the lifetime of the process. This permits unauthorized mutating AWS CLI commands to execute via indirect prompt injection attacks.
An incomplete escaping vulnerability in the npm package 'shescape' allows unauthenticated users to trigger dynamic shell expansions, absolute path disclosure, and command block break-outs on Unix and Windows systems.