From 8c7901c984866a776eb59662dc9d8b028de4f0d0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Feb 2026 00:16:22 +0000 Subject: [PATCH] fix(twitch): enforce allowFrom allowlist --- CHANGELOG.md | 1 + docs/channels/twitch.md | 12 +++++++----- extensions/twitch/src/access-control.test.ts | 16 ++++++++-------- extensions/twitch/src/access-control.ts | 11 ++++++++--- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07198eb22..b12b4da69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - fix(lobster): block arbitrary exec via lobsterPath/cwd injection (GHSA-4mhr-g7xj-cg8j). (#5335) Thanks @vignesh07. - Security: block LD_/DYLD_ env overrides for host exec. (#4896) Thanks @HassanFleyah. - Security: harden web tool content wrapping + file parsing safeguards. (#4058) Thanks @VACInc. +- Security: enforce Twitch `allowFrom` allowlist gating (deny non-allowlisted senders). Thanks @MegaManSec. ## 2026.1.30 diff --git a/docs/channels/twitch.md b/docs/channels/twitch.md index c6258d688..7901c0427 100644 --- a/docs/channels/twitch.md +++ b/docs/channels/twitch.md @@ -112,12 +112,13 @@ If both env and config are set, config takes precedence. channels: { twitch: { allowFrom: ["123456789"], // (recommended) Your Twitch user ID only - allowedRoles: ["moderator"], // Or restrict to roles }, }, } ``` +Prefer `allowFrom` for a hard allowlist. Use `allowedRoles` instead if you want role-based access. + **Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`. **Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent. @@ -208,9 +209,10 @@ Example (one bot account in two channels): } ``` -### Combined allowlist + roles +### Role-based access (alternative) -Users in `allowFrom` bypass role checks: +`allowFrom` is a hard allowlist. When set, only those user IDs are allowed. +If you want role-based access, leave `allowFrom` unset and configure `allowedRoles` instead: ```json5 { @@ -218,7 +220,6 @@ Users in `allowFrom` bypass role checks: twitch: { accounts: { default: { - allowFrom: ["123456789"], allowedRoles: ["moderator"], }, }, @@ -256,7 +257,8 @@ openclaw channels status --probe ### Bot doesn't respond to messages -**Check access control:** Temporarily set `allowedRoles: ["all"]` to test. +**Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove +`allowFrom` and set `allowedRoles: ["all"]` to test. **Check the bot is in the channel:** The bot must join the channel specified in `channel`. diff --git a/extensions/twitch/src/access-control.test.ts b/extensions/twitch/src/access-control.test.ts index 94c7e5533..098745753 100644 --- a/extensions/twitch/src/access-control.test.ts +++ b/extensions/twitch/src/access-control.test.ts @@ -135,7 +135,7 @@ describe("checkTwitchAccessControl", () => { expect(result.matchSource).toBe("allowlist"); }); - it("allows users not in allowlist via fallback (open access)", () => { + it("blocks users not in allowlist when allowFrom is set", () => { const account: TwitchAccountConfig = { ...mockAccount, allowFrom: ["789012"], @@ -150,8 +150,8 @@ describe("checkTwitchAccessControl", () => { account, botUsername: "testbot", }); - // Falls through to final fallback since allowedRoles is not set - expect(result.allowed).toBe(true); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("allowFrom"); }); it("blocks messages without userId", () => { @@ -194,7 +194,7 @@ describe("checkTwitchAccessControl", () => { expect(result.allowed).toBe(true); }); - it("allows user with role even if not in allowlist", () => { + it("blocks user with role when not in allowlist", () => { const account: TwitchAccountConfig = { ...mockAccount, allowFrom: ["789012"], @@ -212,11 +212,11 @@ describe("checkTwitchAccessControl", () => { account, botUsername: "testbot", }); - expect(result.allowed).toBe(true); - expect(result.matchSource).toBe("role"); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("allowFrom"); }); - it("blocks user with neither allowlist nor role", () => { + it("blocks user not in allowlist even when roles configured", () => { const account: TwitchAccountConfig = { ...mockAccount, allowFrom: ["789012"], @@ -235,7 +235,7 @@ describe("checkTwitchAccessControl", () => { botUsername: "testbot", }); expect(result.allowed).toBe(false); - expect(result.reason).toContain("does not have any of the required roles"); + expect(result.reason).toContain("allowFrom"); }); }); diff --git a/extensions/twitch/src/access-control.ts b/extensions/twitch/src/access-control.ts index 51328b12e..18c1c7502 100644 --- a/extensions/twitch/src/access-control.ts +++ b/extensions/twitch/src/access-control.ts @@ -19,10 +19,10 @@ export type TwitchAccessControlResult = { * Priority order: * 1. If `requireMention` is true, message must mention the bot * 2. If `allowFrom` is set, sender must be in the allowlist (by user ID) - * 3. If `allowedRoles` is set, sender must have at least one of the specified roles + * 3. If `allowedRoles` is set (and `allowFrom` is not), sender must have at least one role * - * Note: You can combine `allowFrom` with `allowedRoles`. If a user is in `allowFrom`, - * they bypass role checks. This is useful for allowing specific users regardless of role. + * Note: `allowFrom` is a hard allowlist. When set, only those user IDs are allowed. + * Use `allowedRoles` as an alternative when you don't want to maintain an allowlist. * * Available roles: * - "moderator": Moderators @@ -66,6 +66,11 @@ export function checkTwitchAccessControl(params: { matchSource: "allowlist", }; } + + return { + allowed: false, + reason: "sender is not in allowFrom allowlist", + }; } if (account.allowedRoles && account.allowedRoles.length > 0) {