mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(security): default standalone servers to loopback bind (#13184)
* fix(security): default standalone servers to loopback bind (#4) Change canvas host and telegram webhook default bind from 0.0.0.0 (all interfaces) to 127.0.0.1 (loopback only) to prevent unintended network exposure when no explicit host is configured. * fix: restore telegram webhook host override while keeping loopback defaults (openclaw#13184) thanks @davidrudduck * style: format telegram docs after rebase (openclaw#13184) thanks @davidrudduck --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -161,6 +161,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CI: Implement pipeline and workflow order. Thanks @quotentiroler.
|
||||
- WhatsApp: preserve original filenames for inbound documents. (#12691) Thanks @akramcodez.
|
||||
- Telegram: harden quote parsing; preserve quote context; avoid QUOTE_TEXT_INVALID; avoid nested reply quote misclassification. (#12156) Thanks @rybnikov.
|
||||
- Security/Telegram: breaking default-behavior change — standalone canvas host + Telegram webhook listeners now bind loopback (`127.0.0.1`) instead of `0.0.0.0`; set `channels.telegram.webhookHost` when external ingress is required. (#13184) Thanks @davidrudduck.
|
||||
- Telegram: recover proactive sends when stale topic thread IDs are used by retrying without `message_thread_id`. (#11620)
|
||||
- Discord: auto-create forum/media thread posts on send, with chunked follow-up replies and media handling for forum sends. (#12380) Thanks @magendary, @thewilloftheshadow.
|
||||
- Discord: cap gateway reconnect attempts to avoid infinite retry loops. (#12230) Thanks @Yida-Dev.
|
||||
|
||||
@@ -20,7 +20,7 @@ title: grammY
|
||||
- **Proxy:** optional `channels.telegram.proxy` uses `undici.ProxyAgent` through grammY’s `client.baseFetch`.
|
||||
- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls).
|
||||
- **Sessions:** direct chats collapse into the agent main session (`agent:<agentId>:<mainKey>`); groups use `agent:<agentId>:telegram:group:<chatId>`; replies route back to the same channel.
|
||||
- **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`.
|
||||
- **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`, `channels.telegram.webhookHost`.
|
||||
- **Draft streaming:** optional `channels.telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+). This is separate from channel block streaming.
|
||||
- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
|
||||
|
||||
|
||||
@@ -595,10 +595,12 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- set `channels.telegram.webhookUrl`
|
||||
- set `channels.telegram.webhookSecret` (required when webhook URL is set)
|
||||
- optional `channels.telegram.webhookPath` (default `/telegram-webhook`)
|
||||
- optional `channels.telegram.webhookHost` (default `127.0.0.1`)
|
||||
|
||||
Default local listener for webhook mode binds to `0.0.0.0:8787`.
|
||||
Default local listener for webhook mode binds to `127.0.0.1:8787`.
|
||||
|
||||
If your public endpoint differs, place a reverse proxy in front and point `webhookUrl` at the public URL.
|
||||
Set `webhookHost` (for example `0.0.0.0`) when you intentionally need external ingress.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -673,6 +675,45 @@ More help: [Channel troubleshooting](/channels/troubleshooting).
|
||||
|
||||
Primary reference:
|
||||
|
||||
- `channels.telegram.enabled`: enable/disable channel startup.
|
||||
- `channels.telegram.botToken`: bot token (BotFather).
|
||||
- `channels.telegram.tokenFile`: read token from file path.
|
||||
- `channels.telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
|
||||
- `channels.telegram.allowFrom`: DM allowlist (ids/usernames). `open` requires `"*"`.
|
||||
- `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
|
||||
- `channels.telegram.groupAllowFrom`: group sender allowlist (ids/usernames).
|
||||
- `channels.telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
|
||||
- `channels.telegram.groups.<id>.groupPolicy`: per-group override for groupPolicy (`open | allowlist | disabled`).
|
||||
- `channels.telegram.groups.<id>.requireMention`: mention gating default.
|
||||
- `channels.telegram.groups.<id>.skills`: skill filter (omit = all skills, empty = none).
|
||||
- `channels.telegram.groups.<id>.allowFrom`: per-group sender allowlist override.
|
||||
- `channels.telegram.groups.<id>.systemPrompt`: extra system prompt for the group.
|
||||
- `channels.telegram.groups.<id>.enabled`: disable the group when `false`.
|
||||
- `channels.telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
|
||||
- `channels.telegram.groups.<id>.topics.<threadId>.groupPolicy`: per-topic override for groupPolicy (`open | allowlist | disabled`).
|
||||
- `channels.telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
|
||||
- `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist).
|
||||
- `channels.telegram.accounts.<account>.capabilities.inlineButtons`: per-account override.
|
||||
- `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
|
||||
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
|
||||
- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
|
||||
- `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
|
||||
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
|
||||
- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||
- `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
||||
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts.
|
||||
- `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP).
|
||||
- `channels.telegram.webhookUrl`: enable webhook mode (requires `channels.telegram.webhookSecret`).
|
||||
- `channels.telegram.webhookSecret`: webhook secret (required when webhookUrl is set).
|
||||
- `channels.telegram.webhookPath`: local webhook path (default `/telegram-webhook`).
|
||||
- `channels.telegram.webhookHost`: local webhook bind host (default `127.0.0.1`).
|
||||
- `channels.telegram.actions.reactions`: gate Telegram tool reactions.
|
||||
- `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
|
||||
- `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes.
|
||||
- `channels.telegram.actions.sticker`: gate Telegram sticker actions — send and search (default: false).
|
||||
- `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `own` when not set).
|
||||
- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when not set).
|
||||
|
||||
- [Configuration reference - Telegram](/gateway/configuration-reference#telegram)
|
||||
|
||||
Telegram-specific high-signal fields:
|
||||
@@ -684,7 +725,7 @@ Telegram-specific high-signal fields:
|
||||
- streaming: `streamMode`, `draftChunk`, `blockStreaming`
|
||||
- formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix`
|
||||
- media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy`
|
||||
- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`
|
||||
- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`
|
||||
- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker`
|
||||
- reactions: `reactionNotifications`, `reactionLevel`
|
||||
- writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
|
||||
|
||||
@@ -414,6 +414,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
webhookUrl: account.config.webhookUrl,
|
||||
webhookSecret: account.config.webhookSecret,
|
||||
webhookPath: account.config.webhookPath,
|
||||
webhookHost: account.config.webhookHost,
|
||||
});
|
||||
},
|
||||
logoutAccount: async ({ accountId, cfg }) => {
|
||||
|
||||
@@ -449,7 +449,7 @@ export async function startCanvasHost(opts: CanvasHostServerOpts): Promise<Canva
|
||||
}));
|
||||
const ownsHandler = opts.ownsHandler ?? opts.handler === undefined;
|
||||
|
||||
const bindHost = opts.listenHost?.trim() || "0.0.0.0";
|
||||
const bindHost = opts.listenHost?.trim() || "127.0.0.1";
|
||||
const server: Server = http.createServer((req, res) => {
|
||||
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") {
|
||||
return;
|
||||
|
||||
@@ -109,6 +109,8 @@ export type TelegramAccountConfig = {
|
||||
webhookUrl?: string;
|
||||
webhookSecret?: string;
|
||||
webhookPath?: string;
|
||||
/** Local webhook listener bind host (default: 127.0.0.1). */
|
||||
webhookHost?: string;
|
||||
/** Per-action tool gating (default: true for all). */
|
||||
actions?: TelegramActionConfig;
|
||||
/**
|
||||
|
||||
@@ -127,6 +127,7 @@ export const TelegramAccountSchemaBase = z
|
||||
webhookUrl: z.string().optional(),
|
||||
webhookSecret: z.string().optional().register(sensitive),
|
||||
webhookPath: z.string().optional(),
|
||||
webhookHost: z.string().optional(),
|
||||
actions: z
|
||||
.object({
|
||||
reactions: z.boolean().optional(),
|
||||
|
||||
@@ -38,6 +38,9 @@ const { computeBackoff, sleepWithAbort } = vi.hoisted(() => ({
|
||||
computeBackoff: vi.fn(() => 0),
|
||||
sleepWithAbort: vi.fn(async () => undefined),
|
||||
}));
|
||||
const { startTelegramWebhookSpy } = vi.hoisted(() => ({
|
||||
startTelegramWebhookSpy: vi.fn(async () => ({ server: { close: vi.fn() }, stop: vi.fn() })),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
@@ -83,6 +86,10 @@ vi.mock("../infra/backoff.js", () => ({
|
||||
sleepWithAbort,
|
||||
}));
|
||||
|
||||
vi.mock("./webhook.js", () => ({
|
||||
startTelegramWebhook: (...args: unknown[]) => startTelegramWebhookSpy(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../auto-reply/reply.js", () => ({
|
||||
getReplyFromConfig: async (ctx: { Body?: string }) => ({
|
||||
text: `echo:${ctx.Body}`,
|
||||
@@ -99,6 +106,7 @@ describe("monitorTelegramProvider (grammY)", () => {
|
||||
runSpy.mockClear();
|
||||
computeBackoff.mockClear();
|
||||
sleepWithAbort.mockClear();
|
||||
startTelegramWebhookSpy.mockClear();
|
||||
});
|
||||
|
||||
it("processes a DM and sends reply", async () => {
|
||||
@@ -187,4 +195,28 @@ describe("monitorTelegramProvider (grammY)", () => {
|
||||
|
||||
await expect(monitorTelegramProvider({ token: "tok" })).rejects.toThrow("bad token");
|
||||
});
|
||||
|
||||
it("passes configured webhookHost to webhook listener", async () => {
|
||||
await monitorTelegramProvider({
|
||||
token: "tok",
|
||||
useWebhook: true,
|
||||
webhookUrl: "https://example.test/telegram",
|
||||
webhookSecret: "secret",
|
||||
config: {
|
||||
agents: { defaults: { maxConcurrent: 2 } },
|
||||
channels: {
|
||||
telegram: {
|
||||
webhookHost: "0.0.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(startTelegramWebhookSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
host: "0.0.0.0",
|
||||
}),
|
||||
);
|
||||
expect(runSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ export type MonitorTelegramOpts = {
|
||||
webhookPath?: string;
|
||||
webhookPort?: number;
|
||||
webhookSecret?: string;
|
||||
webhookHost?: string;
|
||||
proxyFetch?: typeof fetch;
|
||||
webhookUrl?: string;
|
||||
};
|
||||
@@ -158,6 +159,7 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
|
||||
path: opts.webhookPath,
|
||||
port: opts.webhookPort,
|
||||
secret: opts.webhookSecret,
|
||||
host: opts.webhookHost ?? account.config.webhookHost,
|
||||
runtime: opts.runtime as RuntimeEnv,
|
||||
fetch: proxyFetch,
|
||||
abortSignal: opts.abortSignal,
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function startTelegramWebhook(opts: {
|
||||
const path = opts.path ?? "/telegram-webhook";
|
||||
const healthPath = opts.healthPath ?? "/healthz";
|
||||
const port = opts.port ?? 8787;
|
||||
const host = opts.host ?? "0.0.0.0";
|
||||
const host = opts.host ?? "127.0.0.1";
|
||||
const runtime = opts.runtime ?? defaultRuntime;
|
||||
const diagnosticsEnabled = isDiagnosticsEnabled(opts.config);
|
||||
const bot = createTelegramBot({
|
||||
|
||||
Reference in New Issue
Block a user