fix(scripts): harden clawtributors updater

This commit is contained in:
Peter Steinberger
2026-02-14 23:17:20 +01:00
parent 43f75e53b8
commit a429380e33
2 changed files with 36 additions and 10 deletions
+1
View File
@@ -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/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/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. - 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. - 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. - 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. - Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent.
+35 -10
View File
@@ -1,4 +1,4 @@
import { execSync } from "node:child_process"; import { execFileSync, execSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs"; import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path"; import { resolve } from "node:path";
import type { ApiContributor, Entry, MapConfig, User } from "./update-clawtributors.types.js"; 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; 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 { function normalizeAvatar(url: string): string {
if (!/^https?:/i.test(url)) { if (!/^https?:/i.test(url)) {
return url; return url;
@@ -307,8 +328,12 @@ function isGhostAvatar(url: string): boolean {
} }
function fetchUser(login: string): User | null { function fetchUser(login: string): User | null {
const normalized = normalizeLogin(login);
if (!normalized) {
return null;
}
try { try {
const data = execSync(`gh api users/${login}`, { const data = execFileSync("gh", ["api", `users/${normalized}`], {
encoding: "utf8", encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
}); });
@@ -334,45 +359,45 @@ function resolveLogin(
emailToLogin: Record<string, string>, emailToLogin: Record<string, string>,
): string | null { ): string | null {
if (email && emailToLogin[email]) { if (email && emailToLogin[email]) {
return emailToLogin[email]; return normalizeLogin(emailToLogin[email]);
} }
if (email && name) { if (email && name) {
const guessed = guessLoginFromEmailName(name, email, apiByLogin); const guessed = guessLoginFromEmailName(name, email, apiByLogin);
if (guessed) { if (guessed) {
return guessed; return normalizeLogin(guessed);
} }
} }
if (email && email.endsWith("@users.noreply.github.com")) { if (email && email.endsWith("@users.noreply.github.com")) {
const local = email.split("@", 1)[0]; const local = email.split("@", 1)[0];
const login = local.includes("+") ? local.split("+")[1] : local; const login = local.includes("+") ? local.split("+")[1] : local;
return login || null; return normalizeLogin(login);
} }
if (email && email.endsWith("@github.com")) { if (email && email.endsWith("@github.com")) {
const login = email.split("@", 1)[0]; const login = email.split("@", 1)[0];
if (apiByLogin.has(login.toLowerCase())) { if (apiByLogin.has(login.toLowerCase())) {
return login; return normalizeLogin(login);
} }
} }
const normalized = normalizeName(name); const normalized = normalizeName(name);
if (nameToLogin[normalized]) { if (nameToLogin[normalized]) {
return nameToLogin[normalized]; return normalizeLogin(nameToLogin[normalized]);
} }
const compact = normalized.replace(/\s+/g, ""); const compact = normalized.replace(/\s+/g, "");
if (nameToLogin[compact]) { if (nameToLogin[compact]) {
return nameToLogin[compact]; return normalizeLogin(nameToLogin[compact]);
} }
if (apiByLogin.has(normalized)) { if (apiByLogin.has(normalized)) {
return normalized; return normalizeLogin(normalized);
} }
if (apiByLogin.has(compact)) { if (apiByLogin.has(compact)) {
return compact; return normalizeLogin(compact);
} }
return null; return null;