VYPR
Low severityNVD Advisory· Published Mar 21, 2026· Updated Mar 23, 2026

OpenClaw < 2026.2.22 - Authentication Token Reuse in Owner ID Prompt Hashing Fallback

CVE-2026-32897

Description

OpenClaw versions prior to 2026.2.22 reuse gateway.auth.token as a fallback hash secret for owner-ID prompt obfuscation when commands.ownerDisplay is set to hash and commands.ownerDisplaySecret is unset, creating dual-use of authentication secrets across security domains. Attackers with access to system prompts sent to third-party model providers can derive the gateway authentication token from the hash outputs, compromising gateway authentication security.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.222026.2.22

Affected products

1

Patches

1
c99e7696e689

fix: decouple owner display secret from gateway auth token

https://github.com/openclaw/openclawPeter SteinbergerFeb 22, 2026via ghsa
8 files changed · +237 16
  • CHANGELOG.md+1 0 modified
    @@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
     - Security/Exec: in non-default setups that manually add `sort` to `tools.exec.safeBins`, block `sort --compress-program` so allowlist-mode safe-bin checks cannot bypass approval. Thanks @tdjackey for reporting.
     - Security/Exec approvals: when users choose `allow-always` for shell-wrapper commands (for example `/bin/zsh -lc ...`), persist allowlist patterns for the inner executable(s) instead of the wrapper shell binary, preventing accidental broad shell allowlisting in moderate mode. (#23276) Thanks @xrom2863.
     - Security/macOS app beta: enforce path-only `system.run` allowlist matching (drop basename matches like `echo`), migrate legacy basename entries to last resolved paths when available, and harden shell-chain handling to fail closed on unsafe parse/control syntax (including quoted command substitution/backticks). This is an optional allowlist-mode feature; default installs remain deny-by-default. This ships in the next npm release. Thanks @tdjackey for reporting.
    +- Security/Agents: auto-generate and persist a dedicated `commands.ownerDisplaySecret` when `commands.ownerDisplay=hash`, remove gateway token fallback from owner-ID prompt hashing across CLI and embedded agent runners, and centralize owner-display secret resolution in one shared helper. This ships in the next npm release. Thanks @aether-ai-agent for reporting.
     - Security/SSRF: expand IPv4 fetch guard blocking to include RFC special-use/non-global ranges (including benchmarking, TEST-NET, multicast, and reserved/broadcast blocks), and centralize range checks into a single CIDR policy table to reduce classifier drift.
     - Security/Archive: block zip symlink escapes during archive extraction.
     - Security/Media sandbox: keep tmp media allowance for absolute tmp paths only and enforce symlink-escape checks before sandbox-validated reads, preventing tmp symlink exfiltration and relative `../` sandbox escapes when sandboxes live under tmp. (#17892) Thanks @dashed.
    
  • src/agents/cli-runner/helpers.ts+4 5 modified
    @@ -11,6 +11,7 @@ import { buildTtsSystemPromptHint } from "../../tts/tts.js";
     import { isRecord } from "../../utils.js";
     import { buildModelAliasLines } from "../model-alias-lines.js";
     import { resolveDefaultModelForAgent } from "../model-selection.js";
    +import { resolveOwnerDisplaySetting } from "../owner-display.js";
     import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
     import { detectRuntimeShell } from "../shell-utils.js";
     import { buildSystemPromptParams } from "../system-prompt-params.js";
    @@ -81,16 +82,14 @@ export function buildSystemPrompt(params: {
         },
       });
       const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
    +  const ownerDisplay = resolveOwnerDisplaySetting(params.config);
       return buildAgentSystemPrompt({
         workspaceDir: params.workspaceDir,
         defaultThinkLevel: params.defaultThinkLevel,
         extraSystemPrompt: params.extraSystemPrompt,
         ownerNumbers: params.ownerNumbers,
    -    ownerDisplay: params.config?.commands?.ownerDisplay,
    -    ownerDisplaySecret:
    -      params.config?.commands?.ownerDisplaySecret ??
    -      params.config?.gateway?.auth?.token ??
    -      params.config?.gateway?.remote?.token,
    +    ownerDisplay: ownerDisplay.ownerDisplay,
    +    ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
         reasoningTagHint: false,
         heartbeatPrompt: params.heartbeatPrompt,
         docsPath: params.docsPath,
    
  • src/agents/owner-display.test.ts+78 0 added
    @@ -0,0 +1,78 @@
    +import { describe, expect, it } from "vitest";
    +import type { OpenClawConfig } from "../config/config.js";
    +import { ensureOwnerDisplaySecret, resolveOwnerDisplaySetting } from "./owner-display.js";
    +
    +describe("resolveOwnerDisplaySetting", () => {
    +  it("returns keyed hash settings when hash mode has an explicit secret", () => {
    +    const cfg = {
    +      commands: {
    +        ownerDisplay: "hash",
    +        ownerDisplaySecret: "  owner-secret  ",
    +      },
    +    } as OpenClawConfig;
    +
    +    expect(resolveOwnerDisplaySetting(cfg)).toEqual({
    +      ownerDisplay: "hash",
    +      ownerDisplaySecret: "owner-secret",
    +    });
    +  });
    +
    +  it("does not fall back to gateway tokens when hash secret is missing", () => {
    +    const cfg = {
    +      commands: {
    +        ownerDisplay: "hash",
    +      },
    +      gateway: {
    +        auth: { token: "gateway-auth-token" },
    +        remote: { token: "gateway-remote-token" },
    +      },
    +    } as OpenClawConfig;
    +
    +    expect(resolveOwnerDisplaySetting(cfg)).toEqual({
    +      ownerDisplay: "hash",
    +      ownerDisplaySecret: undefined,
    +    });
    +  });
    +
    +  it("disables owner hash secret when display mode is raw", () => {
    +    const cfg = {
    +      commands: {
    +        ownerDisplay: "raw",
    +        ownerDisplaySecret: "owner-secret",
    +      },
    +    } as OpenClawConfig;
    +
    +    expect(resolveOwnerDisplaySetting(cfg)).toEqual({
    +      ownerDisplay: "raw",
    +      ownerDisplaySecret: undefined,
    +    });
    +  });
    +});
    +
    +describe("ensureOwnerDisplaySecret", () => {
    +  it("generates a dedicated secret when hash mode is enabled without one", () => {
    +    const cfg = {
    +      commands: {
    +        ownerDisplay: "hash",
    +      },
    +    } as OpenClawConfig;
    +
    +    const result = ensureOwnerDisplaySecret(cfg, () => "generated-owner-secret");
    +    expect(result.generatedSecret).toBe("generated-owner-secret");
    +    expect(result.config.commands?.ownerDisplaySecret).toBe("generated-owner-secret");
    +    expect(result.config.commands?.ownerDisplay).toBe("hash");
    +  });
    +
    +  it("does nothing when a hash secret is already configured", () => {
    +    const cfg = {
    +      commands: {
    +        ownerDisplay: "hash",
    +        ownerDisplaySecret: "existing-owner-secret",
    +      },
    +    } as OpenClawConfig;
    +
    +    const result = ensureOwnerDisplaySecret(cfg, () => "generated-owner-secret");
    +    expect(result.generatedSecret).toBeUndefined();
    +    expect(result.config).toEqual(cfg);
    +  });
    +});
    
  • src/agents/owner-display.ts+58 0 added
    @@ -0,0 +1,58 @@
    +import crypto from "node:crypto";
    +import type { OpenClawConfig } from "../config/config.js";
    +
    +export type OwnerDisplaySetting = {
    +  ownerDisplay?: "raw" | "hash";
    +  ownerDisplaySecret?: string;
    +};
    +
    +export type OwnerDisplaySecretResolution = {
    +  config: OpenClawConfig;
    +  generatedSecret?: string;
    +};
    +
    +function trimToUndefined(value?: string): string | undefined {
    +  const trimmed = value?.trim();
    +  return trimmed ? trimmed : undefined;
    +}
    +
    +/**
    + * Resolve owner display settings for prompt rendering.
    + * Keep auth secrets decoupled from owner hash secrets.
    + */
    +export function resolveOwnerDisplaySetting(config?: OpenClawConfig): OwnerDisplaySetting {
    +  const ownerDisplay = config?.commands?.ownerDisplay;
    +  if (ownerDisplay !== "hash") {
    +    return { ownerDisplay, ownerDisplaySecret: undefined };
    +  }
    +  return {
    +    ownerDisplay: "hash",
    +    ownerDisplaySecret: trimToUndefined(config?.commands?.ownerDisplaySecret),
    +  };
    +}
    +
    +/**
    + * Ensure hash mode has a dedicated secret.
    + * Returns updated config and generated secret when autofill was needed.
    + */
    +export function ensureOwnerDisplaySecret(
    +  config: OpenClawConfig,
    +  generateSecret: () => string = () => crypto.randomBytes(32).toString("hex"),
    +): OwnerDisplaySecretResolution {
    +  const settings = resolveOwnerDisplaySetting(config);
    +  if (settings.ownerDisplay !== "hash" || settings.ownerDisplaySecret) {
    +    return { config };
    +  }
    +  const generatedSecret = generateSecret();
    +  return {
    +    config: {
    +      ...config,
    +      commands: {
    +        ...config.commands,
    +        ownerDisplay: "hash",
    +        ownerDisplaySecret: generatedSecret,
    +      },
    +    },
    +    generatedSecret,
    +  };
    +}
    
  • src/agents/pi-embedded-runner/compact.ts+4 5 modified
    @@ -33,6 +33,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
     import { resolveOpenClawDocsPath } from "../docs-path.js";
     import { getApiKeyForModel, resolveModelAuthMode } from "../model-auth.js";
     import { ensureOpenClawModelsJson } from "../models-config.js";
    +import { resolveOwnerDisplaySetting } from "../owner-display.js";
     import {
       ensureSessionHeader,
       validateAnthropicTurns,
    @@ -480,17 +481,15 @@ export async function compactEmbeddedPiSessionDirect(
           moduleUrl: import.meta.url,
         });
         const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
    +    const ownerDisplay = resolveOwnerDisplaySetting(params.config);
         const appendPrompt = buildEmbeddedSystemPrompt({
           workspaceDir: effectiveWorkspace,
           defaultThinkLevel: params.thinkLevel,
           reasoningLevel: params.reasoningLevel ?? "off",
           extraSystemPrompt: params.extraSystemPrompt,
           ownerNumbers: params.ownerNumbers,
    -      ownerDisplay: params.config?.commands?.ownerDisplay,
    -      ownerDisplaySecret:
    -        params.config?.commands?.ownerDisplaySecret ??
    -        params.config?.gateway?.auth?.token ??
    -        params.config?.gateway?.remote?.token,
    +      ownerDisplay: ownerDisplay.ownerDisplay,
    +      ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
           reasoningTagHint,
           heartbeatPrompt: isDefaultAgent
             ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
    
  • src/agents/pi-embedded-runner/run/attempt.ts+4 5 modified
    @@ -47,6 +47,7 @@ import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
     import { resolveModelAuthMode } from "../../model-auth.js";
     import { resolveDefaultModelForAgent } from "../../model-selection.js";
     import { createOllamaStreamFn, OLLAMA_NATIVE_BASE_URL } from "../../ollama-stream.js";
    +import { resolveOwnerDisplaySetting } from "../../owner-display.js";
     import {
       isCloudCodeAssistFormatError,
       resolveBootstrapMaxChars,
    @@ -505,18 +506,16 @@ export async function runEmbeddedAttempt(
           moduleUrl: import.meta.url,
         });
         const ttsHint = params.config ? buildTtsSystemPromptHint(params.config) : undefined;
    +    const ownerDisplay = resolveOwnerDisplaySetting(params.config);
     
         const appendPrompt = buildEmbeddedSystemPrompt({
           workspaceDir: effectiveWorkspace,
           defaultThinkLevel: params.thinkLevel,
           reasoningLevel: params.reasoningLevel ?? "off",
           extraSystemPrompt: params.extraSystemPrompt,
           ownerNumbers: params.ownerNumbers,
    -      ownerDisplay: params.config?.commands?.ownerDisplay,
    -      ownerDisplaySecret:
    -        params.config?.commands?.ownerDisplaySecret ??
    -        params.config?.gateway?.auth?.token ??
    -        params.config?.gateway?.remote?.token,
    +      ownerDisplay: ownerDisplay.ownerDisplay,
    +      ownerDisplaySecret: ownerDisplay.ownerDisplaySecret,
           reasoningTagHint,
           heartbeatPrompt: isDefaultAgent
             ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
    
  • src/config/io.owner-display-secret.test.ts+48 0 added
    @@ -0,0 +1,48 @@
    +import fs from "node:fs/promises";
    +import path from "node:path";
    +import { describe, expect, it } from "vitest";
    +import { withTempHome } from "./home-env.test-harness.js";
    +import { createConfigIO } from "./io.js";
    +
    +async function waitForPersistedSecret(configPath: string, expectedSecret: string): Promise<void> {
    +  const deadline = Date.now() + 3_000;
    +  while (Date.now() < deadline) {
    +    const raw = await fs.readFile(configPath, "utf-8");
    +    const parsed = JSON.parse(raw) as {
    +      commands?: { ownerDisplaySecret?: string };
    +    };
    +    if (parsed.commands?.ownerDisplaySecret === expectedSecret) {
    +      return;
    +    }
    +    await new Promise((resolve) => setTimeout(resolve, 25));
    +  }
    +  throw new Error("timed out waiting for ownerDisplaySecret persistence");
    +}
    +
    +describe("config io owner display secret autofill", () => {
    +  it("auto-generates and persists commands.ownerDisplaySecret in hash mode", async () => {
    +    await withTempHome("openclaw-owner-display-secret-", async (home) => {
    +      const configPath = path.join(home, ".openclaw", "openclaw.json");
    +      await fs.mkdir(path.dirname(configPath), { recursive: true });
    +      await fs.writeFile(
    +        configPath,
    +        JSON.stringify({ commands: { ownerDisplay: "hash" } }, null, 2),
    +        "utf-8",
    +      );
    +
    +      const io = createConfigIO({
    +        env: {} as NodeJS.ProcessEnv,
    +        homedir: () => home,
    +        logger: { warn: () => {}, error: () => {} },
    +      });
    +      const cfg = io.loadConfig();
    +      const secret = cfg.commands?.ownerDisplaySecret;
    +
    +      expect(secret).toMatch(/^[a-f0-9]{64}$/);
    +      await waitForPersistedSecret(configPath, secret ?? "");
    +
    +      const cfgReloaded = io.loadConfig();
    +      expect(cfgReloaded.commands?.ownerDisplaySecret).toBe(secret);
    +    });
    +  });
    +});
    
  • src/config/io.ts+40 1 modified
    @@ -4,6 +4,7 @@ import os from "node:os";
     import path from "node:path";
     import { isDeepStrictEqual } from "node:util";
     import JSON5 from "json5";
    +import { ensureOwnerDisplaySecret } from "../agents/owner-display.js";
     import { loadDotEnv } from "../infra/dotenv.js";
     import { resolveRequiredHomeDir } from "../infra/home-dir.js";
     import {
    @@ -696,7 +697,42 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
             });
           }
     
    -      return applyConfigOverrides(cfg);
    +      const pendingSecret = AUTO_OWNER_DISPLAY_SECRET_BY_PATH.get(configPath);
    +      const ownerDisplaySecretResolution = ensureOwnerDisplaySecret(
    +        cfg,
    +        () => pendingSecret ?? crypto.randomBytes(32).toString("hex"),
    +      );
    +      const cfgWithOwnerDisplaySecret = ownerDisplaySecretResolution.config;
    +      if (ownerDisplaySecretResolution.generatedSecret) {
    +        AUTO_OWNER_DISPLAY_SECRET_BY_PATH.set(
    +          configPath,
    +          ownerDisplaySecretResolution.generatedSecret,
    +        );
    +        if (!AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT.has(configPath)) {
    +          AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT.add(configPath);
    +          void writeConfigFile(cfgWithOwnerDisplaySecret, { expectedConfigPath: configPath })
    +            .then(() => {
    +              AUTO_OWNER_DISPLAY_SECRET_BY_PATH.delete(configPath);
    +              AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED.delete(configPath);
    +            })
    +            .catch((err) => {
    +              if (!AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED.has(configPath)) {
    +                AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED.add(configPath);
    +                deps.logger.warn(
    +                  `Failed to persist auto-generated commands.ownerDisplaySecret at ${configPath}: ${String(err)}`,
    +                );
    +              }
    +            })
    +            .finally(() => {
    +              AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT.delete(configPath);
    +            });
    +        }
    +      } else {
    +        AUTO_OWNER_DISPLAY_SECRET_BY_PATH.delete(configPath);
    +        AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED.delete(configPath);
    +      }
    +
    +      return applyConfigOverrides(cfgWithOwnerDisplaySecret);
         } catch (err) {
           if (err instanceof DuplicateAgentDirError) {
             deps.logger.error(err.message);
    @@ -1149,6 +1185,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
     // module scope. `OPENCLAW_CONFIG_PATH` (and friends) are expected to work even
     // when set after the module has been imported (tests, one-off scripts, etc.).
     const DEFAULT_CONFIG_CACHE_MS = 200;
    +const AUTO_OWNER_DISPLAY_SECRET_BY_PATH = new Map<string, string>();
    +const AUTO_OWNER_DISPLAY_SECRET_PERSIST_IN_FLIGHT = new Set<string>();
    +const AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED = new Set<string>();
     let configCache: {
       configPath: string;
       expiresAt: number;
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.