Empty Means Everyone: holaOS’s Messaging Gateway Defaults to Allow-All, and Usernames Are Recyclable

NVD published two CVEs against holaboss-ai/holaOS one second apart on 14 October 2026 (19:17:12Z), both sourced to disclosure@vulncheck.com and both reported by hieuPenguinnn with a full technical writeup. holaOS is no hobby project — 11,449 stars, 742 forks, an “agentic workspace” with 100+ integrations, MCP, browser, and chat tooling. Its runtime channel-gateway connects the operator’s local agent to messaging platforms: a per-platform connector normalises an inbound message and enqueues its text as a turn for the workspace owner’s agent, which runs with the workspace’s full tool set. The only authorization control on who may drive that agent is a per-connection sender allowlist, allowFrom — and both CVEs are that control failing in the two ways allowlists fail.

  • CVE-2026-110769 (CVSS 4.0 8.3, 3.1 8.6, CWE-1188 insecure default, through 2026.822.1). The connector’s sender check accepts all senders when the default allowFrom list is empty. Anyone who can message the operator’s Telegram, Slack, Discord, or other bot gets ChannelIngress.handle to pass their turns to the workspace agent, which runs configured tools on the operator’s machine.
  • CVE-2026-110768 (CVSS 4.0 6.3, 3.1 4.8, CWE-290 authentication bypass by spoofing, through 2026.822.1). When an operator does configure the allowlist, the Telegram connector matches the inbound sender against the sender’s current @username. Telegram usernames are mutable and recyclable: listing a principal by handle authorises whoever currently holds that handle, so an attacker who registers a released username in the list inherits its access.

Still present on main at time of writing

This is not a fixed-and-recorded batch. No fixed version is named in either record (“through 2026.822.1” is the current state, not a range with a patch at the end), the repository carries no semantic release tags at all — the tag list contains only latest — and the defects are visible in the current sources. We read runtime/channel-gateway/src/connectors/telegram.ts first-hand on main:

#isAllowed(senderId: string, senderUsername: string): boolean {
  if (this.#allowFrom.size === 0) return true;
  const id = senderId.trim().toLowerCase();
  const username = senderUsername.trim().replace(/^@/, "").toLowerCase();
  return (id !== "" && this.#allowFrom.has(id)) || (username !== "" && this.#allowFrom.has(username));
}

Both findings are in those five lines. Line one is the insecure default: an empty set authorises everyone, and the constructor builds that set from config.allowFrom ?? [], while the researcher’s writeup documents that the environment quick-connect path leaves the list empty by default and the configuration contract itself states “empty = allow all”. The last line is the mutable identity: a username entry in the set is compared against the live @username from the inbound update (extracted at the call site alongside the numeric from.id), with no stability check and no preference for the immutable numeric id. The most recent channel-gateway commit (18 August 2026, long-running-turn reply handling) touches neither path. There is no ingress-side authorization either — the writeup’s ingress.ts excerpt shows ChannelIngress.handle resolving the workspace and firing the message with no caller-identity check, by design delegating the entire trust decision to the connector’s allowlist. The one control is these five lines, and these five lines fail open twice.

Why the scores split — and why the lower one still matters

The 8.6 versus 4.8 split is a worked example of CVSS measuring prerequisites rather than consequences. The allow-all default needs nothing the attacker doesn’t already have — the ability to message a bot — so it scores high. The username-recycling path needs the victim’s handle to lapse and the attacker to claim it (AT:P / AC:H doing the work), so it scores medium. But the consequence on success is identical: arbitrary task submission to an agent holding the operator’s tools, on the operator’s machine. Defenders should read the pair as one exposure with two preconditions, not as one serious bug and one minor one. The researcher’s severity badge (“high-critical”) agrees with that reading over the arithmetic.

The agent-security framing is what elevates this above an ordinary chatbot-admin flaw. The trust boundary crossed is remote unauthenticated messaging-platform user to local agent task execution — not reading messages, not sending spam, but submitting turns to a workspace agent with runtime tools, browser tools, MCP tools, and whatever coding-agent harness is configured. Every message-platform bot wired to an agentic workspace is this shape: the connector is the authentication boundary, and here the boundary is a list that defaults to absent and matches on a recyclable string. Compare the ClaudeHookBridge guardrail bypasses covered earlier this week: there the control inspected the wrong field; here the control inspects the wrong identity — or nobody at all.

What to do

  • Set a non-empty allowFrom on every channel-gateway connection, now. There is no patched version to upgrade to; configuration is the only control. An empty list is documented to mean allow-all, so audit every connection — Telegram, Slack, Discord, and the rest — for the default.
  • Allowlist by numeric sender id, never by username. The check accepts both forms, so an id entry works today with no code change. Remove every @username entry: Telegram handles are mutable and recyclable, and whoever holds the handle inherits the access. Re-audit periodically — ids are stable, but the list itself drifts.
  • Treat the bot token’s chat surface as an unauthenticated agent-input channel until patched. Restrict who can discover and message the bot (privacy mode, no public links), and scope the workspace agent’s tool set to what a stranger’s prompt should be allowed to reach. A coding-agent harness behind an allow-all messaging connector is remote code execution with extra steps.
  • Watch the repository for a fix that changes the default. The correct upstream fix is fail-closed (empty list denies) plus id-preferring matching. Until that ships, any fresh deployment following the quick-connect path is exposed from first boot.

Verification note. Both CVE identifiers, NVD publication timestamps (14 October 2026, 19:17:12Z), CVSS 4.0 and 3.1 scores and vectors, CWE assignments, affected-version strings, vulnStatus Deferred, and the VulnCheck source identifier were read first-hand from the NVD 2.0 API. Advisory dates, affected versions, reporter credit, and reference links were read first-hand from both VulnCheck advisory pages; the root-cause analysis, pinned commit (4684714ee133794cdbb86630e42b7d93447fb2e2, 21 August 2026), ingress-path description, quick-connect default claim, and tool-set scope from the reporter’s HackMD writeup read first-hand. The #isAllowed function, its empty-set early return, the username normalisation, the call-site sender extraction, and the allowFrom ?? [] construction were read first-hand in current telegram.ts on main; the latest channel-gateway commit summary and date plus the tag list (only latest) from the GitHub API; repository stars, forks, and description from the GitHub Repository API. Brave Search corroboration found only CVE-mirror pages and no independent analysis. Nothing was installed, no bot was messaged, no username was registered, and no third-party request was sent; remediation guidance is our editorial analysis. We have no evidence of exploitation and imply none.

Sources: