mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 04:10:24 +00:00
refactor: dedupe security lowercase helpers
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
|||||||
MemoryEmbeddingProvider,
|
MemoryEmbeddingProvider,
|
||||||
MemoryEmbeddingProviderAdapter,
|
MemoryEmbeddingProviderAdapter,
|
||||||
} from "../plugins/memory-embedding-providers.js";
|
} from "../plugins/memory-embedding-providers.js";
|
||||||
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||||
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
||||||
import type { ResolvedGatewayAuth } from "./auth.js";
|
import type { ResolvedGatewayAuth } from "./auth.js";
|
||||||
import { sendJson } from "./http-common.js";
|
import { sendJson } from "./http-common.js";
|
||||||
@@ -175,7 +176,7 @@ function resolveEmbeddingsTarget(params: {
|
|||||||
return { provider: params.configuredProvider, model: raw };
|
return { provider: params.configuredProvider, model: raw };
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = raw.slice(0, slash).trim().toLowerCase();
|
const provider = normalizeLowercaseStringOrEmpty(raw.slice(0, slash));
|
||||||
const model = raw.slice(slash + 1).trim();
|
const model = raw.slice(slash + 1).trim();
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return { errorMessage: "Unsupported embedding model reference." };
|
return { errorMessage: "Unsupported embedding model reference." };
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||||
import { isLoopbackHost, normalizeHostHeader } from "./net.js";
|
import { isLoopbackHost, normalizeHostHeader } from "./net.js";
|
||||||
|
|
||||||
type OriginCheckResult =
|
type OriginCheckResult =
|
||||||
@@ -39,7 +40,9 @@ export function checkBrowserOrigin(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allowlist = new Set(
|
const allowlist = new Set(
|
||||||
(params.allowedOrigins ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean),
|
(params.allowedOrigins ?? [])
|
||||||
|
.map((value) => normalizeOptionalLowercaseString(value))
|
||||||
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
if (allowlist.has("*") || allowlist.has(parsedOrigin.origin)) {
|
if (allowlist.has("*") || allowlist.has(parsedOrigin.origin)) {
|
||||||
return { ok: true, matchedBy: "allowlist" };
|
return { ok: true, matchedBy: "allowlist" };
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { loadConfig } from "../config/config.js";
|
|||||||
import type { createSubsystemLogger } from "../logging/subsystem.js";
|
import type { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
import { resolveHookExternalContentSource as resolveHookExternalContentSourceFromSession } from "../security/external-content.js";
|
import { resolveHookExternalContentSource as resolveHookExternalContentSourceFromSession } from "../security/external-content.js";
|
||||||
import { safeEqualSecret } from "../security/secret-equal.js";
|
import { safeEqualSecret } from "../security/secret-equal.js";
|
||||||
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||||
import {
|
import {
|
||||||
AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH,
|
AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH,
|
||||||
createAuthRateLimiter,
|
createAuthRateLimiter,
|
||||||
@@ -97,8 +98,7 @@ function resolveMappedHookExternalContentSource(params: {
|
|||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
}) {
|
}) {
|
||||||
const payloadSource =
|
const payloadSource = normalizeLowercaseStringOrEmpty(params.payload.source);
|
||||||
typeof params.payload.source === "string" ? params.payload.source.trim().toLowerCase() : "";
|
|
||||||
if (params.subPath === "gmail" || payloadSource === "gmail") {
|
if (params.subPath === "gmail" || payloadSource === "gmail") {
|
||||||
return "gmail" as const;
|
return "gmail" as const;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { deriveSessionTotalTokens, hasNonzeroUsage, normalizeUsage } from "../ag
|
|||||||
import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js";
|
import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js";
|
||||||
import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js";
|
import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js";
|
||||||
import { extractAssistantVisibleText } from "../shared/chat-message-content.js";
|
import { extractAssistantVisibleText } from "../shared/chat-message-content.js";
|
||||||
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||||
import { stripInlineDirectiveTagsForDisplay } from "../utils/directive-tags.js";
|
import { stripInlineDirectiveTagsForDisplay } from "../utils/directive-tags.js";
|
||||||
import { extractToolCallNames, hasToolCall } from "../utils/transcript-tools.js";
|
import { extractToolCallNames, hasToolCall } from "../utils/transcript-tools.js";
|
||||||
import { stripEnvelope } from "./chat-sanitize.js";
|
import { stripEnvelope } from "./chat-sanitize.js";
|
||||||
@@ -631,7 +632,7 @@ function truncatePreviewText(text: string, maxChars: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractPreviewText(message: TranscriptPreviewMessage): string | null {
|
function extractPreviewText(message: TranscriptPreviewMessage): string | null {
|
||||||
const role = typeof message.role === "string" ? message.role.trim().toLowerCase() : "";
|
const role = normalizeLowercaseStringOrEmpty(message.role);
|
||||||
if (role === "assistant") {
|
if (role === "assistant") {
|
||||||
const assistantText = extractAssistantVisibleText(message);
|
const assistantText = extractAssistantVisibleText(message);
|
||||||
if (assistantText) {
|
if (assistantText) {
|
||||||
@@ -674,7 +675,7 @@ function extractMediaSummary(message: TranscriptPreviewMessage): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (const entry of message.content) {
|
for (const entry of message.content) {
|
||||||
const raw = typeof entry?.type === "string" ? entry.type.trim().toLowerCase() : "";
|
const raw = normalizeLowercaseStringOrEmpty(entry?.type);
|
||||||
if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") {
|
if (!raw || raw === "text" || raw === "toolcall" || raw === "tool_call") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
import {
|
||||||
|
normalizeLowercaseStringOrEmpty,
|
||||||
|
normalizeOptionalString,
|
||||||
|
} from "../shared/string-coerce.js";
|
||||||
import { isDispatchWrapperExecutable } from "./dispatch-wrapper-resolution.js";
|
import { isDispatchWrapperExecutable } from "./dispatch-wrapper-resolution.js";
|
||||||
import {
|
import {
|
||||||
analyzeShellCommand,
|
analyzeShellCommand,
|
||||||
@@ -48,7 +51,7 @@ export function normalizeSafeBins(entries?: readonly string[]): Set<string> {
|
|||||||
return new Set();
|
return new Set();
|
||||||
}
|
}
|
||||||
const normalized = entries
|
const normalized = entries
|
||||||
.map((entry) => entry.trim().toLowerCase())
|
.map((entry) => normalizeLowercaseStringOrEmpty(entry))
|
||||||
.filter((entry) => entry.length > 0);
|
.filter((entry) => entry.length > 0);
|
||||||
return new Set(normalized);
|
return new Set(normalized);
|
||||||
}
|
}
|
||||||
@@ -978,11 +981,11 @@ function collectAllowAlwaysPatterns(params: {
|
|||||||
const isPowerShellFileInvocation =
|
const isPowerShellFileInvocation =
|
||||||
POWERSHELL_WRAPPERS.has(normalizeExecutableToken(segment.argv[0] ?? "")) &&
|
POWERSHELL_WRAPPERS.has(normalizeExecutableToken(segment.argv[0] ?? "")) &&
|
||||||
segment.argv.some((t) => {
|
segment.argv.some((t) => {
|
||||||
const lower = t.trim().toLowerCase();
|
const lower = normalizeLowercaseStringOrEmpty(t);
|
||||||
return lower === "-file" || lower === "-f";
|
return lower === "-file" || lower === "-f";
|
||||||
}) &&
|
}) &&
|
||||||
!segment.argv.some((t) => {
|
!segment.argv.some((t) => {
|
||||||
const lower = t.trim().toLowerCase();
|
const lower = normalizeLowercaseStringOrEmpty(t);
|
||||||
return lower === "-command" || lower === "-c" || lower === "--command";
|
return lower === "-command" || lower === "-c" || lower === "--command";
|
||||||
});
|
});
|
||||||
const inlineCommand = isPowerShellFileInvocation
|
const inlineCommand = isPowerShellFileInvocation
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||||
|
|
||||||
export type SafeBinSemanticValidationParams = {
|
export type SafeBinSemanticValidationParams = {
|
||||||
binName?: string;
|
binName?: string;
|
||||||
positional: readonly string[];
|
positional: readonly string[];
|
||||||
@@ -53,7 +55,7 @@ const SAFE_BIN_SEMANTIC_RULES: Readonly<Record<string, SafeBinSemanticRule>> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function normalizeSafeBinName(raw: string): string {
|
export function normalizeSafeBinName(raw: string): string {
|
||||||
const trimmed = raw.trim().toLowerCase();
|
const trimmed = normalizeLowercaseStringOrEmpty(raw);
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type { AgentToolsConfig } from "../config/types.tools.js";
|
|||||||
import { readInstalledPackageVersion } from "../infra/package-update-utils.js";
|
import { readInstalledPackageVersion } from "../infra/package-update-utils.js";
|
||||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||||
import { normalizeAgentId } from "../routing/session-key.js";
|
import { normalizeAgentId } from "../routing/session-key.js";
|
||||||
|
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||||
import {
|
import {
|
||||||
formatPermissionDetail,
|
formatPermissionDetail,
|
||||||
formatPermissionRemediation,
|
formatPermissionRemediation,
|
||||||
@@ -239,7 +240,11 @@ function resolveToolPolicies(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizePluginIdSet(entries: string[]): Set<string> {
|
function normalizePluginIdSet(entries: string[]): Set<string> {
|
||||||
return new Set(entries.map((entry) => entry.trim().toLowerCase()).filter(Boolean));
|
return new Set(
|
||||||
|
entries
|
||||||
|
.map((entry) => normalizeOptionalLowercaseString(entry))
|
||||||
|
.filter((entry): entry is string => Boolean(entry)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveEnabledExtensionPluginIds(params: {
|
function resolveEnabledExtensionPluginIds(params: {
|
||||||
@@ -255,12 +260,16 @@ function resolveEnabledExtensionPluginIds(params: {
|
|||||||
const denySet = normalizePluginIdSet(normalized.deny);
|
const denySet = normalizePluginIdSet(normalized.deny);
|
||||||
const entryById = new Map<string, { enabled?: boolean }>();
|
const entryById = new Map<string, { enabled?: boolean }>();
|
||||||
for (const [id, entry] of Object.entries(normalized.entries)) {
|
for (const [id, entry] of Object.entries(normalized.entries)) {
|
||||||
entryById.set(id.trim().toLowerCase(), entry);
|
const normalizedId = normalizeOptionalLowercaseString(id);
|
||||||
|
if (!normalizedId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entryById.set(normalizedId, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabled: string[] = [];
|
const enabled: string[] = [];
|
||||||
for (const id of params.pluginDirs) {
|
for (const id of params.pluginDirs) {
|
||||||
const normalizedId = id.trim().toLowerCase();
|
const normalizedId = normalizeOptionalLowercaseString(id);
|
||||||
if (!normalizedId) {
|
if (!normalizedId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -286,7 +295,9 @@ function collectAllowEntries(config?: { allow?: string[]; alsoAllow?: string[] }
|
|||||||
if (Array.isArray(config?.alsoAllow)) {
|
if (Array.isArray(config?.alsoAllow)) {
|
||||||
out.push(...config.alsoAllow);
|
out.push(...config.alsoAllow);
|
||||||
}
|
}
|
||||||
return out.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
return out
|
||||||
|
.map((entry) => normalizeOptionalLowercaseString(entry))
|
||||||
|
.filter((entry): entry is string => Boolean(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasExplicitPluginAllow(params: {
|
function hasExplicitPluginAllow(params: {
|
||||||
@@ -496,7 +507,7 @@ function parsePublishedHostFromDockerPortLine(line: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isLoopbackPublishHost(host: string): boolean {
|
function isLoopbackPublishHost(host: string): boolean {
|
||||||
const normalized = host.trim().toLowerCase();
|
const normalized = normalizeOptionalLowercaseString(host);
|
||||||
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
DEFAULT_DANGEROUS_NODE_COMMANDS,
|
DEFAULT_DANGEROUS_NODE_COMMANDS,
|
||||||
resolveNodeCommandAllowlist,
|
resolveNodeCommandAllowlist,
|
||||||
} from "../gateway/node-command-policy.js";
|
} from "../gateway/node-command-policy.js";
|
||||||
|
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||||
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
|
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
|
||||||
|
|
||||||
export type SecurityAuditFinding = {
|
export type SecurityAuditFinding = {
|
||||||
@@ -815,7 +816,7 @@ export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): Secu
|
|||||||
|
|
||||||
const seccompProfile =
|
const seccompProfile =
|
||||||
typeof docker.seccompProfile === "string" ? docker.seccompProfile : undefined;
|
typeof docker.seccompProfile === "string" ? docker.seccompProfile : undefined;
|
||||||
if (seccompProfile && seccompProfile.trim().toLowerCase() === "unconfined") {
|
if (normalizeOptionalLowercaseString(seccompProfile) === "unconfined") {
|
||||||
findings.push({
|
findings.push({
|
||||||
checkId: "sandbox.dangerous_seccomp_profile",
|
checkId: "sandbox.dangerous_seccomp_profile",
|
||||||
severity: "critical",
|
severity: "critical",
|
||||||
@@ -827,7 +828,7 @@ export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): Secu
|
|||||||
|
|
||||||
const apparmorProfile =
|
const apparmorProfile =
|
||||||
typeof docker.apparmorProfile === "string" ? docker.apparmorProfile : undefined;
|
typeof docker.apparmorProfile === "string" ? docker.apparmorProfile : undefined;
|
||||||
if (apparmorProfile && apparmorProfile.trim().toLowerCase() === "unconfined") {
|
if (normalizeOptionalLowercaseString(apparmorProfile) === "unconfined") {
|
||||||
findings.push({
|
findings.push({
|
||||||
checkId: "sandbox.dangerous_apparmor_profile",
|
checkId: "sandbox.dangerous_apparmor_profile",
|
||||||
severity: "critical",
|
severity: "critical",
|
||||||
@@ -842,7 +843,7 @@ export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): Secu
|
|||||||
const defaultBrowser = resolveSandboxConfigForAgent(cfg).browser;
|
const defaultBrowser = resolveSandboxConfigForAgent(cfg).browser;
|
||||||
if (
|
if (
|
||||||
defaultBrowser.enabled &&
|
defaultBrowser.enabled &&
|
||||||
defaultBrowser.network.trim().toLowerCase() === "bridge" &&
|
normalizeOptionalLowercaseString(defaultBrowser.network) === "bridge" &&
|
||||||
!defaultBrowser.cdpSourceRange?.trim()
|
!defaultBrowser.cdpSourceRange?.trim()
|
||||||
) {
|
) {
|
||||||
browserExposurePaths.push("agents.defaults.sandbox.browser");
|
browserExposurePaths.push("agents.defaults.sandbox.browser");
|
||||||
@@ -855,7 +856,7 @@ export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): Secu
|
|||||||
if (!browser.enabled) {
|
if (!browser.enabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (browser.network.trim().toLowerCase() !== "bridge") {
|
if (normalizeOptionalLowercaseString(browser.network) !== "bridge") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (browser.cdpSourceRange?.trim()) {
|
if (browser.cdpSourceRange?.trim()) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security utilities for handling untrusted external content.
|
* Security utilities for handling untrusted external content.
|
||||||
@@ -109,7 +110,7 @@ const EXTERNAL_SOURCE_LABELS: Record<ExternalContentSource, string> = {
|
|||||||
export function resolveHookExternalContentSource(
|
export function resolveHookExternalContentSource(
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
): HookExternalContentSource | undefined {
|
): HookExternalContentSource | undefined {
|
||||||
const normalized = sessionKey.trim().toLowerCase();
|
const normalized = normalizeLowercaseStringOrEmpty(sessionKey);
|
||||||
if (normalized.startsWith("hook:gmail:")) {
|
if (normalized.startsWith("hook:gmail:")) {
|
||||||
return "gmail";
|
return "gmail";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { runExec } from "../process/exec.js";
|
import { runExec } from "../process/exec.js";
|
||||||
|
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||||
|
|
||||||
export type ExecFn = typeof runExec;
|
export type ExecFn = typeof runExec;
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ const STATUS_PREFIXES = [
|
|||||||
"no mapping between account names",
|
"no mapping between account names",
|
||||||
];
|
];
|
||||||
|
|
||||||
const normalize = (value: string) => value.trim().toLowerCase();
|
const normalize = (value: string) => normalizeLowercaseStringOrEmpty(value);
|
||||||
|
|
||||||
function normalizeSid(value: string): string {
|
function normalizeSid(value: string): string {
|
||||||
const normalized = normalize(value);
|
const normalized = normalize(value);
|
||||||
|
|||||||
Reference in New Issue
Block a user