OpenClaw has a command injection in maintainer clawtributors updater
Description
OpenClaw is a personal AI assistant. Versions 2026.1.8 through 2026.2.13 have a command injection in the maintainer/dev script scripts/update-clawtributors.ts. The issue affects contributors/maintainers (or CI) who run bun scripts/update-clawtributors.ts in a source checkout that contains a malicious commit author email (e.g. crafted @users[.]noreply[.]github[.]com values). Normal CLI usage is not affected (npm i -g openclaw): this script is not part of the shipped CLI and is not executed during routine operation. The script derived a GitHub login from git log author metadata and interpolated it into a shell command (via execSync). A malicious commit record could inject shell metacharacters and execute arbitrary commands when the script is run. Version 2026.2.14 contains a patch.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | >= 2026.1.8, < 2026.2.14 | 2026.2.14 |
Affected products
1Patches
1a429380e3371fix(scripts): harden clawtributors updater
2 files changed · +36 −10
CHANGELOG.md+1 −0 modified@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai - Security/Exec approvals: prevent safeBins allowlist bypass via shell expansion (host exec allowlist mode only; not enabled by default). Thanks @christos-eth. - Security/Gateway: block `system.execApprovals.*` via `node.invoke` (use `exec.approvals.node.*` instead). Thanks @christos-eth. - Security/Exec: harden PATH handling by disabling project-local `node_modules/.bin` bootstrapping by default, disallowing node-host `PATH` overrides, and spawning ACP servers via the current executable by default. Thanks @akhmittra. +- Scripts: harden clawtributors updater against command injection via untrusted commit metadata. Thanks @scanleale. - CLI: fix lazy core command registration so top-level maintenance commands (`doctor`, `dashboard`, `reset`, `uninstall`) resolve correctly instead of exposing a non-functional `maintenance` placeholder command. - Telegram: when `channels.telegram.commands.native` is `false`, exclude plugin commands from `setMyCommands` menu registration while keeping plugin slash handlers callable. (#15132) Thanks @Glucksberg. - Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent.
scripts/update-clawtributors.ts+35 −10 modified@@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { execFileSync, execSync } from "node:child_process"; import { readFileSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import type { ApiContributor, Entry, MapConfig, User } from "./update-clawtributors.types.js"; @@ -290,6 +290,27 @@ function parseCount(value: string): number { return /^\d+$/.test(value) ? Number(value) : 0; } +function isValidLogin(login: string): boolean { + if (!/^[A-Za-z0-9-]{1,39}$/.test(login)) { + return false; + } + if (login.startsWith("-") || login.endsWith("-")) { + return false; + } + if (login.includes("--")) { + return false; + } + return true; +} + +function normalizeLogin(login: string | null): string | null { + if (!login) { + return null; + } + const trimmed = login.trim(); + return isValidLogin(trimmed) ? trimmed : null; +} + function normalizeAvatar(url: string): string { if (!/^https?:/i.test(url)) { return url; @@ -307,8 +328,12 @@ function isGhostAvatar(url: string): boolean { } function fetchUser(login: string): User | null { + const normalized = normalizeLogin(login); + if (!normalized) { + return null; + } try { - const data = execSync(`gh api users/${login}`, { + const data = execFileSync("gh", ["api", `users/${normalized}`], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); @@ -334,45 +359,45 @@ function resolveLogin( emailToLogin: Record<string, string>, ): string | null { if (email && emailToLogin[email]) { - return emailToLogin[email]; + return normalizeLogin(emailToLogin[email]); } if (email && name) { const guessed = guessLoginFromEmailName(name, email, apiByLogin); if (guessed) { - return guessed; + return normalizeLogin(guessed); } } if (email && email.endsWith("@users.noreply.github.com")) { const local = email.split("@", 1)[0]; const login = local.includes("+") ? local.split("+")[1] : local; - return login || null; + return normalizeLogin(login); } if (email && email.endsWith("@github.com")) { const login = email.split("@", 1)[0]; if (apiByLogin.has(login.toLowerCase())) { - return login; + return normalizeLogin(login); } } const normalized = normalizeName(name); if (nameToLogin[normalized]) { - return nameToLogin[normalized]; + return normalizeLogin(nameToLogin[normalized]); } const compact = normalized.replace(/\s+/g, ""); if (nameToLogin[compact]) { - return nameToLogin[compact]; + return normalizeLogin(nameToLogin[compact]); } if (apiByLogin.has(normalized)) { - return normalized; + return normalizeLogin(normalized); } if (apiByLogin.has(compact)) { - return compact; + return normalizeLogin(compact); } return null;
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- github.com/advisories/GHSA-m7x8-2w3w-pr42ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-26323ghsaADVISORY
- github.com/openclaw/openclaw/commit/a429380e337152746031d290432a4b93aa553d55ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/releases/tag/v2026.2.14ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/security/advisories/GHSA-m7x8-2w3w-pr42ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.