Loading .vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,7 @@ "rels", "setext", "spki", "SSRF", "subproperty", "superproperty", "tempserver", Loading CHANGES.md +11 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,17 @@ Version 0.9.2 To be released. - Fixed a SSRF vulnerability in the built-in document loader. [[CVE-2024-39687]] - The `fetchDocumentLoader()` function now throws an error when the given URL is not an HTTP or HTTPS URL or refers to a private network address. - The `getAuthenticatedDocumentLoader()` function now returns a document loader that throws an error when the given URL is not an HTTP or HTTPS URL or refers to a private network address. [CVE-2024-39687]: https://github.com/dahlia/fedify/security/advisories/GHSA-p9cg-vqcc-grcx Version 0.9.1 ------------- Loading runtime/docloader.test.ts +31 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ import { getAuthenticatedDocumentLoader, kvCache, } from "./docloader.ts"; import { UrlError } from "./url.ts"; Deno.test("new FetchError()", () => { const e = new FetchError("https://example.com/", "An error message."); Loading Loading @@ -60,6 +61,20 @@ Deno.test("fetchDocumentLoader()", async (t) => { }); mf.uninstall(); await t.step("deny non-HTTP/HTTPS", async () => { await assertRejects( () => fetchDocumentLoader("ftp://localhost"), UrlError, ); }); await t.step("deny private network", async () => { await assertRejects( () => fetchDocumentLoader("https://localhost"), UrlError, ); }); }); Deno.test("getAuthenticatedDocumentLoader()", async (t) => { Loading Loading @@ -92,6 +107,22 @@ Deno.test("getAuthenticatedDocumentLoader()", async (t) => { }); mf.uninstall(); await t.step("deny non-HTTP/HTTPS", async () => { const loader = await getAuthenticatedDocumentLoader({ keyId: new URL("https://example.com/key2"), privateKey: privateKey2, }); assertRejects(() => loader("ftp://localhost"), UrlError); }); await t.step("deny private network", async () => { const loader = await getAuthenticatedDocumentLoader({ keyId: new URL("https://example.com/key2"), privateKey: privateKey2, }); assertRejects(() => loader("http://localhost"), UrlError); }); }); Deno.test("kvCache()", async (t) => { Loading runtime/docloader.ts +3 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import { getLogger } from "@logtape/logtape"; import type { KvKey, KvStore } from "../federation/kv.ts"; import { signRequest } from "../sig/http.ts"; import { validateCryptoKey } from "../sig/key.ts"; import { validatePublicUrl } from "./url.ts"; const logger = getLogger(["fedify", "runtime", "docloader"]); Loading Loading @@ -119,6 +120,7 @@ async function getRemoteDocument( export async function fetchDocumentLoader( url: string, ): Promise<RemoteDocument> { await validatePublicUrl(url); const request = createRequest(url); logRequest(request); const response = await fetch(request, { Loading Loading @@ -152,6 +154,7 @@ export function getAuthenticatedDocumentLoader( ): DocumentLoader { validateCryptoKey(identity.privateKey); async function load(url: string): Promise<RemoteDocument> { await validatePublicUrl(url); let request = createRequest(url); request = await signRequest(request, identity.privateKey, identity.keyId); logRequest(request); Loading runtime/url.test.ts 0 → 100644 +61 −0 Original line number Diff line number Diff line import { assert } from "@std/assert/assert"; import { assertEquals } from "@std/assert/assert-equals"; import { assertFalse } from "@std/assert/assert-false"; import { assertRejects } from "@std/assert/assert-rejects"; import { expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address, UrlError, validatePublicUrl, } from "./url.ts"; Deno.test("validatePublicUrl()", async () => { await assertRejects(() => validatePublicUrl("ftp://localhost"), UrlError); await assertRejects( // cSpell: disable () => validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="), // cSpell: enable UrlError, ); await assertRejects(() => validatePublicUrl("https://localhost"), UrlError); await assertRejects(() => validatePublicUrl("https://127.0.0.1"), UrlError); await assertRejects(() => validatePublicUrl("https://[::1]"), UrlError); }); Deno.test("isValidPublicIPv4Address()", () => { assert(isValidPublicIPv4Address("8.8.8.8")); // Google DNS assertFalse(isValidPublicIPv4Address("192.168.1.1")); // private assertFalse(isValidPublicIPv4Address("127.0.0.1")); // localhost assertFalse(isValidPublicIPv4Address("10.0.0.1")); // private assertFalse(isValidPublicIPv4Address("127.16.0.1")); // private assertFalse(isValidPublicIPv4Address("169.254.0.1")); // link-local }); Deno.test("isValidPublicIPv6Address()", () => { assert(isValidPublicIPv6Address("2001:db8::1")); assertFalse(isValidPublicIPv6Address("::1")); // localhost assertFalse(isValidPublicIPv6Address("fc00::1")); // ULA assertFalse(isValidPublicIPv6Address("fe80::1")); // link-local assertFalse(isValidPublicIPv6Address("ff00::1")); // multicast assertFalse(isValidPublicIPv6Address("::")); // unspecified }); Deno.test("expandIPv6Address()", () => { assertEquals( expandIPv6Address("::"), "0000:0000:0000:0000:0000:0000:0000:0000", ); assertEquals( expandIPv6Address("::1"), "0000:0000:0000:0000:0000:0000:0000:0001", ); assertEquals( expandIPv6Address("2001:db8::"), "2001:0db8:0000:0000:0000:0000:0000:0000", ); assertEquals( expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001", ); }); Loading
.vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,7 @@ "rels", "setext", "spki", "SSRF", "subproperty", "superproperty", "tempserver", Loading
CHANGES.md +11 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,17 @@ Version 0.9.2 To be released. - Fixed a SSRF vulnerability in the built-in document loader. [[CVE-2024-39687]] - The `fetchDocumentLoader()` function now throws an error when the given URL is not an HTTP or HTTPS URL or refers to a private network address. - The `getAuthenticatedDocumentLoader()` function now returns a document loader that throws an error when the given URL is not an HTTP or HTTPS URL or refers to a private network address. [CVE-2024-39687]: https://github.com/dahlia/fedify/security/advisories/GHSA-p9cg-vqcc-grcx Version 0.9.1 ------------- Loading
runtime/docloader.test.ts +31 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ import { getAuthenticatedDocumentLoader, kvCache, } from "./docloader.ts"; import { UrlError } from "./url.ts"; Deno.test("new FetchError()", () => { const e = new FetchError("https://example.com/", "An error message."); Loading Loading @@ -60,6 +61,20 @@ Deno.test("fetchDocumentLoader()", async (t) => { }); mf.uninstall(); await t.step("deny non-HTTP/HTTPS", async () => { await assertRejects( () => fetchDocumentLoader("ftp://localhost"), UrlError, ); }); await t.step("deny private network", async () => { await assertRejects( () => fetchDocumentLoader("https://localhost"), UrlError, ); }); }); Deno.test("getAuthenticatedDocumentLoader()", async (t) => { Loading Loading @@ -92,6 +107,22 @@ Deno.test("getAuthenticatedDocumentLoader()", async (t) => { }); mf.uninstall(); await t.step("deny non-HTTP/HTTPS", async () => { const loader = await getAuthenticatedDocumentLoader({ keyId: new URL("https://example.com/key2"), privateKey: privateKey2, }); assertRejects(() => loader("ftp://localhost"), UrlError); }); await t.step("deny private network", async () => { const loader = await getAuthenticatedDocumentLoader({ keyId: new URL("https://example.com/key2"), privateKey: privateKey2, }); assertRejects(() => loader("http://localhost"), UrlError); }); }); Deno.test("kvCache()", async (t) => { Loading
runtime/docloader.ts +3 −0 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import { getLogger } from "@logtape/logtape"; import type { KvKey, KvStore } from "../federation/kv.ts"; import { signRequest } from "../sig/http.ts"; import { validateCryptoKey } from "../sig/key.ts"; import { validatePublicUrl } from "./url.ts"; const logger = getLogger(["fedify", "runtime", "docloader"]); Loading Loading @@ -119,6 +120,7 @@ async function getRemoteDocument( export async function fetchDocumentLoader( url: string, ): Promise<RemoteDocument> { await validatePublicUrl(url); const request = createRequest(url); logRequest(request); const response = await fetch(request, { Loading Loading @@ -152,6 +154,7 @@ export function getAuthenticatedDocumentLoader( ): DocumentLoader { validateCryptoKey(identity.privateKey); async function load(url: string): Promise<RemoteDocument> { await validatePublicUrl(url); let request = createRequest(url); request = await signRequest(request, identity.privateKey, identity.keyId); logRequest(request); Loading
runtime/url.test.ts 0 → 100644 +61 −0 Original line number Diff line number Diff line import { assert } from "@std/assert/assert"; import { assertEquals } from "@std/assert/assert-equals"; import { assertFalse } from "@std/assert/assert-false"; import { assertRejects } from "@std/assert/assert-rejects"; import { expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address, UrlError, validatePublicUrl, } from "./url.ts"; Deno.test("validatePublicUrl()", async () => { await assertRejects(() => validatePublicUrl("ftp://localhost"), UrlError); await assertRejects( // cSpell: disable () => validatePublicUrl("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="), // cSpell: enable UrlError, ); await assertRejects(() => validatePublicUrl("https://localhost"), UrlError); await assertRejects(() => validatePublicUrl("https://127.0.0.1"), UrlError); await assertRejects(() => validatePublicUrl("https://[::1]"), UrlError); }); Deno.test("isValidPublicIPv4Address()", () => { assert(isValidPublicIPv4Address("8.8.8.8")); // Google DNS assertFalse(isValidPublicIPv4Address("192.168.1.1")); // private assertFalse(isValidPublicIPv4Address("127.0.0.1")); // localhost assertFalse(isValidPublicIPv4Address("10.0.0.1")); // private assertFalse(isValidPublicIPv4Address("127.16.0.1")); // private assertFalse(isValidPublicIPv4Address("169.254.0.1")); // link-local }); Deno.test("isValidPublicIPv6Address()", () => { assert(isValidPublicIPv6Address("2001:db8::1")); assertFalse(isValidPublicIPv6Address("::1")); // localhost assertFalse(isValidPublicIPv6Address("fc00::1")); // ULA assertFalse(isValidPublicIPv6Address("fe80::1")); // link-local assertFalse(isValidPublicIPv6Address("ff00::1")); // multicast assertFalse(isValidPublicIPv6Address("::")); // unspecified }); Deno.test("expandIPv6Address()", () => { assertEquals( expandIPv6Address("::"), "0000:0000:0000:0000:0000:0000:0000:0000", ); assertEquals( expandIPv6Address("::1"), "0000:0000:0000:0000:0000:0000:0000:0001", ); assertEquals( expandIPv6Address("2001:db8::"), "2001:0db8:0000:0000:0000:0000:0000:0000", ); assertEquals( expandIPv6Address("2001:db8::1"), "2001:0db8:0000:0000:0000:0000:0000:0001", ); });