refactor(security): unify command gating and blocked-key guards

This commit is contained in:
Peter Steinberger
2026-02-21 13:04:31 +01:00
parent 356d61aacf
commit 08e020881d
10 changed files with 111 additions and 58 deletions

View File

@@ -1,7 +1,11 @@
import { normalizeChannelId } from "../channels/plugins/index.js";
import type { ChannelId } from "../channels/plugins/types.js";
import { isPlainObject } from "../infra/plain-object.js";
import type { NativeCommandsSetting } from "./types.js";
import type { CommandsConfig, NativeCommandsSetting } from "./types.js";
export type CommandFlagKey = {
[K in keyof CommandsConfig]-?: Exclude<CommandsConfig[K], undefined> extends boolean ? K : never;
}[keyof CommandsConfig];
function resolveAutoDefault(providerId?: ChannelId): boolean {
const id = normalizeChannelId(providerId);
@@ -63,7 +67,10 @@ export function isNativeCommandsExplicitlyDisabled(params: {
return false;
}
function getOwnCommandFlagValue(config: { commands?: unknown } | undefined, key: string): unknown {
function getOwnCommandFlagValue(
config: { commands?: unknown } | undefined,
key: CommandFlagKey,
): unknown {
const { commands } = config ?? {};
if (!isPlainObject(commands) || !Object.hasOwn(commands, key)) {
return undefined;
@@ -73,7 +80,7 @@ function getOwnCommandFlagValue(config: { commands?: unknown } | undefined, key:
export function isCommandFlagEnabled(
config: { commands?: unknown } | undefined,
key: string,
key: CommandFlagKey,
): boolean {
return getOwnCommandFlagValue(config, key) === true;
}

View File

@@ -1,9 +1,8 @@
import { isPlainObject } from "../utils.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
type PathNode = Record<string, unknown>;
const BLOCKED_KEYS = new Set(["__proto__", "prototype", "constructor"]);
export function parseConfigPath(raw: string): {
ok: boolean;
path?: string[];
@@ -23,7 +22,7 @@ export function parseConfigPath(raw: string): {
error: "Invalid path. Use dot notation (e.g. foo.bar).",
};
}
if (parts.some((part) => BLOCKED_KEYS.has(part))) {
if (parts.some((part) => isBlockedObjectKey(part))) {
return { ok: false, error: "Invalid path segment." };
}
return { ok: true, path: parts };

View File

@@ -15,6 +15,7 @@ import path from "node:path";
import JSON5 from "json5";
import { isPathInside } from "../security/scan-paths.js";
import { isPlainObject } from "../utils.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
export const INCLUDE_KEY = "$include";
export const MAX_INCLUDE_DEPTH = 10;
@@ -54,8 +55,6 @@ export class CircularIncludeError extends ConfigIncludeError {
// Utilities
// ============================================================================
const BLOCKED_MERGE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
/** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */
export function deepMerge(target: unknown, source: unknown): unknown {
if (Array.isArray(target) && Array.isArray(source)) {
@@ -64,7 +63,7 @@ export function deepMerge(target: unknown, source: unknown): unknown {
if (isPlainObject(target) && isPlainObject(source)) {
const result: Record<string, unknown> = { ...target };
for (const key of Object.keys(source)) {
if (BLOCKED_MERGE_KEYS.has(key)) {
if (isBlockedObjectKey(key)) {
continue;
}
result[key] = key in result ? deepMerge(result[key], source[key]) : source[key];

View File

@@ -0,0 +1,5 @@
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
export function isBlockedObjectKey(key: string): boolean {
return BLOCKED_OBJECT_KEYS.has(key);
}

View File

@@ -1,11 +1,10 @@
import { isPlainObject } from "../utils.js";
import { parseConfigPath, setConfigValueAtPath, unsetConfigValueAtPath } from "./config-paths.js";
import { isBlockedObjectKey } from "./prototype-keys.js";
import type { OpenClawConfig } from "./types.js";
type OverrideTree = Record<string, unknown>;
const BLOCKED_MERGE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
let overrides: OverrideTree = {};
function sanitizeOverrideValue(value: unknown, seen = new WeakSet<object>()): unknown {
@@ -21,7 +20,7 @@ function sanitizeOverrideValue(value: unknown, seen = new WeakSet<object>()): un
seen.add(value);
const sanitized: OverrideTree = {};
for (const [key, entry] of Object.entries(value)) {
if (entry === undefined || BLOCKED_MERGE_KEYS.has(key)) {
if (entry === undefined || isBlockedObjectKey(key)) {
continue;
}
sanitized[key] = sanitizeOverrideValue(entry, seen);
@@ -36,7 +35,7 @@ function mergeOverrides(base: unknown, override: unknown): unknown {
}
const next: OverrideTree = { ...base };
for (const [key, value] of Object.entries(override)) {
if (value === undefined || BLOCKED_MERGE_KEYS.has(key)) {
if (value === undefined || isBlockedObjectKey(key)) {
continue;
}
next[key] = mergeOverrides((base as OverrideTree)[key], value);