Security: owner-only tools + command auth hardening (#9202)
* Security: gate whatsapp_login by sender auth * Security: treat undefined senderAuthorized as unauthorized (opt-in) * fix: gate whatsapp_login to owner senders (#8768) (thanks @victormier) * fix: add explicit owner allowlist for tools (#8768) (thanks @victormier) * fix: normalize escaped newlines in send actions (#8768) (thanks @victormier) --------- Co-authored-by: Victor Mier <victormier@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0cd47d830f
commit
392bbddf29
@@ -9,6 +9,7 @@ export type CommandAuthorization = {
|
||||
providerId?: ChannelId;
|
||||
ownerList: string[];
|
||||
senderId?: string;
|
||||
senderIsOwner: boolean;
|
||||
isAuthorizedSender: boolean;
|
||||
from?: string;
|
||||
to?: string;
|
||||
@@ -83,6 +84,47 @@ function normalizeAllowFromEntry(params: {
|
||||
return normalized.filter((entry) => entry.trim().length > 0);
|
||||
}
|
||||
|
||||
function resolveOwnerAllowFromList(params: {
|
||||
dock?: ChannelDock;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
providerId?: ChannelId;
|
||||
}): string[] {
|
||||
const raw = params.cfg.commands?.ownerAllowFrom;
|
||||
if (!Array.isArray(raw) || raw.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const filtered: string[] = [];
|
||||
for (const entry of raw) {
|
||||
const trimmed = String(entry ?? "").trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const separatorIndex = trimmed.indexOf(":");
|
||||
if (separatorIndex > 0) {
|
||||
const prefix = trimmed.slice(0, separatorIndex);
|
||||
const channel = normalizeAnyChannelId(prefix);
|
||||
if (channel) {
|
||||
if (params.providerId && channel !== params.providerId) {
|
||||
continue;
|
||||
}
|
||||
const remainder = trimmed.slice(separatorIndex + 1).trim();
|
||||
if (remainder) {
|
||||
filtered.push(remainder);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
filtered.push(trimmed);
|
||||
}
|
||||
return formatAllowFromList({
|
||||
dock: params.dock,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
allowFrom: filtered,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveSenderCandidates(params: {
|
||||
dock?: ChannelDock;
|
||||
providerId?: ChannelId;
|
||||
@@ -141,11 +183,17 @@ export function resolveCommandAuthorization(params: {
|
||||
accountId: ctx.AccountId,
|
||||
allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [],
|
||||
});
|
||||
const ownerAllowFromList = resolveOwnerAllowFromList({
|
||||
dock,
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
providerId,
|
||||
});
|
||||
const allowAll =
|
||||
allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*");
|
||||
|
||||
const ownerCandidates = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
|
||||
if (!allowAll && ownerCandidates.length === 0 && to) {
|
||||
const ownerCandidatesForCommands = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
|
||||
if (!allowAll && ownerCandidatesForCommands.length === 0 && to) {
|
||||
const normalizedTo = normalizeAllowFromEntry({
|
||||
dock,
|
||||
cfg,
|
||||
@@ -153,10 +201,13 @@ export function resolveCommandAuthorization(params: {
|
||||
value: to,
|
||||
});
|
||||
if (normalizedTo.length > 0) {
|
||||
ownerCandidates.push(...normalizedTo);
|
||||
ownerCandidatesForCommands.push(...normalizedTo);
|
||||
}
|
||||
}
|
||||
const ownerList = Array.from(new Set(ownerCandidates));
|
||||
const explicitOwners = ownerAllowFromList.filter((entry) => entry !== "*");
|
||||
const ownerList = Array.from(
|
||||
new Set(explicitOwners.length > 0 ? explicitOwners : ownerCandidatesForCommands),
|
||||
);
|
||||
|
||||
const senderCandidates = resolveSenderCandidates({
|
||||
dock,
|
||||
@@ -170,16 +221,25 @@ export function resolveCommandAuthorization(params: {
|
||||
const matchedSender = ownerList.length
|
||||
? senderCandidates.find((candidate) => ownerList.includes(candidate))
|
||||
: undefined;
|
||||
const matchedCommandOwner = ownerCandidatesForCommands.length
|
||||
? senderCandidates.find((candidate) => ownerCandidatesForCommands.includes(candidate))
|
||||
: undefined;
|
||||
const senderId = matchedSender ?? senderCandidates[0];
|
||||
|
||||
const enforceOwner = Boolean(dock?.commands?.enforceOwnerForCommands);
|
||||
const isOwner = !enforceOwner || allowAll || ownerList.length === 0 || Boolean(matchedSender);
|
||||
const isAuthorizedSender = commandAuthorized && isOwner;
|
||||
const senderIsOwner = Boolean(matchedSender);
|
||||
const isOwnerForCommands =
|
||||
!enforceOwner ||
|
||||
allowAll ||
|
||||
ownerCandidatesForCommands.length === 0 ||
|
||||
Boolean(matchedCommandOwner);
|
||||
const isAuthorizedSender = commandAuthorized && isOwnerForCommands;
|
||||
|
||||
return {
|
||||
providerId,
|
||||
ownerList,
|
||||
senderId: senderId || undefined,
|
||||
senderIsOwner,
|
||||
isAuthorizedSender,
|
||||
from: from || undefined,
|
||||
to: to || undefined,
|
||||
|
||||
Reference in New Issue
Block a user