alvin-bot 4.20.1 → 4.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.20.2] — 2026-05-04
6
+
7
+ ### šŸ›”ļø Security: Web UI loopback by default + Slack caller allowlist
8
+
9
+ Two real attack surfaces closed.
10
+
11
+ **Web UI binds to 127.0.0.1 by default.** Previous versions called `server.listen(port)` with no host argument, which Node interprets as "listen on all interfaces". Combined with an empty `WEB_PASSWORD` (which the login route silently treats as "anyone can log in"), this meant any device on the same LAN could log into the bot's Web UI and reach every authenticated endpoint — user list, memory contents, model switch, the WebSocket chat, etc. New default: bind to `127.0.0.1`. To restore LAN access, set `WEB_HOST=0.0.0.0` explicitly in `.env`. If both `WEB_HOST=0.0.0.0` and an empty `WEB_PASSWORD` are present, the bot logs a loud warning on startup.
12
+
13
+ **Slack caller allowlist.** New `SLACK_ALLOWED_USERS` env var: comma-separated list of Slack user IDs allowed to talk to the bot (DMs, @mentions, slash commands). Empty list keeps the legacy behaviour — any workspace member can interact, which is safe iff the workspace is private to the operator. To find your Slack user ID: open your profile in Slack → "..." → "Copy member ID", or just message the bot once and read the line `[slack] caller discovered: user=U… — to lock the bot to specific users, add to .env: SLACK_ALLOWED_USERS=U…` from the logs (we log each unique caller once when the allowlist is empty).
14
+
15
+ **`alvin-bot doctor` now reports both.** New `Web UI:` and `Slack:` sections flag insecure combos and show whether an allowlist is active.
16
+
17
+ No schema or behaviour changes for users who already have `WEB_PASSWORD` set or only use the bot via Telegram. Telegram allowlist (`ALLOWED_USERS`) is unchanged.
18
+
5
19
  ## [4.20.1] — 2026-05-03
6
20
 
7
21
  ### šŸ›”ļø Hardening for the v4.20.0 SQLite migration
package/bin/cli.js CHANGED
@@ -1361,6 +1361,37 @@ async function doctor() {
1361
1361
  console.log(` āŒ ALLOWED_USERS not set (nobody can message the bot)`);
1362
1362
  }
1363
1363
 
1364
+ // ── Web UI security ──
1365
+ console.log("\n Web UI:");
1366
+ const webHost = getEnv("WEB_HOST") || "127.0.0.1";
1367
+ const webPw = getEnv("WEB_PASSWORD");
1368
+ if (webHost === "127.0.0.1" || webHost === "::1") {
1369
+ console.log(` āœ… WEB_HOST=${webHost} — loopback only (LAN unreachable)`);
1370
+ } else if (webHost === "0.0.0.0" || webHost === "*") {
1371
+ if (webPw) {
1372
+ console.log(` āœ… WEB_HOST=${webHost} (LAN-reachable) + WEB_PASSWORD set`);
1373
+ } else {
1374
+ console.log(` āŒ WEB_HOST=${webHost} (LAN-reachable) WITHOUT WEB_PASSWORD — anyone on LAN can log in`);
1375
+ console.log(` Fix: set WEB_PASSWORD in .env, or set WEB_HOST=127.0.0.1`);
1376
+ }
1377
+ } else {
1378
+ console.log(` ā„¹ļø WEB_HOST=${webHost}${webPw ? " + WEB_PASSWORD set" : " — WEB_PASSWORD empty"}`);
1379
+ }
1380
+
1381
+ // ── Slack caller allowlist ──
1382
+ if (getEnv("SLACK_BOT_TOKEN")) {
1383
+ console.log("\n Slack:");
1384
+ const slackAllow = getEnv("SLACK_ALLOWED_USERS");
1385
+ if (slackAllow) {
1386
+ const ids = slackAllow.split(",").map(s => s.trim()).filter(Boolean);
1387
+ console.log(` āœ… SLACK_ALLOWED_USERS: ${ids.length} user${ids.length === 1 ? "" : "s"} (caller allowlist active)`);
1388
+ } else {
1389
+ console.log(` āš ļø SLACK_ALLOWED_USERS not set — any workspace member can talk to the bot`);
1390
+ console.log(` Safe iff the Slack workspace is private to you. Otherwise add e.g.:`);
1391
+ console.log(` SLACK_ALLOWED_USERS=U0ABC123,U0DEF456`);
1392
+ }
1393
+ }
1394
+
1364
1395
  // ── Memory (semantic search backend) ──
