Files
openclaw/src/secrets/ref-contract.ts
Josh Avant d30dc28b8c Secrets: reject exec SecretRef traversal ids across schema/runtime/gateway (#42370)
* Secrets: harden exec SecretRef validation and reload LKG coverage

* Tests: harden exec fast-exit stdin regression case

* Tests: align lifecycle daemon test formatting with oxfmt 0.36
2026-03-10 13:45:37 -05:00

109 lines
3.0 KiB
TypeScript

import {
DEFAULT_SECRET_PROVIDER_ALIAS,
type SecretRef,
type SecretRefSource,
} from "../config/types.secrets.js";
const FILE_SECRET_REF_SEGMENT_PATTERN = /^(?:[^~]|~0|~1)*$/;
export const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
const EXEC_SECRET_REF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
export const SINGLE_VALUE_FILE_REF_ID = "value";
export const FILE_SECRET_REF_ID_PATTERN = /^(?:value|\/(?:[^~]|~0|~1)*(?:\/(?:[^~]|~0|~1)*)*)$/;
export const EXEC_SECRET_REF_ID_JSON_SCHEMA_PATTERN =
"^(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$";
export type ExecSecretRefIdValidationReason = "pattern" | "traversal-segment";
export type ExecSecretRefIdValidationResult =
| { ok: true }
| {
ok: false;
reason: ExecSecretRefIdValidationReason;
};
export type SecretRefDefaultsCarrier = {
secrets?: {
defaults?: {
env?: string;
file?: string;
exec?: string;
};
providers?: Record<string, { source?: string }>;
};
};
export function secretRefKey(ref: SecretRef): string {
return `${ref.source}:${ref.provider}:${ref.id}`;
}
export function resolveDefaultSecretProviderAlias(
config: SecretRefDefaultsCarrier,
source: SecretRefSource,
options?: { preferFirstProviderForSource?: boolean },
): string {
const configured =
source === "env"
? config.secrets?.defaults?.env
: source === "file"
? config.secrets?.defaults?.file
: config.secrets?.defaults?.exec;
if (configured?.trim()) {
return configured.trim();
}
if (options?.preferFirstProviderForSource) {
const providers = config.secrets?.providers;
if (providers) {
for (const [providerName, provider] of Object.entries(providers)) {
if (provider?.source === source) {
return providerName;
}
}
}
}
return DEFAULT_SECRET_PROVIDER_ALIAS;
}
export function isValidFileSecretRefId(value: string): boolean {
if (value === SINGLE_VALUE_FILE_REF_ID) {
return true;
}
if (!value.startsWith("/")) {
return false;
}
return value
.slice(1)
.split("/")
.every((segment) => FILE_SECRET_REF_SEGMENT_PATTERN.test(segment));
}
export function isValidSecretProviderAlias(value: string): boolean {
return SECRET_PROVIDER_ALIAS_PATTERN.test(value);
}
export function validateExecSecretRefId(value: string): ExecSecretRefIdValidationResult {
if (!EXEC_SECRET_REF_ID_PATTERN.test(value)) {
return { ok: false, reason: "pattern" };
}
for (const segment of value.split("/")) {
if (segment === "." || segment === "..") {
return { ok: false, reason: "traversal-segment" };
}
}
return { ok: true };
}
export function isValidExecSecretRefId(value: string): boolean {
return validateExecSecretRefId(value).ok;
}
export function formatExecSecretRefIdValidationMessage(): string {
return [
"Exec secret reference id must match /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/",
'and must not include "." or ".." path segments',
'(example: "vault/openai/api-key").',
].join(" ");
}