VYPR
Medium severity6.5NVD Advisory· Published Jun 12, 2026

CVE-2026-53825

CVE-2026-53825

Description

OpenClaw before 2026.4.7 allows authenticated Gateway operators with operator.write scope to read arbitrary local files via the memory-wiki ingest feature, bypassing access restrictions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

OpenClaw before 2026.4.7 allows authenticated Gateway operators with operator.write scope to read arbitrary local files via the memory-wiki ingest feature, bypassing access restrictions.

Vulnerability

In OpenClaw versions before 2026.4.7, the memory-wiki ingest feature does not properly restrict file paths. An authenticated Gateway operator with operator.write scope can specify arbitrary local file paths to import content into wiki memory, leading to arbitrary local file read. The vulnerability is a path traversal issue (CWE-22) as described in [1] and [2].

Exploitation

An attacker must have authenticated access to the Gateway and possess the operator.write scope. With that access, the attacker can send a request to the memory-wiki ingest endpoint with a crafted file path pointing to a local file outside the intended ingest sources. No user interaction is required beyond the initial authentication. The feature must be enabled and reachable.

Impact

Successful exploitation allows the attacker to read the contents of arbitrary local files on the server, potentially exposing sensitive information such as configuration files, secrets, or application data. The confidentiality impact is high, while integrity and availability are not affected. The privilege level required is that of an operator with write scope.

Mitigation

Update to OpenClaw version 2026.4.7 or later, which contains the fix. As a workaround, limit memory-wiki write access to trusted operators until patched, keep channel and tool allowlists narrow, avoid sharing a Gateway between mutually untrusted users, and disable the feature when not needed [1]. No KEV listing is indicated.

AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
505001754301

fix(doctor): warn when stale Codex overrides shadow OAuth (#40143)

https://github.com/OpenClaw/OpenClawBApr 8, 2026Fixed in 2026.4.7via release-tag
7 files changed · +347 2
  • CHANGELOG.md+3 0 modified
    @@ -1774,6 +1774,9 @@ Docs: https://docs.openclaw.ai
     - macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
     - macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
     - macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
    +- Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy `OPENROUTER_API_KEY`, `sk-or-...`, and explicit `perplexity.baseUrl` / `model` setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.
    +- Tools/web search: restore Perplexity OpenRouter/Sonar compatibility for legacy `OPENROUTER_API_KEY`, `sk-or-...`, and explicit `perplexity.baseUrl` / `model` setups while keeping direct Perplexity keys on the native Search API path. (#39937) Thanks @obviyus.
    +- Doctor/Codex OAuth: warn only for legacy `models.providers.openai-codex` transport overrides that can shadow the built-in Codex OAuth path, while leaving supported custom proxies and header-only overrides alone. (#40143) Thanks @bde1.
     - Hooks/session-memory: keep `/new` and `/reset` memory artifacts in the bound agent workspace and align saved reset session keys with that workspace when stale main-agent keys leak into the hook path. (#39875) thanks @rbutera.
     - Sessions/model switch: clear stale cached `contextTokens` when a session changes models so status and runtime paths recompute against the active model window. (#38044) thanks @yuweuii.
     - ACP/session history: persist transcripts for successful ACP child runs, preserve exact transcript text, record ACP spawned-session lineage, and keep spawn-time transcript-path persistence best-effort so history storage failures do not block execution. (#40137) thanks @mbelinky.
    
  • docs/gateway/doctor.md+11 0 modified
    @@ -66,6 +66,7 @@ cat ~/.openclaw/openclaw.json
     - Talk config migration from legacy flat `talk.*` fields into `talk.provider` + `talk.providers.<provider>`.
     - Browser migration checks for legacy Chrome extension configs and Chrome MCP readiness.
     - OpenCode provider override warnings (`models.providers.opencode` / `models.providers.opencode-go`).
    +- Codex OAuth shadowing warnings (`models.providers.openai-codex`).
     - OAuth TLS prerequisites check for OpenAI Codex OAuth profiles.
     - Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
     - Legacy plugin manifest contract key migration (`speechProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders`, `videoGenerationProviders`, `webFetchProviders`, `webSearchProviders` → `contracts`).
    @@ -212,6 +213,16 @@ doctor prints platform-specific fix guidance. On macOS with a Homebrew Node, the
     fix is usually `brew postinstall ca-certificates`. With `--deep`, the probe runs
     even if the gateway is healthy.
     
    +### 2c) Codex OAuth provider overrides
    +
    +If you previously added legacy OpenAI transport settings under
    +`models.providers.openai-codex`, they can shadow the built-in Codex OAuth
    +provider path that newer releases use automatically. Doctor warns when it sees
    +those old transport settings alongside Codex OAuth so you can remove or rewrite
    +the stale transport override and get the built-in routing/fallback behavior
    +back. Custom proxies and header-only overrides are still supported and do not
    +trigger this warning.
    +
     ### 3) Legacy state migrations (disk layout)
     
     Doctor can migrate older on-disk layouts into the current structure:
    
  • src/commands/doctor-auth.ts+89 0 modified
    @@ -15,9 +15,15 @@ import type { OpenClawConfig } from "../config/config.js";
     import { formatErrorMessage } from "../infra/errors.js";
     import { resolvePluginProviders } from "../plugins/providers.runtime.js";
     import { note } from "../terminal/note.js";
    +import { isRecord } from "../utils.js";
     import type { DoctorPrompter } from "./doctor-prompter.js";
     import { buildProviderAuthRecoveryHint } from "./provider-auth-guidance.js";
     
    +const CODEX_PROVIDER_ID = "openai-codex";
    +const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth";
    +const OPENAI_BASE_URL = "https://api.openai.com/v1";
    +const LEGACY_CODEX_APIS = new Set(["openai-responses", "openai-completions"]);
    +
     export async function maybeRepairLegacyOAuthProfileIds(
       cfg: OpenClawConfig,
       prompter: DoctorPrompter,
    @@ -55,6 +61,89 @@ export async function maybeRepairLegacyOAuthProfileIds(
       return nextCfg;
     }
     
    +function hasConfiguredCodexOAuthProfile(cfg: OpenClawConfig): boolean {
    +  return Object.values(cfg.auth?.profiles ?? {}).some(
    +    (profile) => profile.provider === CODEX_PROVIDER_ID && profile.mode === "oauth",
    +  );
    +}
    +
    +function hasStoredCodexOAuthProfile(): boolean {
    +  const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false });
    +  return Object.values(store.profiles).some(
    +    (profile) => profile.provider === CODEX_PROVIDER_ID && profile.type === "oauth",
    +  );
    +}
    +
    +function normalizeCodexOverrideBaseUrl(baseUrl: unknown): string | undefined {
    +  if (typeof baseUrl !== "string") {
    +    return undefined;
    +  }
    +  return baseUrl.trim().replace(/\/+$/, "");
    +}
    +
    +function isLegacyCodexTransportShape(value: unknown, inheritedBaseUrl?: unknown): boolean {
    +  if (!isRecord(value)) {
    +    return false;
    +  }
    +  const api = typeof value.api === "string" ? value.api : undefined;
    +  if (!api || !LEGACY_CODEX_APIS.has(api)) {
    +    return false;
    +  }
    +  const baseUrl = normalizeCodexOverrideBaseUrl(value.baseUrl ?? inheritedBaseUrl);
    +  return !baseUrl || baseUrl === OPENAI_BASE_URL;
    +}
    +
    +function hasLegacyCodexTransportOverride(providerOverride: unknown): boolean {
    +  if (!isRecord(providerOverride)) {
    +    return false;
    +  }
    +  if (isLegacyCodexTransportShape(providerOverride)) {
    +    return true;
    +  }
    +  const models = providerOverride.models;
    +  if (!Array.isArray(models)) {
    +    return false;
    +  }
    +  return models.some((model) => isLegacyCodexTransportShape(model, providerOverride.baseUrl));
    +}
    +
    +function buildCodexProviderOverrideWarning(providerOverride: unknown): string {
    +  const lines = [
    +    `- models.providers.${CODEX_PROVIDER_ID} contains a legacy transport override while Codex OAuth is configured.`,
    +    "- Older OpenAI transport settings can shadow the built-in Codex OAuth provider path.",
    +  ];
    +  if (isRecord(providerOverride)) {
    +    const record = providerOverride;
    +    if (typeof record.api === "string") {
    +      lines.push(`- models.providers.${CODEX_PROVIDER_ID}.api=${record.api}`);
    +    }
    +    if (typeof record.baseUrl === "string") {
    +      lines.push(`- models.providers.${CODEX_PROVIDER_ID}.baseUrl=${record.baseUrl}`);
    +    }
    +  }
    +  lines.push(
    +    `- Remove or rewrite the legacy transport override to restore the built-in Codex OAuth provider path after recent fixes.`,
    +  );
    +  lines.push(
    +    "- Custom proxies and header-only overrides can stay; this warning only targets old OpenAI transport settings.",
    +  );
    +  return lines.join("\n");
    +}
    +
    +export function noteLegacyCodexProviderOverride(cfg: OpenClawConfig): void {
    +  const providerOverride = cfg.models?.providers?.[CODEX_PROVIDER_ID];
    +  if (!providerOverride) {
    +    return;
    +  }
    +  if (!hasLegacyCodexTransportOverride(providerOverride)) {
    +    return;
    +  }
    +  if (!hasConfiguredCodexOAuthProfile(cfg) && !hasStoredCodexOAuthProfile()) {
    +    return;
    +  }
    +  note(buildCodexProviderOverrideWarning(providerOverride), CODEX_OAUTH_WARNING_TITLE);
    +}
    +
     type AuthIssue = {
       profileId: string;
       provider: string;
    
  • src/commands/doctor.e2e-harness.ts+14 0 modified
    @@ -76,6 +76,11 @@ export const listPluginDoctorLegacyConfigRules = vi.fn(() => []) as unknown as M
     export const runDoctorHealthContributions = vi.fn(
       defaultRunDoctorHealthContributions,
     ) as unknown as MockFn;
    +export const maybeRepairMemoryRecallHealth = vi
    +  .fn()
    +  .mockResolvedValue(undefined) as unknown as MockFn;
    +export const noteMemorySearchHealth = vi.fn().mockResolvedValue(undefined) as unknown as MockFn;
    +export const noteMemoryRecallHealth = vi.fn().mockResolvedValue(undefined) as unknown as MockFn;
     export const migrateLegacyConfig = vi.fn((raw: unknown) => ({
       config: raw as Record<string, unknown>,
       changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
    @@ -339,6 +344,12 @@ vi.mock("../flows/doctor-health-contributions.js", () => ({
       runDoctorHealthContributions,
     }));
     
    +vi.mock("./doctor-memory-search.js", () => ({
    +  maybeRepairMemoryRecallHealth,
    +  noteMemorySearchHealth,
    +  noteMemoryRecallHealth,
    +}));
    +
     vi.mock("../plugins/doctor-contract-registry.js", () => ({
       listPluginDoctorLegacyConfigRules,
     }));
    @@ -494,6 +505,9 @@ beforeEach(() => {
       runGatewayUpdate.mockReset().mockResolvedValue(createGatewayUpdateResult());
       listPluginDoctorLegacyConfigRules.mockReset().mockReturnValue([]);
       runDoctorHealthContributions.mockReset().mockImplementation(defaultRunDoctorHealthContributions);
    +  maybeRepairMemoryRecallHealth.mockReset().mockResolvedValue(undefined);
    +  noteMemorySearchHealth.mockReset().mockResolvedValue(undefined);
    +  noteMemoryRecallHealth.mockReset().mockResolvedValue(undefined);
       legacyReadConfigFileSnapshot.mockReset().mockResolvedValue(createLegacyConfigSnapshot());
       createConfigIO.mockReset().mockImplementation(() => ({
         readConfigFileSnapshot: legacyReadConfigFileSnapshot,
    
  • src/commands/doctor.fast-path-mocks.ts+2 0 modified
    @@ -26,6 +26,8 @@ vi.mock("./doctor-gateway-health.js", () => ({
     }));
     
     vi.mock("./doctor-memory-search.js", () => ({
    +  maybeRepairMemoryRecallHealth: vi.fn().mockResolvedValue(undefined),
    +  noteMemoryRecallHealth: vi.fn().mockResolvedValue(undefined),
       noteMemorySearchHealth: vi.fn().mockResolvedValue(undefined),
     }));
     
    
  • src/commands/doctor.warns-state-directory-is-missing.e2e.test.ts+226 2 modified
    @@ -2,7 +2,11 @@ import fs from "node:fs";
     import os from "node:os";
     import path from "node:path";
     import { beforeEach, describe, expect, it } from "vitest";
    -import { createDoctorRuntime, mockDoctorConfigSnapshot } from "./doctor.e2e-harness.js";
    +import {
    +  createDoctorRuntime,
    +  ensureAuthProfileStore,
    +  mockDoctorConfigSnapshot,
    +} from "./doctor.e2e-harness.js";
     import { loadDoctorCommandForTest, terminalNoteMock } from "./doctor.note-test-helpers.js";
     import "./doctor.fast-path-mocks.js";
     
    @@ -11,7 +15,7 @@ let doctorCommand: typeof import("./doctor.js").doctorCommand;
     describe("doctor command", () => {
       beforeEach(async () => {
         doctorCommand = await loadDoctorCommandForTest({
    -      unmockModules: ["./doctor-state-integrity.js"],
    +      unmockModules: ["../flows/doctor-health-contributions.js", "./doctor-state-integrity.js"],
         });
       });
     
    @@ -65,6 +69,226 @@ describe("doctor command", () => {
         expect(warned).toBe(true);
       });
     
    +  it("warns when a legacy openai-codex provider override shadows configured Codex OAuth", async () => {
    +    mockDoctorConfigSnapshot({
    +      config: {
    +        models: {
    +          providers: {
    +            "openai-codex": {
    +              api: "openai-responses",
    +              baseUrl: "https://api.openai.com/v1",
    +            },
    +          },
    +        },
    +        auth: {
    +          profiles: {
    +            "openai-codex:user@example.com": {
    +              provider: "openai-codex",
    +              mode: "oauth",
    +              email: "user@example.com",
    +            },
    +          },
    +        },
    +      },
    +    });
    +    ensureAuthProfileStore.mockReturnValue({
    +      version: 1,
    +      profiles: {},
    +    });
    +
    +    await doctorCommand(createDoctorRuntime(), {
    +      nonInteractive: true,
    +      workspaceSuggestions: false,
    +    });
    +
    +    const warned = terminalNoteMock.mock.calls.some(
    +      ([message, title]) =>
    +        title === "Codex OAuth" && String(message).includes("models.providers.openai-codex"),
    +    );
    +    expect(warned).toBe(true);
    +  });
    +
    +  it("warns when a legacy openai-codex provider override shadows stored Codex OAuth", async () => {
    +    mockDoctorConfigSnapshot({
    +      config: {
    +        models: {
    +          providers: {
    +            "openai-codex": {
    +              api: "openai-responses",
    +              baseUrl: "https://api.openai.com/v1",
    +            },
    +          },
    +        },
    +      },
    +    });
    +    ensureAuthProfileStore.mockReturnValue({
    +      version: 1,
    +      profiles: {
    +        "openai-codex:user@example.com": {
    +          type: "oauth",
    +          provider: "openai-codex",
    +          access: "access-token",
    +          refresh: "refresh-token",
    +          expires: Date.now() + 60_000,
    +          email: "user@example.com",
    +        },
    +      },
    +    });
    +
    +    await doctorCommand(createDoctorRuntime(), {
    +      nonInteractive: true,
    +      workspaceSuggestions: false,
    +    });
    +
    +    const warned = terminalNoteMock.mock.calls.some(
    +      ([message, title]) =>
    +        title === "Codex OAuth" && String(message).includes("models.providers.openai-codex"),
    +    );
    +    expect(warned).toBe(true);
    +  });
    +
    +  it("warns when an inline openai-codex model keeps the legacy OpenAI transport", async () => {
    +    mockDoctorConfigSnapshot({
    +      config: {
    +        models: {
    +          providers: {
    +            "openai-codex": {
    +              models: [
    +                {
    +                  id: "gpt-5.4",
    +                  api: "openai-responses",
    +                },
    +              ],
    +            },
    +          },
    +        },
    +        auth: {
    +          profiles: {
    +            "openai-codex:user@example.com": {
    +              provider: "openai-codex",
    +              mode: "oauth",
    +              email: "user@example.com",
    +            },
    +          },
    +        },
    +      },
    +    });
    +    ensureAuthProfileStore.mockReturnValue({
    +      version: 1,
    +      profiles: {},
    +    });
    +
    +    await doctorCommand(createDoctorRuntime(), {
    +      nonInteractive: true,
    +      workspaceSuggestions: false,
    +    });
    +
    +    const warned = terminalNoteMock.mock.calls.some(
    +      ([message, title]) =>
    +        title === "Codex OAuth" && String(message).includes("legacy transport override"),
    +    );
    +    expect(warned).toBe(true);
    +  });
    +
    +  it("does not warn for a custom openai-codex proxy override", async () => {
    +    mockDoctorConfigSnapshot({
    +      config: {
    +        models: {
    +          providers: {
    +            "openai-codex": {
    +              api: "openai-responses",
    +              baseUrl: "https://custom.example.com",
    +            },
    +          },
    +        },
    +        auth: {
    +          profiles: {
    +            "openai-codex:user@example.com": {
    +              provider: "openai-codex",
    +              mode: "oauth",
    +              email: "user@example.com",
    +            },
    +          },
    +        },
    +      },
    +    });
    +    ensureAuthProfileStore.mockReturnValue({
    +      version: 1,
    +      profiles: {},
    +    });
    +
    +    await doctorCommand(createDoctorRuntime(), {
    +      nonInteractive: true,
    +      workspaceSuggestions: false,
    +    });
    +
    +    const warned = terminalNoteMock.mock.calls.some(([, title]) => title === "Codex OAuth");
    +    expect(warned).toBe(false);
    +  });
    +
    +  it("does not warn for header-only openai-codex overrides", async () => {
    +    mockDoctorConfigSnapshot({
    +      config: {
    +        models: {
    +          providers: {
    +            "openai-codex": {
    +              baseUrl: "https://custom.example.com",
    +              headers: { "X-Custom-Auth": "token-123" },
    +              models: [{ id: "gpt-5.4" }],
    +            },
    +          },
    +        },
    +        auth: {
    +          profiles: {
    +            "openai-codex:user@example.com": {
    +              provider: "openai-codex",
    +              mode: "oauth",
    +              email: "user@example.com",
    +            },
    +          },
    +        },
    +      },
    +    });
    +    ensureAuthProfileStore.mockReturnValue({
    +      version: 1,
    +      profiles: {},
    +    });
    +
    +    await doctorCommand(createDoctorRuntime(), {
    +      nonInteractive: true,
    +      workspaceSuggestions: false,
    +    });
    +
    +    const warned = terminalNoteMock.mock.calls.some(([, title]) => title === "Codex OAuth");
    +    expect(warned).toBe(false);
    +  });
    +  it("does not warn about an openai-codex provider override without Codex OAuth", async () => {
    +    mockDoctorConfigSnapshot({
    +      config: {
    +        models: {
    +          providers: {
    +            "openai-codex": {
    +              api: "openai-responses",
    +              baseUrl: "https://api.openai.com/v1",
    +            },
    +          },
    +        },
    +      },
    +    });
    +    ensureAuthProfileStore.mockReturnValue({
    +      version: 1,
    +      profiles: {},
    +    });
    +
    +    await doctorCommand(createDoctorRuntime(), {
    +      nonInteractive: true,
    +      workspaceSuggestions: false,
    +    });
    +
    +    const warned = terminalNoteMock.mock.calls.some(([, title]) => title === "Codex OAuth");
    +    expect(warned).toBe(false);
    +  });
    +
       it("skips gateway auth warning when OPENCLAW_GATEWAY_TOKEN is set", async () => {
         mockDoctorConfigSnapshot({
           config: {
    
  • src/flows/doctor-health-contributions.ts+2 0 modified
    @@ -12,6 +12,7 @@ import { formatCliCommand } from "../cli/command-format.js";
     import {
       maybeRepairLegacyOAuthProfileIds,
       noteAuthProfileHealth,
    +  noteLegacyCodexProviderOverride,
     } from "../commands/doctor-auth.js";
     import { noteBootstrapFileSize } from "../commands/doctor-bootstrap-size.js";
     import { noteChromeMcpBrowserReadiness } from "../commands/doctor-browser.js";
    @@ -147,6 +148,7 @@ async function runAuthProfileHealth(ctx: DoctorHealthFlowContext): Promise<void>
         prompter: ctx.prompter,
         allowKeychainPrompt: ctx.options.nonInteractive !== true && Boolean(process.stdin.isTTY),
       });
    +  noteLegacyCodexProviderOverride(ctx.cfg);
       ctx.gatewayDetails = buildGatewayConnectionDetails({ config: ctx.cfg });
       if (ctx.gatewayDetails.remoteFallbackNote) {
         note(ctx.gatewayDetails.remoteFallbackNote, "Gateway");
    

Vulnerability mechanics

Root cause

"Missing path validation in the memory-wiki ingest feature allows authenticated operators to read arbitrary local files."

Attack vector

An attacker must be an authenticated Gateway operator holding the `operator.write` OAuth scope. The attacker sends a crafted request to the memory-wiki ingest endpoint providing an arbitrary local file path (e.g., `/etc/passwd` or a configuration file) as the ingest source. The server reads the file's content and imports it into the wiki memory, thereby disclosing the contents of any local file the server process can read. The precondition is therefore a valid session with the `operator.write` scope and network access to the ingest API endpoint.

Affected code

The **memory-wiki ingest feature** in OpenClaw before 2026.4.7 fails to restrict file paths supplied through the ingest endpoint. The patch (commit `5050017543011b61df67744ebc6368d889c25a95`) does not touch the ingest code itself — it only adds a `doctor` warning about stale Codex OAuth provider overrides in `src/commands/doctor-auth.ts` and related test harnesses; the arbitrary file-read vulnerability remains unpatched in this diff. The advisory states the flaw is in the "memory-wiki ingest feature" and is triggered by an attacker with `operator.write` scope specifying local file paths outside intended ingest sources.

What the fix does

The current patch does **not** fix the file-read vulnerability. It only adds a `doctor` command warning to detect stale `models.providers.openai-codex` transport overrides that could shadow the built-in Codex OAuth provider path. The actual remediation for CVE-2026-53825 — adding path validation or allow‑listing to the memory-wiki ingest feature — is absent from this diff. The advisory notes that the vulnerability exists in OpenClaw releases before 2026.4.7, implying the fix was shipped in or after that version, but the commit shown here addresses an entirely different issue.

Preconditions

  • authThe attacker must have a valid Gateway session with the operator.write OAuth scope.
  • networkThe attacker must have network access to the memory-wiki ingest API endpoint.
  • inputThe attacker must supply an arbitrary local file path as the ingest source.

Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.