mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 16:33:42 +00:00
fix(secrets): harden plan target paths and ref-only auth profiles
This commit is contained in:
@@ -45,6 +45,15 @@ export type SecretsApplyPlan = {
|
||||
};
|
||||
|
||||
const PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
||||
const FORBIDDEN_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
||||
|
||||
function isSecretsPlanTargetType(value: unknown): value is SecretsPlanTargetType {
|
||||
return (
|
||||
value === "models.providers.apiKey" ||
|
||||
value === "skills.entries.apiKey" ||
|
||||
value === "channels.googlechat.serviceAccount"
|
||||
);
|
||||
}
|
||||
|
||||
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
@@ -54,6 +63,104 @@ function isSecretProviderConfigShape(value: unknown): value is SecretProviderCon
|
||||
return SecretProviderSchema.safeParse(value).success;
|
||||
}
|
||||
|
||||
function parseDotPath(pathname: string): string[] {
|
||||
return pathname
|
||||
.split(".")
|
||||
.map((segment) => segment.trim())
|
||||
.filter((segment) => segment.length > 0);
|
||||
}
|
||||
|
||||
function hasForbiddenPathSegment(segments: string[]): boolean {
|
||||
return segments.some((segment) => FORBIDDEN_PATH_SEGMENTS.has(segment));
|
||||
}
|
||||
|
||||
function hasMatchingPathShape(
|
||||
candidate: Pick<SecretsPlanTarget, "type" | "providerId" | "accountId">,
|
||||
segments: string[],
|
||||
): boolean {
|
||||
if (candidate.type === "models.providers.apiKey") {
|
||||
if (
|
||||
segments.length !== 4 ||
|
||||
segments[0] !== "models" ||
|
||||
segments[1] !== "providers" ||
|
||||
segments[3] !== "apiKey"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
candidate.providerId === undefined ||
|
||||
candidate.providerId.trim().length === 0 ||
|
||||
candidate.providerId === segments[2]
|
||||
);
|
||||
}
|
||||
if (candidate.type === "skills.entries.apiKey") {
|
||||
return (
|
||||
segments.length === 4 &&
|
||||
segments[0] === "skills" &&
|
||||
segments[1] === "entries" &&
|
||||
segments[3] === "apiKey"
|
||||
);
|
||||
}
|
||||
if (
|
||||
segments.length === 3 &&
|
||||
segments[0] === "channels" &&
|
||||
segments[1] === "googlechat" &&
|
||||
segments[2] === "serviceAccount"
|
||||
) {
|
||||
return candidate.accountId === undefined || candidate.accountId.trim().length === 0;
|
||||
}
|
||||
if (
|
||||
segments.length === 5 &&
|
||||
segments[0] === "channels" &&
|
||||
segments[1] === "googlechat" &&
|
||||
segments[2] === "accounts" &&
|
||||
segments[4] === "serviceAccount"
|
||||
) {
|
||||
return (
|
||||
candidate.accountId === undefined ||
|
||||
candidate.accountId.trim().length === 0 ||
|
||||
candidate.accountId === segments[3]
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function resolveValidatedTargetPathSegments(candidate: {
|
||||
type?: SecretsPlanTargetType;
|
||||
path?: string;
|
||||
pathSegments?: string[];
|
||||
providerId?: string;
|
||||
accountId?: string;
|
||||
}): string[] | null {
|
||||
if (!isSecretsPlanTargetType(candidate.type)) {
|
||||
return null;
|
||||
}
|
||||
const path = typeof candidate.path === "string" ? candidate.path.trim() : "";
|
||||
if (!path) {
|
||||
return null;
|
||||
}
|
||||
const segments =
|
||||
Array.isArray(candidate.pathSegments) && candidate.pathSegments.length > 0
|
||||
? candidate.pathSegments.map((segment) => String(segment).trim()).filter(Boolean)
|
||||
: parseDotPath(path);
|
||||
if (
|
||||
segments.length === 0 ||
|
||||
hasForbiddenPathSegment(segments) ||
|
||||
path !== segments.join(".") ||
|
||||
!hasMatchingPathShape(
|
||||
{
|
||||
type: candidate.type,
|
||||
providerId: candidate.providerId,
|
||||
accountId: candidate.accountId,
|
||||
},
|
||||
segments,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function isSecretsApplyPlan(value: unknown): value is SecretsApplyPlan {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
@@ -74,12 +181,14 @@ export function isSecretsApplyPlan(value: unknown): value is SecretsApplyPlan {
|
||||
candidate.type !== "channels.googlechat.serviceAccount") ||
|
||||
typeof candidate.path !== "string" ||
|
||||
!candidate.path.trim() ||
|
||||
(candidate.pathSegments !== undefined &&
|
||||
(!Array.isArray(candidate.pathSegments) ||
|
||||
candidate.pathSegments.length === 0 ||
|
||||
candidate.pathSegments.some(
|
||||
(segment) => typeof segment !== "string" || segment.trim().length === 0,
|
||||
))) ||
|
||||
(candidate.pathSegments !== undefined && !Array.isArray(candidate.pathSegments)) ||
|
||||
!resolveValidatedTargetPathSegments({
|
||||
type: candidate.type,
|
||||
path: candidate.path,
|
||||
pathSegments: candidate.pathSegments,
|
||||
providerId: candidate.providerId,
|
||||
accountId: candidate.accountId,
|
||||
}) ||
|
||||
!ref ||
|
||||
typeof ref !== "object" ||
|
||||
(ref.source !== "env" && ref.source !== "file" && ref.source !== "exec") ||
|
||||
|
||||
Reference in New Issue
Block a user