1365
1396
  console.log("\n Memory:");
1366
1397
  const embJson = resolve(DATA_DIR, "memory", ".embeddings.json");
package/dist/config.js CHANGED
@@ -63,6 +63,20 @@ export const config = {
63
63
  sessionMode: (process.env.SESSION_MODE || "per-user"),
64
64
  webhookEnabled: process.env.WEBHOOK_ENABLED === "true",
65
65
  webhookToken: process.env.WEBHOOK_TOKEN || "",
66
+ // Web UI bind host. Default is 127.0.0.1 (loopback only) — set to "0.0.0.0"
67
+ // explicitly if you want LAN/external access. Combined with WEB_PASSWORD
68
+ // this is the safe default since v4.20.2; previous versions defaulted to
69
+ // listening on all interfaces with no auth required when WEB_PASSWORD was
70
+ // empty.
71
+ webHost: process.env.WEB_HOST || "127.0.0.1",
72
+ // Slack caller allowlist. Comma-separated Slack user IDs (e.g. "U0ABC123,U0DEF456").
73
+ // When non-empty, only these users can talk to the bot in Slack DMs and via @mention.
74
+ // When empty, the bot accepts any Slack workspace member (legacy behavior; safe iff
75
+ // the workspace is private to you).
76
+ slackAllowedUsers: (process.env.SLACK_ALLOWED_USERS || "")
77
+ .split(",")
78
+ .map(s => s.trim())
79
+ .filter(Boolean),
66
80
  // Browser
67
81
  cdpUrl: process.env.CDP_URL || "",
68
82
  browseServerPort: Number(process.env.BROWSE_SERVER_PORT) || 3800,
@@ -18,6 +18,32 @@
18
18
  */
19
19
  import fs from "fs";
20
20
  import { parseSlackSlashCommand } from "./slack-slash-parser.js";
21
+ import { config } from "../config.js";
22
+ /**
23
+ * v4.20.2 — Slack caller allowlist. When SLACK_ALLOWED_USERS is set in the
24
+ * environment (comma-separated Slack user IDs), only those users get past
25
+ * this gate. When the list is empty, fall back to legacy behaviour: any
26
+ * member of the workspace can talk to the bot. The empty-list case is safe
27
+ * iff the workspace is private to the operator.
28
+ *
29
+ * Slack user IDs are workspace-scoped (e.g. "U0ABC123"); rotate the list if
30
+ * you migrate workspaces.
31
+ */
32
+ function isSlackUserAllowed(userId) {
33
+ if (config.slackAllowedUsers.length === 0) {
34
+ // No allowlist set — log each unique caller once so the operator can
35
+ // copy a known ID into SLACK_ALLOWED_USERS and lock the bot down.
36
+ if (userId && !discoveredCallers.has(userId)) {
37
+ discoveredCallers.add(userId);
38
+ console.warn(`[slack] caller discovered: user=${userId} — to lock the bot to specific users, ` +
39
+ `add to .env: SLACK_ALLOWED_USERS=${userId}` +
40
+ (discoveredCallers.size > 1 ? ` (or comma-separate multiple)` : ""));
41
+ }
42
+ return true;
43
+ }
44
+ return config.slackAllowedUsers.includes(userId);
45
+ }
46
+ const discoveredCallers = new Set();
21
47
  let _slackState = {
22
48
  status: "disconnected",
23
49
  botName: null,
@@ -148,6 +174,13 @@ export class SlackAdapter {
148
174
  const userId = message.user || "";
149
175
  const channelId = message.channel || "";
150
176
  const messageId = message.ts || "";
177
+ // v4.20.2 — caller allowlist. If SLACK_ALLOWED_USERS is set, silently
178
+ // ignore anyone not on the list. Empty list = legacy behaviour
179
+ // (any workspace member can talk to the bot — safe iff the workspace
180
+ // is private to the operator).
181
+ if (!isSlackUserAllowed(userId)) {
182
+ return;
183
+ }
151
184
  // Determine channel type
152
185
  // DMs (im) have channel_type "im", group DMs are "mpim", channels are "channel"/"group"
153
186
  const channelType = message.channel_type || "";
@@ -222,6 +255,10 @@ export class SlackAdapter {
222
255
  const channelId = command.channel_id || "";
223
256
  const userId = command.user_id || "";
224
257
  const userName = command.user_name || userId;
258
+ // v4.20.2 — caller allowlist for slash commands.
259
+ if (!isSlackUserAllowed(userId)) {
260
+ return;
261
+ }
225
262
  const incoming = {
226
263
  platform: "slack",
227
264
  messageId: `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -247,6 +284,10 @@ export class SlackAdapter {
247
284
  const userId = event.user || "";
248
285
  const channelId = event.channel || "";
249
286
  const messageId = event.ts || "";
287
+ // v4.20.2 — same caller allowlist as DMs.
288
+ if (!isSlackUserAllowed(userId)) {
289
+ return;
290
+ }
250
291
  // Strip the @mention from text
251
292
  text = text.replace(new RegExp(`<@${this.botUserId}>`, "g"), "").trim();
252
293
  if (!text)
@@ -1566,7 +1566,11 @@ function scheduleBindAttempt(port, attempt) {
1566
1566
  // invalid backlog, kernel hiccup) can throw synchronously. Catch here
1567
1567
  // so the main routine never crashes during web-UI bind.
1568
1568
  try {
1569
- server.listen(port, () => {
1569
+ // v4.20.2 — bind to config.webHost (default 127.0.0.1) so the Web UI
1570
+ // is loopback-only unless the operator opts in by setting WEB_HOST=0.0.0.0.
1571
+ // Empty/"*" maps to all interfaces.
1572
+ const bindHost = (config.webHost === "*" || config.webHost === "") ? undefined : config.webHost;
1573
+ server.listen(port, bindHost, () => {
1570
1574
  if (handled)
1571
1575
  return; // Should be impossible; paranoia.
1572
1576
  handled = true;
@@ -1587,10 +1591,17 @@ function scheduleBindAttempt(port, attempt) {
1587
1591
  server.on("error", (err) => {
1588
1592
  console.warn(`[web] post-bind server error (ignored): ${err.message}`);
1589
1593
  });
1590
- console.log(`🌐 Web UI: http://localhost:${actualWebPort}`);
1594
+ const bindLabel = bindHost && bindHost !== "127.0.0.1" && bindHost !== "::1"
1595
+ ? `http://${bindHost}:${actualWebPort}` + (bindHost === "0.0.0.0" ? " (LAN-reachable)" : "")
1596
+ : `http://localhost:${actualWebPort}`;
1597
+ console.log(`🌐 Web UI: ${bindLabel}`);
1591
1598
  if (actualWebPort !== originalPort) {
1592
1599
  console.log(` (Port ${originalPort} was busy, using ${actualWebPort} instead)`);
1593
1600
  }
1601
+ if (bindHost === "0.0.0.0" && !process.env.WEB_PASSWORD) {
1602
+ console.warn("āš ļø Web UI is bound to 0.0.0.0 but WEB_PASSWORD is empty — anyone on the LAN can log in. " +
1603
+ "Set WEB_PASSWORD in ~/.alvin-bot/.env or set WEB_HOST=127.0.0.1.");
1604
+ }
1594
1605
  });
1595
1606
  }
1596
1607
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "4.20.1",
3
+ "version": "4.20.2",
4
4
  "description": "Alvin Bot — Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",