fix(twitch): enforce allowFrom allowlist
This commit is contained in:
parent
aa2eb48b9c
commit
8c7901c984
4 changed files with 24 additions and 16 deletions
|
|
@ -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.
|
- 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: block LD_/DYLD_ env overrides for host exec. (#4896) Thanks @HassanFleyah.
|
||||||
- Security: harden web tool content wrapping + file parsing safeguards. (#4058) Thanks @VACInc.
|
- 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
|
## 2026.1.30
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,13 @@ If both env and config are set, config takes precedence.
|
||||||
channels: {
|
channels: {
|
||||||
twitch: {
|
twitch: {
|
||||||
allowFrom: ["123456789"], // (recommended) Your Twitch user ID only
|
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"`.
|
**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`.
|
||||||
|
|
||||||
**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent.
|
**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
|
```json5
|
||||||
{
|
{
|
||||||
|
|
@ -218,7 +220,6 @@ Users in `allowFrom` bypass role checks:
|
||||||
twitch: {
|
twitch: {
|
||||||
accounts: {
|
accounts: {
|
||||||
default: {
|
default: {
|
||||||
allowFrom: ["123456789"],
|
|
||||||
allowedRoles: ["moderator"],
|
allowedRoles: ["moderator"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -256,7 +257,8 @@ openclaw channels status --probe
|
||||||
|
|
||||||
### Bot doesn't respond to messages
|
### 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`.
|
**Check the bot is in the channel:** The bot must join the channel specified in `channel`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ describe("checkTwitchAccessControl", () => {
|
||||||
expect(result.matchSource).toBe("allowlist");
|
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 = {
|
const account: TwitchAccountConfig = {
|
||||||
...mockAccount,
|
...mockAccount,
|
||||||
allowFrom: ["789012"],
|
allowFrom: ["789012"],
|
||||||
|
|
@ -150,8 +150,8 @@ describe("checkTwitchAccessControl", () => {
|
||||||
account,
|
account,
|
||||||
botUsername: "testbot",
|
botUsername: "testbot",
|
||||||
});
|
});
|
||||||
// Falls through to final fallback since allowedRoles is not set
|
expect(result.allowed).toBe(false);
|
||||||
expect(result.allowed).toBe(true);
|
expect(result.reason).toContain("allowFrom");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks messages without userId", () => {
|
it("blocks messages without userId", () => {
|
||||||
|
|
@ -194,7 +194,7 @@ describe("checkTwitchAccessControl", () => {
|
||||||
expect(result.allowed).toBe(true);
|
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 = {
|
const account: TwitchAccountConfig = {
|
||||||
...mockAccount,
|
...mockAccount,
|
||||||
allowFrom: ["789012"],
|
allowFrom: ["789012"],
|
||||||
|
|
@ -212,11 +212,11 @@ describe("checkTwitchAccessControl", () => {
|
||||||
account,
|
account,
|
||||||
botUsername: "testbot",
|
botUsername: "testbot",
|
||||||
});
|
});
|
||||||
expect(result.allowed).toBe(true);
|
expect(result.allowed).toBe(false);
|
||||||
expect(result.matchSource).toBe("role");
|
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 = {
|
const account: TwitchAccountConfig = {
|
||||||
...mockAccount,
|
...mockAccount,
|
||||||
allowFrom: ["789012"],
|
allowFrom: ["789012"],
|
||||||
|
|
@ -235,7 +235,7 @@ describe("checkTwitchAccessControl", () => {
|
||||||
botUsername: "testbot",
|
botUsername: "testbot",
|
||||||
});
|
});
|
||||||
expect(result.allowed).toBe(false);
|
expect(result.allowed).toBe(false);
|
||||||
expect(result.reason).toContain("does not have any of the required roles");
|
expect(result.reason).toContain("allowFrom");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ export type TwitchAccessControlResult = {
|
||||||
* Priority order:
|
* Priority order:
|
||||||
* 1. If `requireMention` is true, message must mention the bot
|
* 1. If `requireMention` is true, message must mention the bot
|
||||||
* 2. If `allowFrom` is set, sender must be in the allowlist (by user ID)
|
* 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`,
|
* Note: `allowFrom` is a hard allowlist. When set, only those user IDs are allowed.
|
||||||
* they bypass role checks. This is useful for allowing specific users regardless of role.
|
* Use `allowedRoles` as an alternative when you don't want to maintain an allowlist.
|
||||||
*
|
*
|
||||||
* Available roles:
|
* Available roles:
|
||||||
* - "moderator": Moderators
|
* - "moderator": Moderators
|
||||||
|
|
@ -66,6 +66,11 @@ export function checkTwitchAccessControl(params: {
|
||||||
matchSource: "allowlist",
|
matchSource: "allowlist",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: "sender is not in allowFrom allowlist",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.allowedRoles && account.allowedRoles.length > 0) {
|
if (account.allowedRoles && account.allowedRoles.length > 0) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue