VYPR
High severityNVD Advisory· Published Mar 19, 2026· Updated Mar 23, 2026

OpenClaw < 2026.2.19 - Command Injection via Unescaped Environment Variables in Windows Scheduled Task Script Generation

CVE-2026-22176

Description

OpenClaw versions prior to 2026.2.19 contain a command injection vulnerability in Windows Scheduled Task script generation where environment variables are written to gateway.cmd using unquoted set KEY=VALUE assignments, allowing shell metacharacters to break out of assignment context. Attackers can inject arbitrary commands through environment variable values containing metacharacters like &, |, ^, %, or ! to achieve command execution when the scheduled task script is generated and executed.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.192026.2.19

Affected products

1

Patches

1
dafe52e8cf1a

fix(daemon): escape schtasks environment assignments

https://github.com/openclaw/openclawPeter SteinbergerFeb 19, 2026via ghsa
2 files changed · +161 17
  • src/daemon/schtasks.test.ts+96 2 modified
    @@ -1,8 +1,27 @@
     import fs from "node:fs/promises";
     import os from "node:os";
     import path from "node:path";
    -import { describe, expect, it } from "vitest";
    -import { parseSchtasksQuery, readScheduledTaskCommand, resolveTaskScriptPath } from "./schtasks.js";
    +import { PassThrough } from "node:stream";
    +import { beforeEach, describe, expect, it, vi } from "vitest";
    +import {
    +  installScheduledTask,
    +  parseSchtasksQuery,
    +  readScheduledTaskCommand,
    +  resolveTaskScriptPath,
    +} from "./schtasks.js";
    +
    +const schtasksCalls: string[][] = [];
    +
    +vi.mock("./schtasks-exec.js", () => ({
    +  execSchtasks: async (argv: string[]) => {
    +    schtasksCalls.push(argv);
    +    return { code: 0, stdout: "", stderr: "" };
    +  },
    +}));
    +
    +beforeEach(() => {
    +  schtasksCalls.length = 0;
    +});
     
     describe("schtasks runtime parsing", () => {
       it.each(["Ready", "Running"])("parses %s status", (status) => {
    @@ -198,4 +217,79 @@ describe("readScheduledTaskCommand", () => {
           },
         );
       });
    +
    +  it("parses quoted set assignments with escaped metacharacters", async () => {
    +    await withScheduledTaskScript(
    +      {
    +        scriptLines: [
    +          "@echo off",
    +          'set "OC_AMP=left & right"',
    +          'set "OC_PIPE=a | b"',
    +          'set "OC_CARET=^^"',
    +          'set "OC_PERCENT=%%TEMP%%"',
    +          'set "OC_BANG=^!token^!"',
    +          'set "OC_QUOTE=he said ^"hi^""',
    +          "node gateway.js --verbose",
    +        ],
    +      },
    +      async (env) => {
    +        const result = await readScheduledTaskCommand(env);
    +        expect(result?.environment).toEqual({
    +          OC_AMP: "left & right",
    +          OC_PIPE: "a | b",
    +          OC_CARET: "^",
    +          OC_PERCENT: "%TEMP%",
    +          OC_BANG: "!token!",
    +          OC_QUOTE: 'he said "hi"',
    +        });
    +      },
    +    );
    +  });
    +});
    +
    +describe("installScheduledTask", () => {
    +  it("writes quoted set assignments and escapes metacharacters", async () => {
    +    const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-install-"));
    +    try {
    +      const env = {
    +        USERPROFILE: tmpDir,
    +        OPENCLAW_PROFILE: "default",
    +      };
    +      const { scriptPath } = await installScheduledTask({
    +        env,
    +        stdout: new PassThrough(),
    +        programArguments: ["node", "gateway.js", "--verbose"],
    +        environment: {
    +          OC_INJECT: "safe & whoami | calc",
    +          OC_CARET: "a^b",
    +          OC_PERCENT: "%TEMP%",
    +          OC_BANG: "!token!",
    +          OC_QUOTE: 'he said "hi"',
    +        },
    +      });
    +
    +      const script = await fs.readFile(scriptPath, "utf8");
    +      expect(script).toContain('set "OC_INJECT=safe & whoami | calc"');
    +      expect(script).toContain('set "OC_CARET=a^^b"');
    +      expect(script).toContain('set "OC_PERCENT=%%TEMP%%"');
    +      expect(script).toContain('set "OC_BANG=^!token^!"');
    +      expect(script).toContain('set "OC_QUOTE=he said ^"hi^""');
    +      expect(script).not.toContain("set OC_INJECT=");
    +
    +      const parsed = await readScheduledTaskCommand(env);
    +      expect(parsed?.environment).toMatchObject({
    +        OC_INJECT: "safe & whoami | calc",
    +        OC_CARET: "a^b",
    +        OC_PERCENT: "%TEMP%",
    +        OC_BANG: "!token!",
    +        OC_QUOTE: 'he said "hi"',
    +      });
    +
    +      expect(schtasksCalls[0]).toEqual(["/Query"]);
    +      expect(schtasksCalls[1]?.[0]).toBe("/Create");
    +      expect(schtasksCalls[2]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
    +    } finally {
    +      await fs.rm(tmpDir, { recursive: true, force: true });
    +    }
    +  });
     });
    
  • src/daemon/schtasks.ts+65 15 modified
    @@ -1,11 +1,5 @@
     import fs from "node:fs/promises";
     import path from "node:path";
    -import { splitArgsPreservingQuotes } from "./arg-split.js";
    -import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
    -import { formatLine, writeFormattedLines } from "./output.js";
    -import { resolveGatewayStateDir } from "./paths.js";
    -import { parseKeyValueOutput } from "./runtime-parse.js";
    -import { execSchtasks } from "./schtasks-exec.js";
     import type { GatewayServiceRuntime } from "./service-runtime.js";
     import type {
       GatewayServiceCommandConfig,
    @@ -16,6 +10,12 @@ import type {
       GatewayServiceManageArgs,
       GatewayServiceRenderArgs,
     } from "./service-types.js";
    +import { splitArgsPreservingQuotes } from "./arg-split.js";
    +import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
    +import { formatLine, writeFormattedLines } from "./output.js";
    +import { resolveGatewayStateDir } from "./paths.js";
    +import { parseKeyValueOutput } from "./runtime-parse.js";
    +import { execSchtasks } from "./schtasks-exec.js";
     
     function resolveTaskName(env: GatewayServiceEnv): string {
       const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim();
    @@ -42,6 +42,61 @@ function quoteCmdArg(value: string): string {
       return `"${value.replace(/"/g, '\\"')}"`;
     }
     
    +function escapeCmdSetAssignmentComponent(value: string): string {
    +  return value.replace(/\^/g, "^^").replace(/%/g, "%%").replace(/!/g, "^!").replace(/"/g, '^"');
    +}
    +
    +function unescapeCmdSetAssignmentComponent(value: string): string {
    +  let out = "";
    +  for (let i = 0; i < value.length; i += 1) {
    +    const ch = value[i];
    +    const next = value[i + 1];
    +    if (ch === "^" && (next === "^" || next === '"' || next === "!")) {
    +      out += next;
    +      i += 1;
    +      continue;
    +    }
    +    if (ch === "%" && next === "%") {
    +      out += "%";
    +      i += 1;
    +      continue;
    +    }
    +    out += ch;
    +  }
    +  return out;
    +}
    +
    +function parseCmdSetAssignment(line: string): { key: string; value: string } | null {
    +  const raw = line.trim();
    +  if (!raw) {
    +    return null;
    +  }
    +  const quoted = raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2;
    +  const assignment = quoted ? raw.slice(1, -1) : raw;
    +  const index = assignment.indexOf("=");
    +  if (index <= 0) {
    +    return null;
    +  }
    +  const key = assignment.slice(0, index).trim();
    +  const value = assignment.slice(index + 1).trim();
    +  if (!key) {
    +    return null;
    +  }
    +  if (!quoted) {
    +    return { key, value };
    +  }
    +  return {
    +    key: unescapeCmdSetAssignmentComponent(key),
    +    value: unescapeCmdSetAssignmentComponent(value),
    +  };
    +}
    +
    +function renderCmdSetAssignment(key: string, value: string): string {
    +  const escapedKey = escapeCmdSetAssignmentComponent(key);
    +  const escapedValue = escapeCmdSetAssignmentComponent(value);
    +  return `set "${escapedKey}=${escapedValue}"`;
    +}
    +
     function resolveTaskUser(env: GatewayServiceEnv): string | null {
       const username = env.USERNAME || env.USER || env.LOGNAME;
       if (!username) {
    @@ -84,14 +139,9 @@ export async function readScheduledTaskCommand(
             continue;
           }
           if (line.toLowerCase().startsWith("set ")) {
    -        const assignment = line.slice(4).trim();
    -        const index = assignment.indexOf("=");
    -        if (index > 0) {
    -          const key = assignment.slice(0, index).trim();
    -          const value = assignment.slice(index + 1).trim();
    -          if (key) {
    -            environment[key] = value;
    -          }
    +        const assignment = parseCmdSetAssignment(line.slice(4));
    +        if (assignment) {
    +          environment[assignment.key] = assignment.value;
             }
             continue;
           }
    @@ -157,7 +207,7 @@ function buildTaskScript({
           if (!value) {
             continue;
           }
    -      lines.push(`set ${key}=${value}`);
    +      lines.push(renderCmdSetAssignment(key, value));
         }
       }
       const command = programArguments.map(quoteCmdArg).join(" ");
    

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.