Unverified Commit 30f9cf4a authored by Hong Minhee's avatar Hong Minhee
Browse files
parent 1a3a9b8d
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -71,6 +71,7 @@
    "rels",
    "setext",
    "spki",
    "SSRF",
    "subproperty",
    "superproperty",
    "tempserver",
+11 −0
Original line number Diff line number Diff line
@@ -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
-------------
+31 −0
Original line number Diff line number Diff line
@@ -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.");
@@ -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) => {
@@ -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) => {
+3 −0
Original line number Diff line number Diff line
@@ -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"]);

@@ -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, {
@@ -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);

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