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:
David Rudduck
2026-02-14 01:39:56 +10:00
committed by GitHub
parent a17f74306d
commit 5643a93479
10 changed files with 85 additions and 5 deletions

View File

@@ -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;

View File

@@ -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;
/**

View File

@@ -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(),

View File

@@ -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();
});
});

View File

@@ -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,

View File

@@ -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({