mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
refactor(security): unify command gating and blocked-key guards
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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];
|
||||
|
||||
5
src/config/prototype-keys.ts
Normal file
5
src/config/prototype-keys.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user