Loading CHANGES.md +42 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,23 @@ Version 1.1.11 To be released. - Fixed several security vulnerabilities of the `lookupWebFinger()` function. [[CVE-2025-23221]] - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the infinite number of redirects, which could lead to a denial of service attack. Now it follows up to 5 redirects. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to other than the HTTP/HTTPS schemes, which could lead to a security breach. Now it follows only the same scheme as the original request. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to the private network addresses, which could lead to a SSRF attack. Now it follows only the public network addresses. Version 1.1.10 -------------- Loading Loading @@ -274,6 +291,31 @@ Released on October 20, 2024. [#150]: https://github.com/dahlia/fedify/issues/150 Version 1.0.14 -------------- Released on January 21, 2025. - Fixed several security vulnerabilities of the `lookupWebFinger()` function. [[CVE-2025-23221]] - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the infinite number of redirects, which could lead to a denial of service attack. Now it follows up to 5 redirects. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to other than the HTTP/HTTPS schemes, which could lead to a security breach. Now it follows only the same scheme as the original request. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to the private network addresses, which could lead to a SSRF attack. Now it follows only the public network addresses. [CVE-2025-23221]: https://github.com/dahlia/fedify/security/advisories/GHSA-c59p-wq67-24wx Version 1.0.13 -------------- Loading src/runtime/url.ts +7 −1 Original line number Diff line number Diff line import type { LookupAddress } from "node:dns"; import { lookup } from "node:dns/promises"; import { isIP } from "node:net"; Loading Loading @@ -38,7 +39,12 @@ export async function validatePublicUrl(url: string): Promise<void> { } // To prevent SSRF via DNS rebinding, we need to resolve all IP addresses // and ensure that they are all public: const addresses = await lookup(hostname, { all: true }); let addresses: LookupAddress[]; try { addresses = await lookup(hostname, { all: true }); } catch { addresses = []; } for (const { address, family } of addresses) { if ( family === 4 && !isValidPublicIPv4Address(address) || Loading src/webfinger/lookup.test.ts +49 −1 Original line number Diff line number Diff line import { assertEquals } from "@std/assert"; import { assertEquals, assertRejects } from "@std/assert"; import { deadline } from "@std/async/deadline"; import * as mf from "mock_fetch"; import { UrlError } from "../runtime/url.ts"; import { test } from "../testing/mod.ts"; import type { ResourceDescriptor } from "./jrd.ts"; import { lookupWebFinger } from "./lookup.ts"; Loading Loading @@ -91,6 +93,52 @@ test("lookupWebFinger()", async (t) => { assertEquals(await lookupWebFinger("acct:johndoe@example.com"), expected); }); mf.mock( "GET@/.well-known/webfinger", (_) => new Response("", { status: 302, headers: { Location: "/.well-known/webfinger" }, }), ); await t.step("infinite redirection", async () => { const result = await deadline( lookupWebFinger("acct:johndoe@example.com"), 2000, ); assertEquals(result, null); }); mf.mock( "GET@/.well-known/webfinger", (_) => new Response("", { status: 302, headers: { Location: "ftp://example.com/" }, }), ); await t.step("redirection to different protocol", async () => { assertEquals(await lookupWebFinger("acct:johndoe@example.com"), null); }); mf.mock( "GET@/.well-known/webfinger", (_) => new Response("", { status: 302, headers: { Location: "https://localhost/" }, }), ); await t.step("redirection to private address", async () => { await assertRejects( () => lookupWebFinger("acct:johndoe@example.com"), UrlError, ); }); mf.uninstall(); }); Loading src/webfinger/lookup.ts +28 −1 Original line number Diff line number Diff line import { getLogger } from "@logtape/logtape"; import { validatePublicUrl } from "../runtime/url.ts"; import type { ResourceDescriptor } from "./jrd.ts"; const logger = getLogger(["fedify", "webfinger", "lookup"]); const MAX_REDIRECTION = 5; // TODO: Make this configurable. /** * Looks up a WebFinger resource. * @param resource The resource URL to look up. Loading @@ -26,12 +29,14 @@ export async function lookupWebFinger( } let url = new URL(`${protocol}//${server}/.well-known/webfinger`); url.searchParams.set("resource", resource.href); let redirected = 0; while (true) { logger.debug( "Fetching WebFinger resource descriptor from {url}...", { url: url.href }, ); let response: Response; await validatePublicUrl(url.href); try { response = await fetch(url, { headers: { Accept: "application/jrd+json" }, Loading @@ -48,10 +53,32 @@ export async function lookupWebFinger( response.status >= 300 && response.status < 400 && response.headers.has("Location") ) { url = new URL( redirected++; if (redirected >= MAX_REDIRECTION) { logger.error( "Too many redirections ({redirections}) while fetching WebFinger " + "resource descriptor.", { redirections: redirected }, ); return null; } const redirectedUrl = new URL( response.headers.get("Location")!, response.url == null || response.url === "" ? url : response.url, ); if (redirectedUrl.protocol !== url.protocol) { logger.error( "Redirected to a different protocol ({protocol} to " + "{redirectedProtocol}) while fetching WebFinger resource " + "descriptor.", { protocol: url.protocol, redirectedProtocol: redirectedUrl.protocol, }, ); return null; } url = redirectedUrl; continue; } if (!response.ok) { Loading Loading
CHANGES.md +42 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,23 @@ Version 1.1.11 To be released. - Fixed several security vulnerabilities of the `lookupWebFinger()` function. [[CVE-2025-23221]] - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the infinite number of redirects, which could lead to a denial of service attack. Now it follows up to 5 redirects. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to other than the HTTP/HTTPS schemes, which could lead to a security breach. Now it follows only the same scheme as the original request. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to the private network addresses, which could lead to a SSRF attack. Now it follows only the public network addresses. Version 1.1.10 -------------- Loading Loading @@ -274,6 +291,31 @@ Released on October 20, 2024. [#150]: https://github.com/dahlia/fedify/issues/150 Version 1.0.14 -------------- Released on January 21, 2025. - Fixed several security vulnerabilities of the `lookupWebFinger()` function. [[CVE-2025-23221]] - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the infinite number of redirects, which could lead to a denial of service attack. Now it follows up to 5 redirects. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to other than the HTTP/HTTPS schemes, which could lead to a security breach. Now it follows only the same scheme as the original request. - Fixed a security vulnerability where the `lookupWebFinger()` function had followed the redirects to the private network addresses, which could lead to a SSRF attack. Now it follows only the public network addresses. [CVE-2025-23221]: https://github.com/dahlia/fedify/security/advisories/GHSA-c59p-wq67-24wx Version 1.0.13 -------------- Loading
src/runtime/url.ts +7 −1 Original line number Diff line number Diff line import type { LookupAddress } from "node:dns"; import { lookup } from "node:dns/promises"; import { isIP } from "node:net"; Loading Loading @@ -38,7 +39,12 @@ export async function validatePublicUrl(url: string): Promise<void> { } // To prevent SSRF via DNS rebinding, we need to resolve all IP addresses // and ensure that they are all public: const addresses = await lookup(hostname, { all: true }); let addresses: LookupAddress[]; try { addresses = await lookup(hostname, { all: true }); } catch { addresses = []; } for (const { address, family } of addresses) { if ( family === 4 && !isValidPublicIPv4Address(address) || Loading
src/webfinger/lookup.test.ts +49 −1 Original line number Diff line number Diff line import { assertEquals } from "@std/assert"; import { assertEquals, assertRejects } from "@std/assert"; import { deadline } from "@std/async/deadline"; import * as mf from "mock_fetch"; import { UrlError } from "../runtime/url.ts"; import { test } from "../testing/mod.ts"; import type { ResourceDescriptor } from "./jrd.ts"; import { lookupWebFinger } from "./lookup.ts"; Loading Loading @@ -91,6 +93,52 @@ test("lookupWebFinger()", async (t) => { assertEquals(await lookupWebFinger("acct:johndoe@example.com"), expected); }); mf.mock( "GET@/.well-known/webfinger", (_) => new Response("", { status: 302, headers: { Location: "/.well-known/webfinger" }, }), ); await t.step("infinite redirection", async () => { const result = await deadline( lookupWebFinger("acct:johndoe@example.com"), 2000, ); assertEquals(result, null); }); mf.mock( "GET@/.well-known/webfinger", (_) => new Response("", { status: 302, headers: { Location: "ftp://example.com/" }, }), ); await t.step("redirection to different protocol", async () => { assertEquals(await lookupWebFinger("acct:johndoe@example.com"), null); }); mf.mock( "GET@/.well-known/webfinger", (_) => new Response("", { status: 302, headers: { Location: "https://localhost/" }, }), ); await t.step("redirection to private address", async () => { await assertRejects( () => lookupWebFinger("acct:johndoe@example.com"), UrlError, ); }); mf.uninstall(); }); Loading
src/webfinger/lookup.ts +28 −1 Original line number Diff line number Diff line import { getLogger } from "@logtape/logtape"; import { validatePublicUrl } from "../runtime/url.ts"; import type { ResourceDescriptor } from "./jrd.ts"; const logger = getLogger(["fedify", "webfinger", "lookup"]); const MAX_REDIRECTION = 5; // TODO: Make this configurable. /** * Looks up a WebFinger resource. * @param resource The resource URL to look up. Loading @@ -26,12 +29,14 @@ export async function lookupWebFinger( } let url = new URL(`${protocol}//${server}/.well-known/webfinger`); url.searchParams.set("resource", resource.href); let redirected = 0; while (true) { logger.debug( "Fetching WebFinger resource descriptor from {url}...", { url: url.href }, ); let response: Response; await validatePublicUrl(url.href); try { response = await fetch(url, { headers: { Accept: "application/jrd+json" }, Loading @@ -48,10 +53,32 @@ export async function lookupWebFinger( response.status >= 300 && response.status < 400 && response.headers.has("Location") ) { url = new URL( redirected++; if (redirected >= MAX_REDIRECTION) { logger.error( "Too many redirections ({redirections}) while fetching WebFinger " + "resource descriptor.", { redirections: redirected }, ); return null; } const redirectedUrl = new URL( response.headers.get("Location")!, response.url == null || response.url === "" ? url : response.url, ); if (redirectedUrl.protocol !== url.protocol) { logger.error( "Redirected to a different protocol ({protocol} to " + "{redirectedProtocol}) while fetching WebFinger resource " + "descriptor.", { protocol: url.protocol, redirectedProtocol: redirectedUrl.protocol, }, ); return null; } url = redirectedUrl; continue; } if (!response.ok) { Loading