fix(security): block startup-file env injection across host execution paths

This commit is contained in:
Peter Steinberger
2026-02-21 11:43:53 +01:00
parent 6b2f2811dc
commit 2cdbadee1f
13 changed files with 318 additions and 147 deletions

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../../config/config.js";
import { isDangerousHostEnvVarName } from "../../infra/host-env-security.js";
import { sanitizeEnvVars, validateEnvVarValue } from "../sandbox/sanitize-env-vars.js";
import { resolveSkillConfig } from "./config.js";
import { resolveSkillKey } from "./frontmatter.js";
@@ -13,18 +14,19 @@ type SanitizedSkillEnvOverrides = {
warnings: string[];
};
// Never allow skill env overrides that can alter runtime loader flags.
const HARD_BLOCKED_SKILL_ENV_PATTERNS: ReadonlyArray<RegExp> = [
/^NODE_OPTIONS$/i,
/^OPENSSL_CONF$/i,
/^LD_PRELOAD$/i,
/^DYLD_INSERT_LIBRARIES$/i,
];
// Always block skill env overrides that can alter runtime loading or host execution behavior.
const SKILL_ALWAYS_BLOCKED_ENV_PATTERNS: ReadonlyArray<RegExp> = [/^OPENSSL_CONF$/i];
function matchesAnyPattern(value: string, patterns: readonly RegExp[]): boolean {
return patterns.some((pattern) => pattern.test(value));
}
function isAlwaysBlockedSkillEnvKey(key: string): boolean {
return (
isDangerousHostEnvVarName(key) || matchesAnyPattern(key, SKILL_ALWAYS_BLOCKED_ENV_PATTERNS)
);
}
function sanitizeSkillEnvOverrides(params: {
overrides: Record<string, string>;
allowedSensitiveKeys: Set<string>;
@@ -33,19 +35,22 @@ function sanitizeSkillEnvOverrides(params: {
return { allowed: {}, blocked: [], warnings: [] };
}
const result = sanitizeEnvVars(params.overrides, {
customBlockedPatterns: HARD_BLOCKED_SKILL_ENV_PATTERNS,
});
const allowed = { ...result.allowed };
const blocked: string[] = [];
const result = sanitizeEnvVars(params.overrides);
const allowed: Record<string, string> = {};
const blocked = new Set<string>();
const warnings = [...result.warnings];
for (const [key, value] of Object.entries(result.allowed)) {
if (isAlwaysBlockedSkillEnvKey(key)) {
blocked.add(key);
continue;
}
allowed[key] = value;
}
for (const key of result.blocked) {
if (
matchesAnyPattern(key, HARD_BLOCKED_SKILL_ENV_PATTERNS) ||
!params.allowedSensitiveKeys.has(key)
) {
blocked.push(key);
if (isAlwaysBlockedSkillEnvKey(key) || !params.allowedSensitiveKeys.has(key)) {
blocked.add(key);
continue;
}
const value = params.overrides[key];
@@ -55,7 +60,7 @@ function sanitizeSkillEnvOverrides(params: {
const warning = validateEnvVarValue(value);
if (warning) {
if (warning === "Contains null bytes") {
blocked.push(key);
blocked.add(key);
continue;
}
warnings.push(`${key}: ${warning}`);
@@ -63,7 +68,7 @@ function sanitizeSkillEnvOverrides(params: {
allowed[key] = value;
}
return { allowed, blocked, warnings };
return { allowed, blocked: [...blocked], warnings };
}
function applySkillConfigEnvOverrides(params: {