Telegram: tighten media SSRF policy (#56004)

* Telegram: tighten media SSRF policy

* Telegram: restrict media downloads to configured hosts

* Telegram: preserve custom media apiRoot hosts
This commit is contained in:
Jacob Tomlinson 2026-03-27 13:39:24 -07:00 committed by GitHub
parent 511093d4b3
commit 20c7cbbf78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 38 additions and 6 deletions

View File

@ -155,8 +155,8 @@ async function expectTransientGetFileRetrySuccess() {
expect.objectContaining({
url: `https://api.telegram.org/file/bot${BOT_TOKEN}/voice/file_0.oga`,
ssrfPolicy: {
allowRfc2544BenchmarkRange: true,
allowedHostnames: ["api.telegram.org"],
allowRfc2544BenchmarkRange: false,
hostnameAllowlist: ["api.telegram.org"],
},
}),
);
@ -514,4 +514,29 @@ describe("resolveMedia original filename preservation", () => {
);
expect(result).not.toBeNull();
});
it("allows a configured custom apiRoot host while keeping the hostname allowlist", 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,
"http://192.168.1.50:8081/custom-bot-api/",
);
expect(fetchRemoteMedia).toHaveBeenCalledWith(
expect.objectContaining({
ssrfPolicy: {
hostnameAllowlist: ["api.telegram.org", "192.168.1.50"],
allowedHostnames: ["192.168.1.50"],
allowRfc2544BenchmarkRange: false,
},
}),
);
expect(result).not.toBeNull();
});
});

View File

@ -20,21 +20,28 @@ const GrammyErrorCtor: typeof GrammyError | undefined =
function buildTelegramMediaSsrfPolicy(apiRoot?: string) {
const hostnames = ["api.telegram.org"];
let allowedHostnames: string[] | undefined;
if (apiRoot) {
try {
const customHost = new URL(apiRoot).hostname;
if (customHost && !hostnames.includes(customHost)) {
hostnames.push(customHost);
// A configured custom Bot API host is an explicit operator override and
// may legitimately live on a private network (for example, self-hosted
// Bot API or an internal reverse proxy). Keep that host reachable while
// still enforcing resolved-IP checks for the default public host.
allowedHostnames = [customHost];
}
} catch {
// invalid URL; fall through to default
}
}
return {
// Telegram file downloads should trust the API hostname even when DNS/proxy
// resolution maps to private/internal ranges in restricted networks.
allowedHostnames: hostnames,
allowRfc2544BenchmarkRange: true,
// Restrict media downloads to the configured Telegram API hosts while still
// enforcing SSRF checks on the resolved and redirected targets.
hostnameAllowlist: hostnames,
...(allowedHostnames ? { allowedHostnames } : {}),
allowRfc2544BenchmarkRange: false,
};
}