diff --git a/CHANGES.md b/CHANGES.md index f44fb87..8bd3076 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,6 +48,29 @@ To be released. [#195]: https://github.com/dahlia/fedify/issues/195 +Version 1.3.4 +------------- + +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. + + Version 1.3.3 ------------- @@ -192,6 +215,29 @@ Released on November 30, 2024. [#193]: https://github.com/dahlia/fedify/issues/193 +Version 1.2.11 +-------------- + +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. + + Version 1.2.10 -------------- @@ -416,6 +462,29 @@ Released on October 31, 2024. [#118]: https://github.com/dahlia/fedify/issues/118 +Version 1.1.11 +-------------- + +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. + + Version 1.1.10 -------------- @@ -681,6 +750,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 -------------- diff --git a/src/runtime/url.ts b/src/runtime/url.ts index dd95b54..5c8ed2d 100644 --- a/src/runtime/url.ts +++ b/src/runtime/url.ts @@ -1,3 +1,4 @@ +import type { LookupAddress } from "node:dns"; import { lookup } from "node:dns/promises"; import { isIP } from "node:net"; @@ -38,7 +39,12 @@ export async function validatePublicUrl(url: string): Promise { } // 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) || diff --git a/src/webfinger/lookup.test.ts b/src/webfinger/lookup.test.ts index 0630e76..b7af79e 100644 --- a/src/webfinger/lookup.test.ts +++ b/src/webfinger/lookup.test.ts @@ -1,5 +1,7 @@ -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"; @@ -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(); }); diff --git a/src/webfinger/lookup.ts b/src/webfinger/lookup.ts index cca0f67..a8921af 100644 --- a/src/webfinger/lookup.ts +++ b/src/webfinger/lookup.ts @@ -10,10 +10,13 @@ import { getUserAgent, type GetUserAgentOptions, } from "../runtime/docloader.ts"; +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. + /** * Options for {@link lookupWebFinger}. * @since 1.3.0 @@ -99,12 +102,14 @@ async function lookupWebFingerInternal( } 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: { @@ -126,10 +131,32 @@ async function lookupWebFingerInternal( 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) {