From d4e5d08515d58a9605e1e80475b2238115bcacbd Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:49:31 +0100 Subject: [PATCH 1/3] fix(bot): enforce role lists in DMs, other guilds and autocomplete checkRolePermission() returned true whenever there was no guild member, which is the case for interactions in a DM with the bot. Since slash commands are registered globally and available in DMs, a user missing from ROLE_ALLOWLIST or listed in ROLE_BLOCKLIST could still use /request and /search by messaging the bot directly. The check also ignored which guild an interaction came from, so a blocklisted user could use the bot in any other server it is in, where the blocked role doesn't exist. When a role list is set, interactions are now only allowed from members of the configured GUILD_ID (denied and logged if GUILD_ID is empty). Autocomplete is gated as well (empty suggestions instead of TMDB/Seerr lookups), and two select menu prefixes that no handler uses are no longer exempt from the check. Without role lists, nothing changes. --- bot/botUtils.js | 14 +++++++++++--- bot/interactions.js | 12 ++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/bot/botUtils.js b/bot/botUtils.js index c557aaf..9bf680a 100644 --- a/bot/botUtils.js +++ b/bot/botUtils.js @@ -144,9 +144,7 @@ export function parseQualityAndServerOptions(options, mediaType, isAnime = false return { profileId, serverId }; } -export function checkRolePermission(member) { - if (!member || !member.roles) return true; - +export function checkRolePermission(member, guildId) { let allowlist = []; let blocklist = []; try { @@ -160,6 +158,16 @@ export function checkRolePermission(member) { logger.error(`Invalid JSON in ROLE_BLOCKLIST — blocklist disabled, no users blocked: ${e.message}`); } + if (allowlist.length === 0 && blocklist.length === 0) return true; + + // Roles are only meaningful in the configured guild; DMs and other guilds are denied + if (!member?.roles?.cache) return false; + if (!process.env.GUILD_ID) { + logger.warn("Role lists are set but GUILD_ID is empty — denying interaction"); + return false; + } + if (guildId !== process.env.GUILD_ID) return false; + const userRoles = member.roles.cache.map((r) => r.id); if (allowlist.length > 0 && !userRoles.some((r) => allowlist.includes(r))) { diff --git a/bot/interactions.js b/bot/interactions.js index b72523d..50c8b14 100644 --- a/bot/interactions.js +++ b/bot/interactions.js @@ -317,16 +317,16 @@ async function handleSearchOrRequest( export function registerInteractions(client) { client.on("interactionCreate", async (interaction) => { try { - // Check role permissions for all commands, buttons, and select menus + // Check role permissions for all commands, buttons, select menus, and autocomplete if ( interaction.isCommand() || interaction.isButton() || - (interaction.isStringSelectMenu() && - !interaction.customId.startsWith("request_seasons|") && - !interaction.customId.startsWith("request_with_tags|")) + interaction.isStringSelectMenu() || + interaction.isAutocomplete() ) { - if (!checkRolePermission(interaction.member)) { - return interaction.reply({ + if (!checkRolePermission(interaction.member, interaction.guildId)) { + if (interaction.isAutocomplete()) return await interaction.respond([]); + return await interaction.reply({ content: "❌ You don't have permission to use this command.", flags: 64, }); From d79f042cfa628692873c964661991e027fec6518 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:49:31 +0100 Subject: [PATCH 2/3] chore: reword local tooling comment in .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2b63492..b2109b0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,6 @@ config/dedup-*.json .worktrees/ PRODUCT.md -# Local skills (Claude Code / agent tooling) +# Local editor tooling .agents/ skills-lock.json From 0ce0a8edcc27f1c4b1bc9828b71c6aded7871959 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:52:31 +0100 Subject: [PATCH 3/3] fix(bot): fail closed on invalid role lists and localize the deny reply An unparseable ROLE_ALLOWLIST or ROLE_BLOCKLIST was treated as an empty list, which silently turned access control off for everyone. Values that parse but aren't an array of role ID strings behaved inconsistently between guild members and DMs. Both cases now deny role-restricted interactions and log the reason. The deny reply now comes from locales/ and tells DM users to use the command in the server instead of claiming they lack permission. Denials are logged with the user ID and whether it was a DM or which guild. --- bot/botUtils.js | 49 ++++++++++++++++++++++++++++++++----------- bot/interactions.js | 9 +++++++- locales/de.json | 4 ++++ locales/en.json | 4 ++++ locales/template.json | 4 ++++ 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/bot/botUtils.js b/bot/botUtils.js index 9bf680a..a28a3c6 100644 --- a/bot/botUtils.js +++ b/bot/botUtils.js @@ -144,28 +144,53 @@ export function parseQualityAndServerOptions(options, mediaType, isAnime = false return { profileId, serverId }; } -export function checkRolePermission(member, guildId) { - let allowlist = []; - let blocklist = []; - try { - allowlist = process.env.ROLE_ALLOWLIST ? JSON.parse(process.env.ROLE_ALLOWLIST) : []; - } catch (e) { - logger.error(`Invalid JSON in ROLE_ALLOWLIST — allowlist disabled, all users permitted: ${e.message}`); +// Config problems are logged when they appear, not on every interaction +const activeRoleConfigErrors = new Map(); +function reportRoleConfigError(name, value, message) { + if (activeRoleConfigErrors.get(name) === value) return; + activeRoleConfigErrors.set(name, value); + logger.error(message); +} + +// Returns the parsed role ID list, or null if the value is set but invalid +function parseRoleList(name) { + const raw = process.env[name]; + if (!raw) { + activeRoleConfigErrors.delete(name); + return []; } + let parsed; try { - blocklist = process.env.ROLE_BLOCKLIST ? JSON.parse(process.env.ROLE_BLOCKLIST) : []; - } catch (e) { - logger.error(`Invalid JSON in ROLE_BLOCKLIST — blocklist disabled, no users blocked: ${e.message}`); + parsed = JSON.parse(raw); + } catch { + reportRoleConfigError(name, raw, `Invalid JSON in ${name}: denying role-restricted interactions`); + return null; } + if (Array.isArray(parsed) && parsed.every((id) => typeof id === "string")) { + activeRoleConfigErrors.delete(name); + return parsed; + } + reportRoleConfigError(name, raw, `${name} is not an array of role IDs: denying role-restricted interactions`); + return null; +} - if (allowlist.length === 0 && blocklist.length === 0) return true; +export function checkRolePermission(member, guildId) { + const allowlist = parseRoleList("ROLE_ALLOWLIST"); + const blocklist = parseRoleList("ROLE_BLOCKLIST"); + if (allowlist === null || blocklist === null) return false; + + if (allowlist.length === 0 && blocklist.length === 0) { + activeRoleConfigErrors.delete("GUILD_ID"); + return true; + } // Roles are only meaningful in the configured guild; DMs and other guilds are denied if (!member?.roles?.cache) return false; if (!process.env.GUILD_ID) { - logger.warn("Role lists are set but GUILD_ID is empty — denying interaction"); + reportRoleConfigError("GUILD_ID", "", "Role lists are set but GUILD_ID is empty: denying role-restricted interactions"); return false; } + activeRoleConfigErrors.delete("GUILD_ID"); if (guildId !== process.env.GUILD_ID) return false; const userRoles = member.roles.cache.map((r) => r.id); diff --git a/bot/interactions.js b/bot/interactions.js index 50c8b14..3318327 100644 --- a/bot/interactions.js +++ b/bot/interactions.js @@ -326,8 +326,15 @@ export function registerInteractions(client) { ) { if (!checkRolePermission(interaction.member, interaction.guildId)) { if (interaction.isAutocomplete()) return await interaction.respond([]); + logger.info( + `[PERMISSION] Denied interaction from user ${interaction.user.id} (${interaction.guildId ? `guild ${interaction.guildId}` : "DM"})` + ); return await interaction.reply({ - content: "❌ You don't have permission to use this command.", + content: t( + interaction.guildId + ? "permission_errors.denied" + : "permission_errors.dm_denied" + ), flags: 64, }); } diff --git a/locales/de.json b/locales/de.json index 53fc41e..9f8844e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -492,5 +492,9 @@ "generic": "⚠️ Seerr-Fehler: {msg}", "unreachable": "⚠️ Seerr konnte nicht erreicht werden. Prüfe, ob die Seerr-URL korrekt und erreichbar ist.", "unknown": "⚠️ Ein Fehler ist aufgetreten: {msg}" + }, + "permission_errors": { + "denied": "❌ Du hast keine Berechtigung, diesen Befehl zu verwenden.", + "dm_denied": "❌ Dieser Bot ist auf Server-Rollen beschränkt. Bitte nutze den Befehl im Server." } } diff --git a/locales/en.json b/locales/en.json index e5efbdc..a1404e6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -425,5 +425,9 @@ "generic": "⚠️ Seerr error: {msg}", "unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.", "unknown": "⚠️ An error occurred: {msg}" + }, + "permission_errors": { + "denied": "❌ You don't have permission to use this command.", + "dm_denied": "❌ This bot is restricted to server roles. Please use this command in the server." } } diff --git a/locales/template.json b/locales/template.json index c794ed7..1f01875 100644 --- a/locales/template.json +++ b/locales/template.json @@ -425,5 +425,9 @@ "generic": "⚠️ Seerr error: {msg}", "unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.", "unknown": "⚠️ An error occurred: {msg}" + }, + "permission_errors": { + "denied": "", + "dm_denied": "" } }