VYPR
High severityNVD Advisory· Published Feb 19, 2026· Updated Feb 20, 2026

OpenClaw: Unsanitized CWD path injection into LLM prompts

CVE-2026-27001

Description

OpenClaw is a personal AI assistant. Prior to version 2026.2.15, OpenClaw embedded the current working directory (workspace path) into the agent system prompt without sanitization. If an attacker can cause OpenClaw to run inside a directory whose name contains control/format characters (for example newlines or Unicode bidi/zero-width markers), those characters could break the prompt structure and inject attacker-controlled instructions. Starting in version 2026.2.15, the workspace path is sanitized before it is embedded into any LLM prompt output, stripping Unicode control/format characters and explicit line/paragraph separators. Workspace path resolution also applies the same sanitization as defense-in-depth.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.152026.2.15

Affected products

1

Patches

1
6254e96acf16

fix(security): harden prompt path sanitization

https://github.com/openclaw/openclawPeter SteinbergerFeb 16, 2026via ghsa
4 files changed · +97 11
  • src/agents/sanitize-for-prompt.test.ts+53 0 added
    @@ -0,0 +1,53 @@
    +import { describe, expect, it } from "vitest";
    +import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
    +import { buildAgentSystemPrompt } from "./system-prompt.js";
    +
    +describe("sanitizeForPromptLiteral (OC-19 hardening)", () => {
    +  it("strips ASCII control chars (CR/LF/NUL/tab)", () => {
    +    expect(sanitizeForPromptLiteral("/tmp/a\nb\rc\x00d\te")).toBe("/tmp/abcde");
    +  });
    +
    +  it("strips Unicode line/paragraph separators", () => {
    +    expect(sanitizeForPromptLiteral(`/tmp/a\u2028b\u2029c`)).toBe("/tmp/abc");
    +  });
    +
    +  it("strips Unicode format chars (bidi override)", () => {
    +    // U+202E RIGHT-TO-LEFT OVERRIDE (Cf) can spoof rendered text.
    +    expect(sanitizeForPromptLiteral(`/tmp/a\u202Eb`)).toBe("/tmp/ab");
    +  });
    +
    +  it("preserves ordinary Unicode + spaces", () => {
    +    const value = "/tmp/my project/日本語-folder.v2";
    +    expect(sanitizeForPromptLiteral(value)).toBe(value);
    +  });
    +});
    +
    +describe("buildAgentSystemPrompt uses sanitized workspace/sandbox strings", () => {
    +  it("sanitizes workspaceDir (no newlines / separators)", () => {
    +    const prompt = buildAgentSystemPrompt({
    +      workspaceDir: "/tmp/project\nINJECT\u2028MORE",
    +    });
    +    expect(prompt).toContain("Your working directory is: /tmp/projectINJECTMORE");
    +    expect(prompt).not.toContain("Your working directory is: /tmp/project\n");
    +    expect(prompt).not.toContain("\u2028");
    +  });
    +
    +  it("sanitizes sandbox workspace/mount/url strings", () => {
    +    const prompt = buildAgentSystemPrompt({
    +      workspaceDir: "/tmp/test",
    +      sandboxInfo: {
    +        enabled: true,
    +        containerWorkspaceDir: "/work\u2029space",
    +        workspaceDir: "/host\nspace",
    +        workspaceAccess: "read-write",
    +        agentWorkspaceMount: "/mnt\u2028mount",
    +        browserNoVncUrl: "http://example.test/\nui",
    +      },
    +    });
    +    expect(prompt).toContain("Sandbox container workdir: /workspace");
    +    expect(prompt).toContain("Sandbox host workspace: /hostspace");
    +    expect(prompt).toContain("(mounted at /mntmount)");
    +    expect(prompt).toContain("Sandbox browser observer (noVNC): http://example.test/ui");
    +    expect(prompt).not.toContain("\nui");
    +  });
    +});
    
  • src/agents/sanitize-for-prompt.ts+18 0 added
    @@ -0,0 +1,18 @@
    +/**
    + * Sanitize untrusted strings before embedding them into an LLM prompt.
    + *
    + * Threat model (OC-19): attacker-controlled directory names (or other runtime strings)
    + * that contain newline/control characters can break prompt structure and inject
    + * arbitrary instructions.
    + *
    + * Strategy (Option 3 hardening):
    + * - Strip Unicode "control" (Cc) + "format" (Cf) characters (includes CR/LF/NUL, bidi marks, zero-width chars).
    + * - Strip explicit line/paragraph separators (Zl/Zp): U+2028/U+2029.
    + *
    + * Notes:
    + * - This is intentionally lossy; it trades edge-case path fidelity for prompt integrity.
    + * - If you need lossless representation, escape instead of stripping.
    + */
    +export function sanitizeForPromptLiteral(value: string): string {
    +  return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, "");
    +}
    
  • src/agents/system-prompt.ts+14 9 modified
    @@ -4,6 +4,7 @@ import type { ResolvedTimeFormat } from "./date-time.js";
     import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
     import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
     import { listDeliverableMessageChannels } from "../utils/message-channel.js";
    +import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
     
     /**
      * Controls which hardcoded sections are included in the system prompt.
    @@ -355,13 +356,17 @@ export function buildAgentSystemPrompt(params: {
       const promptMode = params.promptMode ?? "full";
       const isMinimal = promptMode === "minimal" || promptMode === "none";
       const sandboxContainerWorkspace = params.sandboxInfo?.containerWorkspaceDir?.trim();
    +  const sanitizedWorkspaceDir = sanitizeForPromptLiteral(params.workspaceDir);
    +  const sanitizedSandboxContainerWorkspace = sandboxContainerWorkspace
    +    ? sanitizeForPromptLiteral(sandboxContainerWorkspace)
    +    : "";
       const displayWorkspaceDir =
    -    params.sandboxInfo?.enabled && sandboxContainerWorkspace
    -      ? sandboxContainerWorkspace
    -      : params.workspaceDir;
    +    params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
    +      ? sanitizedSandboxContainerWorkspace
    +      : sanitizedWorkspaceDir;
       const workspaceGuidance =
    -    params.sandboxInfo?.enabled && sandboxContainerWorkspace
    -      ? `For read/write/edit/apply_patch, file paths resolve against host workspace: ${params.workspaceDir}. Prefer relative paths so both sandboxed exec and file tools work consistently.`
    +    params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
    +      ? `For read/write/edit/apply_patch, file paths resolve against host workspace: ${sanitizedWorkspaceDir}. Prefer relative paths so both sandboxed exec and file tools work consistently.`
           : "Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.";
       const safetySection = [
         "## Safety",
    @@ -480,21 +485,21 @@ export function buildAgentSystemPrompt(params: {
               "Some tools may be unavailable due to sandbox policy.",
               "Sub-agents stay sandboxed (no elevated/host access). Need outside-sandbox read/write? Don't spawn; ask first.",
               params.sandboxInfo.containerWorkspaceDir
    -            ? `Sandbox container workdir: ${params.sandboxInfo.containerWorkspaceDir}`
    +            ? `Sandbox container workdir: ${sanitizeForPromptLiteral(params.sandboxInfo.containerWorkspaceDir)}`
                 : "",
               params.sandboxInfo.workspaceDir
    -            ? `Sandbox host workspace: ${params.sandboxInfo.workspaceDir}`
    +            ? `Sandbox host workspace: ${sanitizeForPromptLiteral(params.sandboxInfo.workspaceDir)}`
                 : "",
               params.sandboxInfo.workspaceAccess
                 ? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${
                     params.sandboxInfo.agentWorkspaceMount
    -                  ? ` (mounted at ${params.sandboxInfo.agentWorkspaceMount})`
    +                  ? ` (mounted at ${sanitizeForPromptLiteral(params.sandboxInfo.agentWorkspaceMount)})`
                       : ""
                   }`
                 : "",
               params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "",
               params.sandboxInfo.browserNoVncUrl
    -            ? `Sandbox browser observer (noVNC): ${params.sandboxInfo.browserNoVncUrl}`
    +            ? `Sandbox browser observer (noVNC): ${sanitizeForPromptLiteral(params.sandboxInfo.browserNoVncUrl)}`
                 : "",
               params.sandboxInfo.hostBrowserAllowed === true
                 ? "Host browser control: allowed."
    
  • src/agents/workspace-run.ts+12 2 modified
    @@ -1,4 +1,5 @@
     import type { OpenClawConfig } from "../config/config.js";
    +import { logWarn } from "../logger.js";
     import { redactIdentifier } from "../logging/redact-identifier.js";
     import {
       classifySessionKeyShape,
    @@ -8,6 +9,7 @@ import {
     } from "../routing/session-key.js";
     import { resolveUserPath } from "../utils.js";
     import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "./agent-scope.js";
    +import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
     
     export type WorkspaceFallbackReason = "missing" | "blank" | "invalid_type";
     type AgentIdSource = "explicit" | "session_key" | "default";
    @@ -84,8 +86,12 @@ export function resolveRunWorkspaceDir(params: {
       if (typeof requested === "string") {
         const trimmed = requested.trim();
         if (trimmed) {
    +      const sanitized = sanitizeForPromptLiteral(trimmed);
    +      if (sanitized !== trimmed) {
    +        logWarn("Control/format characters stripped from workspaceDir (OC-19 hardening).");
    +      }
           return {
    -        workspaceDir: resolveUserPath(trimmed),
    +        workspaceDir: resolveUserPath(sanitized),
             usedFallback: false,
             agentId,
             agentIdSource,
    @@ -96,8 +102,12 @@ export function resolveRunWorkspaceDir(params: {
       const fallbackReason: WorkspaceFallbackReason =
         requested == null ? "missing" : typeof requested === "string" ? "blank" : "invalid_type";
       const fallbackWorkspace = resolveAgentWorkspaceDir(params.config ?? {}, agentId);
    +  const sanitizedFallback = sanitizeForPromptLiteral(fallbackWorkspace);
    +  if (sanitizedFallback !== fallbackWorkspace) {
    +    logWarn("Control/format characters stripped from fallback workspaceDir (OC-19 hardening).");
    +  }
       return {
    -    workspaceDir: resolveUserPath(fallbackWorkspace),
    +    workspaceDir: resolveUserPath(sanitizedFallback),
         usedFallback: true,
         fallbackReason,
         agentId,
    

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.