diff --git a/CHANGELOG.md b/CHANGELOG.md index 7828bc333e2..c2e800a714b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/scripts/update-clawtributors.ts b/scripts/update-clawtributors.ts index 87be6b66c73..77724d2b019 100644 --- a/scripts/update-clawtributors.ts +++ b/scripts/update-clawtributors.ts @@ -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 | 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;