mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 07:10:24 +00:00
fix(telegram): add dangerous private-network media opt-in
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { danger, logVerbose, warn } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { mergeTelegramAccountConfig } from "./accounts.js";
|
||||
import {
|
||||
hasInboundMedia,
|
||||
isRecoverableMediaGroupError,
|
||||
@@ -84,7 +85,9 @@ export function createTelegramInboundBufferRuntime(params: {
|
||||
runtime,
|
||||
telegramTransport,
|
||||
} = params;
|
||||
const telegramCfg = cfg.channels?.telegram;
|
||||
const telegramCfg = accountId
|
||||
? mergeTelegramAccountConfig(cfg, accountId)
|
||||
: cfg.channels?.telegram;
|
||||
const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000;
|
||||
const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS =
|
||||
typeof opts.testTimings?.textFragmentGapMs === "number" &&
|
||||
@@ -158,6 +161,7 @@ export function createTelegramInboundBufferRuntime(params: {
|
||||
opts.token,
|
||||
telegramTransport,
|
||||
telegramCfg?.apiRoot,
|
||||
telegramCfg?.network?.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
if (!media) {
|
||||
return [];
|
||||
@@ -188,7 +192,14 @@ export function createTelegramInboundBufferRuntime(params: {
|
||||
for (const { ctx } of entry.messages) {
|
||||
let media;
|
||||
try {
|
||||
media = await resolveMedia(ctx, mediaMaxBytes, opts.token, telegramTransport, telegramCfg?.apiRoot);
|
||||
media = await resolveMedia(
|
||||
ctx,
|
||||
mediaMaxBytes,
|
||||
opts.token,
|
||||
telegramTransport,
|
||||
telegramCfg?.apiRoot,
|
||||
telegramCfg?.network?.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
} catch (mediaErr) {
|
||||
if (!isRecoverableMediaGroupError(mediaErr)) {
|
||||
throw mediaErr;
|
||||
|
||||
@@ -387,6 +387,7 @@ export const registerTelegramHandlers = ({
|
||||
opts.token,
|
||||
telegramTransport,
|
||||
telegramCfg.apiRoot,
|
||||
telegramCfg.network?.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
} catch (mediaErr) {
|
||||
if (!isRecoverableMediaGroupError(mediaErr)) {
|
||||
@@ -495,6 +496,7 @@ export const registerTelegramHandlers = ({
|
||||
opts.token,
|
||||
telegramTransport,
|
||||
telegramCfg.apiRoot,
|
||||
telegramCfg.network?.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
if (!media) {
|
||||
return [];
|
||||
@@ -1019,6 +1021,7 @@ export const registerTelegramHandlers = ({
|
||||
opts.token,
|
||||
telegramTransport,
|
||||
telegramCfg.apiRoot,
|
||||
telegramCfg.network?.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
} catch (mediaErr) {
|
||||
if (isMediaSizeLimitError(mediaErr)) {
|
||||
|
||||
@@ -546,19 +546,32 @@ describe("resolveMedia original filename preservation", () => {
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("opts into private-network Telegram media downloads only when explicitly configured", async () => {
|
||||
const getFile = vi.fn().mockResolvedValue({ file_path: "documents/file_42.pdf" });
|
||||
mockPdfFetchAndSave("file_42.pdf");
|
||||
|
||||
const ctx = makeCtx("document", getFile);
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN, undefined, undefined, true);
|
||||
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ssrfPolicy: {
|
||||
hostnameAllowlist: ["api.telegram.org"],
|
||||
allowPrivateNetwork: true,
|
||||
allowRfc2544BenchmarkRange: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("constructs correct download URL with custom apiRoot for documents", async () => {
|
||||
const getFile = vi.fn().mockResolvedValue({ file_path: "documents/file_42.pdf" });
|
||||
mockPdfFetchAndSave("file_42.pdf");
|
||||
|
||||
const customApiRoot = "http://192.168.1.50:8081/custom-bot-api";
|
||||
const ctx = makeCtx("document", getFile);
|
||||
const result = await resolveMedia(
|
||||
ctx,
|
||||
MAX_MEDIA_BYTES,
|
||||
BOT_TOKEN,
|
||||
undefined,
|
||||
customApiRoot,
|
||||
);
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN, undefined, customApiRoot);
|
||||
|
||||
// Verify the URL uses the custom apiRoot, not the default Telegram API
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledWith(
|
||||
@@ -583,13 +596,7 @@ describe("resolveMedia original filename preservation", () => {
|
||||
|
||||
const customApiRoot = "http://localhost:8081/bot";
|
||||
const ctx = makeCtx("sticker", getFile);
|
||||
const result = await resolveMedia(
|
||||
ctx,
|
||||
MAX_MEDIA_BYTES,
|
||||
BOT_TOKEN,
|
||||
undefined,
|
||||
customApiRoot,
|
||||
);
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN, undefined, customApiRoot);
|
||||
|
||||
// Verify the URL uses the custom apiRoot for sticker downloads
|
||||
expect(fetchRemoteMedia).toHaveBeenCalledWith(
|
||||
|
||||
@@ -18,7 +18,7 @@ const FILE_TOO_BIG_RE = /file is too big/i;
|
||||
const GrammyErrorCtor: typeof GrammyError | undefined =
|
||||
typeof GrammyError === "function" ? GrammyError : undefined;
|
||||
|
||||
function buildTelegramMediaSsrfPolicy(apiRoot?: string) {
|
||||
function buildTelegramMediaSsrfPolicy(apiRoot?: string, dangerouslyAllowPrivateNetwork?: boolean) {
|
||||
const hostnames = ["api.telegram.org"];
|
||||
let allowedHostnames: string[] | undefined;
|
||||
if (apiRoot) {
|
||||
@@ -41,6 +41,7 @@ function buildTelegramMediaSsrfPolicy(apiRoot?: string) {
|
||||
// enforcing SSRF checks on the resolved and redirected targets.
|
||||
hostnameAllowlist: hostnames,
|
||||
...(allowedHostnames ? { allowedHostnames } : {}),
|
||||
...(dangerouslyAllowPrivateNetwork ? { allowPrivateNetwork: true } : {}),
|
||||
allowRfc2544BenchmarkRange: true,
|
||||
};
|
||||
}
|
||||
@@ -169,6 +170,7 @@ async function downloadAndSaveTelegramFile(params: {
|
||||
telegramFileName?: string;
|
||||
mimeType?: string;
|
||||
apiRoot?: string;
|
||||
dangerouslyAllowPrivateNetwork?: boolean;
|
||||
}) {
|
||||
if (path.isAbsolute(params.filePath)) {
|
||||
return { path: params.filePath, contentType: params.mimeType };
|
||||
@@ -183,7 +185,7 @@ async function downloadAndSaveTelegramFile(params: {
|
||||
filePathHint: params.filePath,
|
||||
maxBytes: params.maxBytes,
|
||||
readIdleTimeoutMs: TELEGRAM_DOWNLOAD_IDLE_TIMEOUT_MS,
|
||||
ssrfPolicy: buildTelegramMediaSsrfPolicy(params.apiRoot),
|
||||
ssrfPolicy: buildTelegramMediaSsrfPolicy(params.apiRoot, params.dangerouslyAllowPrivateNetwork),
|
||||
});
|
||||
const originalName = params.telegramFileName ?? fetched.fileName ?? params.filePath;
|
||||
return saveMediaBuffer(
|
||||
@@ -202,6 +204,7 @@ async function resolveStickerMedia(params: {
|
||||
token: string;
|
||||
transport?: TelegramTransport;
|
||||
apiRoot?: string;
|
||||
dangerouslyAllowPrivateNetwork?: boolean;
|
||||
}): Promise<
|
||||
| {
|
||||
path: string;
|
||||
@@ -243,6 +246,7 @@ async function resolveStickerMedia(params: {
|
||||
transport: resolvedTransport,
|
||||
maxBytes,
|
||||
apiRoot: params.apiRoot,
|
||||
dangerouslyAllowPrivateNetwork: params.dangerouslyAllowPrivateNetwork,
|
||||
});
|
||||
|
||||
// Check sticker cache for existing description
|
||||
@@ -299,6 +303,7 @@ export async function resolveMedia(
|
||||
token: string,
|
||||
transport?: TelegramTransport,
|
||||
apiRoot?: string,
|
||||
dangerouslyAllowPrivateNetwork?: boolean,
|
||||
): Promise<{
|
||||
path: string;
|
||||
contentType?: string;
|
||||
@@ -313,6 +318,7 @@ export async function resolveMedia(
|
||||
token,
|
||||
transport,
|
||||
apiRoot,
|
||||
dangerouslyAllowPrivateNetwork,
|
||||
});
|
||||
if (stickerResolved !== undefined) {
|
||||
return stickerResolved;
|
||||
@@ -339,6 +345,7 @@ export async function resolveMedia(
|
||||
telegramFileName: metadata.fileName,
|
||||
mimeType: metadata.mimeType,
|
||||
apiRoot,
|
||||
dangerouslyAllowPrivateNetwork,
|
||||
});
|
||||
const placeholder = resolveTelegramMediaPlaceholder(msg) ?? "<media:document>";
|
||||
return { path: saved.path, contentType: saved.contentType, placeholder };
|
||||
|
||||
@@ -53,6 +53,10 @@ export const telegramChannelConfigUiHints = {
|
||||
label: "Telegram autoSelectFamily",
|
||||
help: "Override Node autoSelectFamily for Telegram (true=enable, false=disable).",
|
||||
},
|
||||
"network.dangerouslyAllowPrivateNetwork": {
|
||||
label: "Telegram Dangerously Allow Private Network",
|
||||
help: "Dangerous opt-in for trusted fake-IP or transparent-proxy environments where Telegram media downloads resolve api.telegram.org to private/internal/special-use addresses.",
|
||||
},
|
||||
timeoutSeconds: {
|
||||
label: "Telegram API Timeout (seconds)",
|
||||
help: "Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
|
||||
|
||||
Reference in New Issue
Block a user