diff --git a/src/browser/output-atomic.ts b/src/browser/output-atomic.ts index 8cd782188b6..6d6e6370927 100644 --- a/src/browser/output-atomic.ts +++ b/src/browser/output-atomic.ts @@ -1,35 +1,11 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; - -function sanitizeFileNameTail(fileName: string): string { - const trimmed = String(fileName ?? "").trim(); - if (!trimmed) { - return "output.bin"; - } - let base = path.posix.basename(trimmed); - base = path.win32.basename(base); - let cleaned = ""; - for (let i = 0; i < base.length; i++) { - const code = base.charCodeAt(i); - if (code < 0x20 || code === 0x7f) { - continue; - } - cleaned += base[i]; - } - base = cleaned.trim(); - if (!base || base === "." || base === "..") { - return "output.bin"; - } - if (base.length > 200) { - base = base.slice(0, 200); - } - return base; -} +import { sanitizeUntrustedFileName } from "./safe-filename.js"; function buildSiblingTempPath(targetPath: string): string { const id = crypto.randomUUID(); - const safeTail = sanitizeFileNameTail(path.basename(targetPath)); + const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), "output.bin"); return path.join(path.dirname(targetPath), `.openclaw-output-${id}-${safeTail}.part`); } diff --git a/src/browser/pw-tools-core.downloads.ts b/src/browser/pw-tools-core.downloads.ts index 8afb3afd8a0..0093c8c388f 100644 --- a/src/browser/pw-tools-core.downloads.ts +++ b/src/browser/pw-tools-core.downloads.ts @@ -19,39 +19,11 @@ import { requireRef, toAIFriendlyError, } from "./pw-tools-core.shared.js"; - -function sanitizeDownloadFileName(fileName: string): string { - const trimmed = String(fileName ?? "").trim(); - if (!trimmed) { - return "download.bin"; - } - - // `suggestedFilename()` is untrusted (influenced by remote servers). Force a basename so - // path separators/traversal can't escape the downloads dir on any platform. - let base = path.posix.basename(trimmed); - base = path.win32.basename(base); - let cleaned = ""; - for (let i = 0; i < base.length; i++) { - const code = base.charCodeAt(i); - if (code < 0x20 || code === 0x7f) { - continue; - } - cleaned += base[i]; - } - base = cleaned.trim(); - - if (!base || base === "." || base === "..") { - return "download.bin"; - } - if (base.length > 200) { - base = base.slice(0, 200); - } - return base; -} +import { sanitizeUntrustedFileName } from "./safe-filename.js"; function buildTempDownloadPath(fileName: string): string { const id = crypto.randomUUID(); - const safeName = sanitizeDownloadFileName(fileName); + const safeName = sanitizeUntrustedFileName(fileName, "download.bin"); return path.join(resolvePreferredOpenClawTmpDir(), "downloads", `${id}-${safeName}`); } diff --git a/src/browser/safe-filename.ts b/src/browser/safe-filename.ts new file mode 100644 index 00000000000..1508d528eaf --- /dev/null +++ b/src/browser/safe-filename.ts @@ -0,0 +1,26 @@ +import path from "node:path"; + +export function sanitizeUntrustedFileName(fileName: string, fallbackName: string): string { + const trimmed = String(fileName ?? "").trim(); + if (!trimmed) { + return fallbackName; + } + let base = path.posix.basename(trimmed); + base = path.win32.basename(base); + let cleaned = ""; + for (let i = 0; i < base.length; i++) { + const code = base.charCodeAt(i); + if (code < 0x20 || code === 0x7f) { + continue; + } + cleaned += base[i]; + } + base = cleaned.trim(); + if (!base || base === "." || base === "..") { + return fallbackName; + } + if (base.length > 200) { + base = base.slice(0, 200); + } + return base; +} diff --git a/src/gateway/server/http-auth.ts b/src/gateway/server/http-auth.ts index 9d143cacdb1..f6e241f4f0b 100644 --- a/src/gateway/server/http-auth.ts +++ b/src/gateway/server/http-auth.ts @@ -9,7 +9,7 @@ import { type ResolvedGatewayAuth, } from "../auth.js"; import { CANVAS_CAPABILITY_TTL_MS } from "../canvas-capability.js"; -import { sendGatewayAuthFailure } from "../http-common.js"; +import { authorizeGatewayBearerRequestOrReply } from "../http-auth-helpers.js"; import { getBearerToken } from "../http-utils.js"; import { GATEWAY_CLIENT_MODES, normalizeGatewayClientMode } from "../protocol/client-info.js"; import type { GatewayWsClient } from "./ws-types.js"; @@ -113,18 +113,5 @@ export async function enforcePluginRouteGatewayAuth(params: { allowRealIpFallback: boolean; rateLimiter?: AuthRateLimiter; }): Promise { - const token = getBearerToken(params.req); - const authResult = await authorizeHttpGatewayConnect({ - auth: params.auth, - connectAuth: token ? { token, password: token } : null, - req: params.req, - trustedProxies: params.trustedProxies, - allowRealIpFallback: params.allowRealIpFallback, - rateLimiter: params.rateLimiter, - }); - if (!authResult.ok) { - sendGatewayAuthFailure(params.res, authResult); - return false; - } - return true; + return await authorizeGatewayBearerRequestOrReply(params); } diff --git a/src/infra/system-run-approval-binding.ts b/src/infra/system-run-approval-binding.ts index 936ba9b0ec3..897ac9d9a31 100644 --- a/src/infra/system-run-approval-binding.ts +++ b/src/infra/system-run-approval-binding.ts @@ -1,21 +1,10 @@ import crypto from "node:crypto"; import type { SystemRunApprovalBinding, SystemRunApprovalPlan } from "./exec-approvals.js"; import { normalizeEnvVarKey } from "./host-env-security.js"; +import { normalizeNonEmptyString, normalizeStringArray } from "./system-run-normalize.js"; type NormalizedSystemRunEnvEntry = [key: string, value: string]; -function normalizeString(value: unknown): string | null { - if (typeof value !== "string") { - return null; - } - const trimmed = value.trim(); - return trimmed ? trimmed : null; -} - -function normalizeStringArray(value: unknown): string[] { - return Array.isArray(value) ? value.map((entry) => String(entry)) : []; -} - export function normalizeSystemRunApprovalPlan(value: unknown): SystemRunApprovalPlan | null { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; @@ -27,10 +16,10 @@ export function normalizeSystemRunApprovalPlan(value: unknown): SystemRunApprova } return { argv, - cwd: normalizeString(candidate.cwd), - rawCommand: normalizeString(candidate.rawCommand), - agentId: normalizeString(candidate.agentId), - sessionKey: normalizeString(candidate.sessionKey), + cwd: normalizeNonEmptyString(candidate.cwd), + rawCommand: normalizeNonEmptyString(candidate.rawCommand), + agentId: normalizeNonEmptyString(candidate.agentId), + sessionKey: normalizeNonEmptyString(candidate.sessionKey), }; } @@ -82,9 +71,9 @@ export function buildSystemRunApprovalBinding(params: { return { binding: { argv: normalizeStringArray(params.argv), - cwd: normalizeString(params.cwd), - agentId: normalizeString(params.agentId), - sessionKey: normalizeString(params.sessionKey), + cwd: normalizeNonEmptyString(params.cwd), + agentId: normalizeNonEmptyString(params.agentId), + sessionKey: normalizeNonEmptyString(params.sessionKey), envHash: envBinding.envHash, }, envKeys: envBinding.envKeys, diff --git a/src/infra/system-run-approval-context.ts b/src/infra/system-run-approval-context.ts index 9d01206b8b1..b94aef88a82 100644 --- a/src/infra/system-run-approval-context.ts +++ b/src/infra/system-run-approval-context.ts @@ -1,6 +1,7 @@ import type { SystemRunApprovalPlan } from "./exec-approvals.js"; import { normalizeSystemRunApprovalPlan } from "./system-run-approval-binding.js"; import { formatExecCommand, resolveSystemRunCommand } from "./system-run-command.js"; +import { normalizeNonEmptyString, normalizeStringArray } from "./system-run-normalize.js"; type PreparedRunPayload = { cmdText: string; @@ -32,18 +33,6 @@ type SystemRunApprovalRuntimeContext = details?: Record; }; -function normalizeString(value: unknown): string | null { - if (typeof value !== "string") { - return null; - } - const trimmed = value.trim(); - return trimmed ? trimmed : null; -} - -function normalizeStringArray(value: unknown): string[] { - return Array.isArray(value) ? value.map((entry) => String(entry)) : []; -} - function normalizeCommandText(value: unknown): string { return typeof value === "string" ? value : ""; } @@ -53,7 +42,7 @@ export function parsePreparedSystemRunPayload(payload: unknown): PreparedRunPayl return null; } const raw = payload as { cmdText?: unknown; plan?: unknown }; - const cmdText = normalizeString(raw.cmdText); + const cmdText = normalizeNonEmptyString(raw.cmdText); const plan = normalizeSystemRunApprovalPlan(raw.plan); if (!cmdText || !plan) { return null; @@ -70,7 +59,7 @@ export function resolveSystemRunApprovalRequestContext(params: { agentId?: unknown; sessionKey?: unknown; }): SystemRunApprovalRequestContext { - const host = normalizeString(params.host) ?? ""; + const host = normalizeNonEmptyString(params.host) ?? ""; const plan = host === "node" ? normalizeSystemRunApprovalPlan(params.systemRunPlan) : null; const fallbackArgv = normalizeStringArray(params.commandArgv); const fallbackCommand = normalizeCommandText(params.command); @@ -78,9 +67,9 @@ export function resolveSystemRunApprovalRequestContext(params: { plan, commandArgv: plan?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined), commandText: plan ? (plan.rawCommand ?? formatExecCommand(plan.argv)) : fallbackCommand, - cwd: plan?.cwd ?? normalizeString(params.cwd), - agentId: plan?.agentId ?? normalizeString(params.agentId), - sessionKey: plan?.sessionKey ?? normalizeString(params.sessionKey), + cwd: plan?.cwd ?? normalizeNonEmptyString(params.cwd), + agentId: plan?.agentId ?? normalizeNonEmptyString(params.agentId), + sessionKey: plan?.sessionKey ?? normalizeNonEmptyString(params.sessionKey), }; } @@ -115,9 +104,9 @@ export function resolveSystemRunApprovalRuntimeContext(params: { ok: true, plan: null, argv: command.argv, - cwd: normalizeString(params.cwd), - agentId: normalizeString(params.agentId), - sessionKey: normalizeString(params.sessionKey), - rawCommand: normalizeString(params.rawCommand), + cwd: normalizeNonEmptyString(params.cwd), + agentId: normalizeNonEmptyString(params.agentId), + sessionKey: normalizeNonEmptyString(params.sessionKey), + rawCommand: normalizeNonEmptyString(params.rawCommand), }; } diff --git a/src/infra/system-run-normalize.ts b/src/infra/system-run-normalize.ts new file mode 100644 index 00000000000..a3d928b9916 --- /dev/null +++ b/src/infra/system-run-normalize.ts @@ -0,0 +1,11 @@ +export function normalizeNonEmptyString(value: unknown): string | null { + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} + +export function normalizeStringArray(value: unknown): string[] { + return Array.isArray(value) ? value.map((entry) => String(entry)) : []; +} diff --git a/src/secrets/apply.ts b/src/secrets/apply.ts index 18208ffe972..a9756b760a1 100644 --- a/src/secrets/apply.ts +++ b/src/secrets/apply.ts @@ -2,7 +2,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { isDeepStrictEqual } from "node:util"; -import { listAgentIds, resolveAgentDir } from "../agents/agent-scope.js"; import { loadAuthProfileStoreForSecretsRuntime } from "../agents/auth-profiles.js"; import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js"; import { normalizeProviderId } from "../agents/model-selection.js"; @@ -10,6 +9,7 @@ import { resolveStateDir, type OpenClawConfig } from "../config/config.js"; import type { ConfigWriteOptions } from "../config/io.js"; import type { SecretProviderConfig } from "../config/types.secrets.js"; import { resolveConfigDir, resolveUserPath } from "../utils.js"; +import { collectAuthStorePaths } from "./auth-store-paths.js"; import { createSecretsConfigIO } from "./config-io.js"; import { type SecretsApplyPlan, @@ -172,36 +172,6 @@ function scrubEnvRaw( }; } -function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string[] { - const paths = new Set(); - // Scope default auth store discovery to the provided stateDir instead of - // ambient process env, so apply does not touch unrelated host-global stores. - paths.add(path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json")); - - const agentsRoot = path.join(resolveUserPath(stateDir), "agents"); - if (fs.existsSync(agentsRoot)) { - for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) { - continue; - } - paths.add(path.join(agentsRoot, entry.name, "agent", "auth-profiles.json")); - } - } - - for (const agentId of listAgentIds(config)) { - if (agentId === "main") { - paths.add( - path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"), - ); - continue; - } - const agentDir = resolveAgentDir(config, agentId); - paths.add(resolveUserPath(resolveAuthStorePath(agentDir))); - } - - return [...paths]; -} - function collectAuthJsonPaths(stateDir: string): string[] { const out: string[] = []; const agentsRoot = path.join(resolveUserPath(stateDir), "agents"); diff --git a/src/secrets/audit.ts b/src/secrets/audit.ts index 4cd71e12c9a..fc4ba874ca6 100644 --- a/src/secrets/audit.ts +++ b/src/secrets/audit.ts @@ -1,12 +1,11 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { listAgentIds, resolveAgentDir } from "../agents/agent-scope.js"; -import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import { resolveStateDir, type OpenClawConfig } from "../config/config.js"; import { coerceSecretRef, type SecretRef } from "../config/types.secrets.js"; import { resolveConfigDir, resolveUserPath } from "../utils.js"; +import { collectAuthStorePaths } from "./auth-store-paths.js"; import { createSecretsConfigIO } from "./config-io.js"; import { listKnownSecretEnvVarNames } from "./provider-env-vars.js"; import { secretRefKey } from "./ref-contract.js"; @@ -306,36 +305,6 @@ function collectConfigSecrets(params: { } } -function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string[] { - const paths = new Set(); - // Scope default auth store discovery to the provided stateDir instead of - // ambient process env, so audits do not include unrelated host-global stores. - paths.add(path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json")); - - const agentsRoot = path.join(resolveUserPath(stateDir), "agents"); - if (fs.existsSync(agentsRoot)) { - for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) { - continue; - } - paths.add(path.join(agentsRoot, entry.name, "agent", "auth-profiles.json")); - } - } - - for (const agentId of listAgentIds(config)) { - if (agentId === "main") { - paths.add( - path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"), - ); - continue; - } - const agentDir = resolveAgentDir(config, agentId); - paths.add(resolveUserPath(resolveAuthStorePath(agentDir))); - } - - return [...paths]; -} - function collectAuthStoreSecrets(params: { authStorePath: string; collector: AuditCollector; diff --git a/src/secrets/auth-store-paths.ts b/src/secrets/auth-store-paths.ts new file mode 100644 index 00000000000..12fe01dda4d --- /dev/null +++ b/src/secrets/auth-store-paths.ts @@ -0,0 +1,36 @@ +import fs from "node:fs"; +import path from "node:path"; +import { listAgentIds, resolveAgentDir } from "../agents/agent-scope.js"; +import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveUserPath } from "../utils.js"; + +export function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string[] { + const paths = new Set(); + // Scope default auth store discovery to the provided stateDir instead of + // ambient process env, so callers do not touch unrelated host-global stores. + paths.add(path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json")); + + const agentsRoot = path.join(resolveUserPath(stateDir), "agents"); + if (fs.existsSync(agentsRoot)) { + for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + paths.add(path.join(agentsRoot, entry.name, "agent", "auth-profiles.json")); + } + } + + for (const agentId of listAgentIds(config)) { + if (agentId === "main") { + paths.add( + path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"), + ); + continue; + } + const agentDir = resolveAgentDir(config, agentId); + paths.add(resolveUserPath(resolveAuthStorePath(agentDir))); + } + + return [...paths]; +}