VYPR
Medium severity5.4NVD Advisory· Published Apr 23, 2026· Updated Apr 29, 2026

CVE-2026-41344

CVE-2026-41344

Description

OpenClaw before 2026.3.28 contains a privilege escalation vulnerability in the chat.send endpoint that allows write-scoped gateway callers to persist admin-only verboseLevel session overrides. Attackers can exploit the /verbose parameter to bypass access controls and expose sensitive reasoning or tool output intended to be restricted to administrators.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.3.28

Patches

1
c6031235288a

fix(gateway): require admin for persisted verbose defaults (#55916)

https://github.com/openclaw/openclawJacob TomlinsonMar 27, 2026via ghsa
5 files changed · +162 9
  • src/auto-reply/reply/directive-handling.impl.ts+29 7 modified
    @@ -20,10 +20,13 @@ import type { HandleDirectiveOnlyParams } from "./directive-handling.params.js";
     import { maybeHandleQueueDirective } from "./directive-handling.queue-validation.js";
     import {
       canPersistInternalExecDirective,
    +  canPersistInternalVerboseDirective,
       formatDirectiveAck,
       formatElevatedRuntimeHint,
       formatElevatedUnavailableText,
       formatInternalExecPersistenceDeniedText,
    +  formatInternalVerboseCurrentReplyOnlyText,
    +  formatInternalVerbosePersistenceDeniedText,
       enqueueModeSwitchEvents,
       withOptions,
     } from "./directive-handling.shared.js";
    @@ -99,6 +102,10 @@ export async function handleDirectiveOnly(
         surface: params.surface,
         gatewayClientScopes: params.gatewayClientScopes,
       });
    +  const allowInternalVerbosePersistence = canPersistInternalVerboseDirective({
    +    surface: params.surface,
    +    gatewayClientScopes: params.gatewayClientScopes,
    +  });
     
       const modelInfo = await maybeHandleModelDirectiveInfo({
         directives,
    @@ -319,7 +326,9 @@ export async function handleDirectiveOnly(
       const shouldPersistSessionEntry =
         (directives.hasThinkDirective && Boolean(directives.thinkLevel)) ||
         (directives.hasFastDirective && directives.fastMode !== undefined) ||
    -    (directives.hasVerboseDirective && Boolean(directives.verboseLevel)) ||
    +    (directives.hasVerboseDirective &&
    +      Boolean(directives.verboseLevel) &&
    +      allowInternalVerbosePersistence) ||
         (directives.hasReasoningDirective && Boolean(directives.reasoningLevel)) ||
         (directives.hasElevatedDirective && Boolean(directives.elevatedLevel)) ||
         (directives.hasExecDirective && directives.hasExecOptions && allowInternalExecPersistence) ||
    @@ -342,7 +351,11 @@ export async function handleDirectiveOnly(
         if (shouldDowngradeXHigh) {
           sessionEntry.thinkingLevel = "high";
         }
    -    if (directives.hasVerboseDirective && directives.verboseLevel) {
    +    if (
    +      directives.hasVerboseDirective &&
    +      directives.verboseLevel &&
    +      allowInternalVerbosePersistence
    +    ) {
           applyVerboseOverride(sessionEntry, directives.verboseLevel);
         }
         if (directives.hasReasoningDirective && directives.reasoningLevel) {
    @@ -457,13 +470,22 @@ export async function handleDirectiveOnly(
       }
       if (directives.hasVerboseDirective && directives.verboseLevel) {
         parts.push(
    -      directives.verboseLevel === "off"
    -        ? formatDirectiveAck("Verbose logging disabled.")
    -        : directives.verboseLevel === "full"
    -          ? formatDirectiveAck("Verbose logging set to full.")
    -          : formatDirectiveAck("Verbose logging enabled."),
    +      !allowInternalVerbosePersistence
    +        ? formatDirectiveAck(formatInternalVerboseCurrentReplyOnlyText())
    +        : directives.verboseLevel === "off"
    +          ? formatDirectiveAck("Verbose logging disabled.")
    +          : directives.verboseLevel === "full"
    +            ? formatDirectiveAck("Verbose logging set to full.")
    +            : formatDirectiveAck("Verbose logging enabled."),
         );
       }
    +  if (
    +    directives.hasVerboseDirective &&
    +    directives.verboseLevel &&
    +    !allowInternalVerbosePersistence
    +  ) {
    +    parts.push(formatDirectiveAck(formatInternalVerbosePersistenceDeniedText()));
    +  }
       if (directives.hasReasoningDirective && directives.reasoningLevel) {
         parts.push(
           directives.reasoningLevel === "off"
    
  • src/auto-reply/reply/directive-handling.model.test.ts+71 0 modified
    @@ -583,6 +583,43 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {
         expect(sessionEntry.execNode).toBeUndefined();
       });
     
    +  it("blocks internal operator.write verbose persistence in directive-only handling", async () => {
    +    const directives = parseInlineDirectives("/verbose full");
    +    const sessionEntry = createSessionEntry();
    +    const sessionStore = { [sessionKey]: sessionEntry };
    +    const result = await handleDirectiveOnly(
    +      createHandleParams({
    +        directives,
    +        sessionEntry,
    +        sessionStore,
    +        surface: "webchat",
    +        gatewayClientScopes: ["operator.write"],
    +      }),
    +    );
    +
    +    expect(result?.text).toContain("Verbose logging set for the current reply only.");
    +    expect(result?.text).toContain("operator.admin");
    +    expect(sessionEntry.verboseLevel).toBeUndefined();
    +  });
    +
    +  it("allows internal operator.admin verbose persistence in directive-only handling", async () => {
    +    const directives = parseInlineDirectives("/verbose full");
    +    const sessionEntry = createSessionEntry();
    +    const sessionStore = { [sessionKey]: sessionEntry };
    +    const result = await handleDirectiveOnly(
    +      createHandleParams({
    +        directives,
    +        sessionEntry,
    +        sessionStore,
    +        surface: "webchat",
    +        gatewayClientScopes: ["operator.admin"],
    +      }),
    +    );
    +
    +    expect(result?.text).toContain("Verbose logging set to full.");
    +    expect(sessionEntry.verboseLevel).toBe("full");
    +  });
    +
       it("allows internal operator.admin exec persistence in directive-only handling", async () => {
         const directives = parseInlineDirectives(
           "/exec host=node security=allowlist ask=always node=worker-1",
    @@ -646,4 +683,38 @@ describe("persistInlineDirectives internal exec scope gate", () => {
         expect(sessionEntry.execAsk).toBeUndefined();
         expect(sessionEntry.execNode).toBeUndefined();
       });
    +
    +  it("skips verbose persistence for internal operator.write callers", async () => {
    +    const allowedModelKeys = new Set(["anthropic/claude-opus-4-5", "openai/gpt-4o"]);
    +    const directives = parseInlineDirectives("/verbose full");
    +    const sessionEntry = {
    +      sessionId: "s1",
    +      updatedAt: Date.now(),
    +    } as SessionEntry;
    +    const sessionStore = { "agent:main:main": sessionEntry };
    +
    +    await persistInlineDirectives({
    +      directives,
    +      cfg: baseConfig(),
    +      sessionEntry,
    +      sessionStore,
    +      sessionKey: "agent:main:main",
    +      storePath: "/tmp/sessions.json",
    +      elevatedEnabled: true,
    +      elevatedAllowed: true,
    +      defaultProvider: "anthropic",
    +      defaultModel: "claude-opus-4-5",
    +      aliasIndex: baseAliasIndex(),
    +      allowedModelKeys,
    +      provider: "anthropic",
    +      model: "claude-opus-4-5",
    +      initialModelLabel: "anthropic/claude-opus-4-5",
    +      formatModelSwitchEvent: (label) => `Switched to ${label}`,
    +      agentCfg: undefined,
    +      surface: "webchat",
    +      gatewayClientScopes: ["operator.write"],
    +    });
    +
    +    expect(sessionEntry.verboseLevel).toBeUndefined();
    +  });
     });
    
  • src/auto-reply/reply/directive-handling.persist.ts+10 1 modified
    @@ -16,6 +16,7 @@ import { resolveModelSelectionFromDirective } from "./directive-handling.model-s
     import type { InlineDirectives } from "./directive-handling.parse.js";
     import {
       canPersistInternalExecDirective,
    +  canPersistInternalVerboseDirective,
       enqueueModeSwitchEvents,
     } from "./directive-handling.shared.js";
     import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
    @@ -65,6 +66,10 @@ export async function persistInlineDirectives(params: {
         surface: params.surface,
         gatewayClientScopes: params.gatewayClientScopes,
       });
    +  const allowInternalVerbosePersistence = canPersistInternalVerboseDirective({
    +    surface: params.surface,
    +    gatewayClientScopes: params.gatewayClientScopes,
    +  });
       const activeAgentId = sessionKey
         ? resolveSessionAgentId({ sessionKey, config: cfg })
         : resolveDefaultAgentId(cfg);
    @@ -89,7 +94,11 @@ export async function persistInlineDirectives(params: {
           sessionEntry.thinkingLevel = directives.thinkLevel;
           updated = true;
         }
    -    if (directives.hasVerboseDirective && directives.verboseLevel) {
    +    if (
    +      directives.hasVerboseDirective &&
    +      directives.verboseLevel &&
    +      allowInternalVerbosePersistence
    +    ) {
           applyVerboseOverride(sessionEntry, directives.verboseLevel);
           updated = true;
         }
    
  • src/auto-reply/reply/directive-handling.shared.ts+10 1 modified
    @@ -17,7 +17,13 @@ export const formatElevatedRuntimeHint = () =>
     export const formatInternalExecPersistenceDeniedText = () =>
       "Exec defaults require operator.admin for internal gateway callers; skipped persistence.";
     
    -export function canPersistInternalExecDirective(params: {
    +export const formatInternalVerbosePersistenceDeniedText = () =>
    +  "Verbose defaults require operator.admin for internal gateway callers; skipped persistence.";
    +
    +export const formatInternalVerboseCurrentReplyOnlyText = () =>
    +  "Verbose logging set for the current reply only.";
    +
    +function canPersistInternalDirective(params: {
       surface?: string;
       gatewayClientScopes?: string[];
     }): boolean {
    @@ -28,6 +34,9 @@ export function canPersistInternalExecDirective(params: {
       return scopes.includes("operator.admin");
     }
     
    +export const canPersistInternalExecDirective = canPersistInternalDirective;
    +export const canPersistInternalVerboseDirective = canPersistInternalDirective;
    +
     export const formatElevatedEvent = (level: ElevatedLevel) => {
       if (level === "full") {
         return "Elevated FULL — exec runs on host with auto-approval.";
    
  • src/gateway/server.chat.gateway-server-chat.test.ts+42 0 modified
    @@ -14,6 +14,7 @@ import {
       rpcReq,
       testState,
       trackConnectChallengeNonce,
    +  withGatewayServer,
       writeSessionStore,
     } from "./test-helpers.js";
     import { agentCommand } from "./test-helpers.mocks.js";
    @@ -684,6 +685,47 @@ describe("gateway server chat", () => {
         ]);
       });
     
    +  test("chat.send does not persist verboseLevel for operator.write callers", async () => {
    +    await withGatewayServer(async ({ port }) => {
    +      await withMainSessionStore(async () => {
    +        let scopedWs: WebSocket | undefined;
    +
    +        try {
    +          scopedWs = new WebSocket(`ws://127.0.0.1:${port}`);
    +          trackConnectChallengeNonce(scopedWs);
    +          await new Promise<void>((resolve) => scopedWs?.once("open", resolve));
    +          await connectOk(scopedWs, {
    +            scopes: ["operator.write"],
    +          });
    +
    +          const sendRes = await rpcReq(scopedWs, "chat.send", {
    +            sessionKey: "main",
    +            message: "/verbose full",
    +            idempotencyKey: "idem-write-scope-verbose-no-persist",
    +          });
    +          expect(sendRes.ok).toBe(true);
    +
    +          const waitRes = await rpcReq(scopedWs, "agent.wait", {
    +            runId: "idem-write-scope-verbose-no-persist",
    +            timeoutMs: 1_000,
    +          });
    +          expect(waitRes.ok).toBe(true);
    +          expect(waitRes.payload?.status).toBe("ok");
    +
    +          const raw = await fs.readFile(testState.sessionStorePath!, "utf-8");
    +          const stored = JSON.parse(raw) as {
    +            "agent:main:main"?: {
    +              verboseLevel?: string;
    +            };
    +          };
    +          expect(stored["agent:main:main"]?.verboseLevel).toBeUndefined();
    +        } finally {
    +          scopedWs?.close();
    +        }
    +      });
    +    });
    +  });
    +
       test("agent.wait resolves chat.send runs that finish without lifecycle events", async () => {
         await withMainSessionStore(async () => {
           const runId = "idem-wait-chat-1";
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

5

News mentions

0

No linked articles in our index yet.