fix(security): enforce trusted sender auth for discord moderation

This commit is contained in:
Peter Steinberger
2026-02-19 15:18:00 +01:00
parent baa335f258
commit 775816035e
15 changed files with 498 additions and 22 deletions

View File

@@ -353,6 +353,47 @@ describe("handleDiscordMessageAction", () => {
expect.any(Object),
);
});
it("uses trusted requesterSenderId for moderation and ignores params senderUserId", async () => {
await handleDiscordMessageAction({
action: "timeout",
params: {
guildId: "guild-1",
userId: "user-2",
durationMin: 5,
senderUserId: "spoofed-admin-id",
},
cfg: {} as OpenClawConfig,
requesterSenderId: "trusted-sender-id",
toolContext: { currentChannelProvider: "discord" },
});
expect(handleDiscordAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "timeout",
guildId: "guild-1",
userId: "user-2",
durationMinutes: 5,
senderUserId: "trusted-sender-id",
}),
expect.any(Object),
);
});
it("rejects moderation when trusted sender id is missing in Discord tool context", async () => {
await expect(
handleDiscordMessageAction({
action: "kick",
params: {
guildId: "guild-1",
userId: "user-2",
},
cfg: {} as OpenClawConfig,
toolContext: { currentChannelProvider: "discord" },
}),
).rejects.toThrow("Sender user ID required for Discord moderation actions.");
expect(handleDiscordAction).not.toHaveBeenCalled();
});
});
describe("telegramMessageActions", () => {

View File

@@ -1,13 +1,16 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ChannelMessageActionContext } from "../../types.js";
import {
readNumberParam,
readStringArrayParam,
readStringParam,
} from "../../../../agents/tools/common.js";
import { handleDiscordAction } from "../../../../agents/tools/discord-actions.js";
import type { ChannelMessageActionContext } from "../../types.js";
type Ctx = Pick<ChannelMessageActionContext, "action" | "params" | "cfg" | "accountId">;
type Ctx = Pick<
ChannelMessageActionContext,
"action" | "params" | "cfg" | "accountId" | "requesterSenderId" | "toolContext"
>;
export async function tryHandleDiscordMessageActionGuildAdmin(params: {
ctx: Ctx;
@@ -355,6 +358,11 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const deleteMessageDays = readNumberParam(actionParams, "deleteDays", {
integer: true,
});
const senderUserId = ctx.requesterSenderId?.trim() || undefined;
// In channel/tool flows, require trusted sender identity for moderation authorization.
if (ctx.toolContext?.currentChannelProvider === "discord" && !senderUserId) {
throw new Error("Sender user ID required for Discord moderation actions.");
}
const discordAction = action;
return await handleDiscordAction(
{
@@ -366,6 +374,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
until,
reason,
deleteMessageDays,
senderUserId,
},
cfg,
);

View File

@@ -1,4 +1,5 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ChannelMessageActionContext } from "../../types.js";
import {
readNumberParam,
readStringArrayParam,
@@ -6,7 +7,6 @@ import {
} from "../../../../agents/tools/common.js";
import { handleDiscordAction } from "../../../../agents/tools/discord-actions.js";
import { resolveDiscordChannelId } from "../../../../discord/targets.js";
import type { ChannelMessageActionContext } from "../../types.js";
import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js";
const providerId = "discord";
@@ -22,7 +22,10 @@ function readParentIdParam(params: Record<string, unknown>): string | null | und
}
export async function handleDiscordMessageAction(
ctx: Pick<ChannelMessageActionContext, "action" | "params" | "cfg" | "accountId">,
ctx: Pick<
ChannelMessageActionContext,
"action" | "params" | "cfg" | "accountId" | "requesterSenderId" | "toolContext"
>,
): Promise<AgentToolResult<unknown>> {
const { action, params, cfg } = ctx;
const accountId = ctx.accountId ?? readStringParam(params, "accountId");

View File

@@ -304,6 +304,11 @@ export type ChannelMessageActionContext = {
cfg: OpenClawConfig;
params: Record<string, unknown>;
accountId?: string | null;
/**
* Trusted sender id from inbound context. This is server-injected and must
* never be sourced from tool/model-controlled params.
*/
requesterSenderId?: string | null;
gateway?: {
url?: string;
token?: string;