Unverified Commit e7fd6a60 authored by Hong Minhee's avatar Hong Minhee
Browse files

Merge tag '1.5.5' into 1.6-maintenance

Fedify 1.5.5
parents b0e1f060 6a5ae7b4
Loading
Loading
Loading
Loading
+48 −0
Original line number Diff line number Diff line
@@ -8,6 +8,13 @@ Version 1.6.8

To be released.

 -  Fixed a critical authentication bypass vulnerability in the inbox handler
    that allowed unauthenticated attackers to impersonate any ActivityPub actor.
    The vulnerability occurred because activities were processed before
    verifying that the HTTP Signatures key belonged to the claimed actor.
    Now authentication verification is performed before activity processing to
    prevent actor impersonation attacks.  [[CVE-2025-54888]]


Version 1.6.7
-------------
@@ -137,6 +144,19 @@ the versioning.
[#242]: https://github.com/fedify-dev/fedify/pull/242


Version 1.5.5
-------------

Released on August 8, 2025.

 -  Fixed a critical authentication bypass vulnerability in the inbox handler
    that allowed unauthenticated attackers to impersonate any ActivityPub actor.
    The vulnerability occurred because activities were processed before
    verifying that the HTTP Signatures key belonged to the claimed actor.
    Now authentication verification is performed before activity processing to
    prevent actor impersonation attacks.  [[CVE-2025-54888]]


Version 1.5.4
-------------

@@ -311,6 +331,19 @@ Released on March 28, 2025.
[multibase]: https://github.com/multiformats/js-multibase


Version 1.4.13
--------------

Released on August 8, 2025.

 -  Fixed a critical authentication bypass vulnerability in the inbox handler
    that allowed unauthenticated attackers to impersonate any ActivityPub actor.
    The vulnerability occurred because activities were processed before
    verifying that the HTTP Signatures key belonged to the claimed actor.
    Now authentication verification is performed before activity processing to
    prevent actor impersonation attacks.  [[CVE-2025-54888]]


Version 1.4.12
--------------

@@ -560,6 +593,21 @@ Released on February 5, 2025.
[#195]: https://github.com/fedify-dev/fedify/issues/195


Version 1.3.20
--------------

Released on August 8, 2025.

 -  Fixed a critical authentication bypass vulnerability in the inbox handler
    that allowed unauthenticated attackers to impersonate any ActivityPub actor.
    The vulnerability occurred because activities were processed before
    verifying that the HTTP Signatures key belonged to the claimed actor.
    Now authentication verification is performed before activity processing to
    prevent actor impersonation attacks.  [[CVE-2025-54888]]

[CVE-2025-54888]: https://github.com/fedify-dev/fedify/security/advisories/GHSA-6jcc-xgcr-q3h4


Version 1.3.19
--------------

+89 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import {
  respondWithObject,
  respondWithObjectIfAcceptable,
} from "./handler.ts";
import { InboxListenerSet } from "./inbox.ts";
import { MemoryKvStore } from "./kv.ts";
import { createFederation } from "./middleware.ts";

@@ -1377,6 +1378,94 @@ test("respondWithObject()", async () => {
  });
});

test("handleInbox() - authentication bypass vulnerability", async () => {
  // This test reproduces the authentication bypass vulnerability where
  // activities are processed before verifying the signing key belongs
  // to the claimed actor

  const federation = createFederation<void>({ kv: new MemoryKvStore() });
  let processedActivity: Create | undefined;
  const inboxListeners = new InboxListenerSet<void>();
  inboxListeners.add(Create, (_ctx, activity) => {
    // Track that the malicious activity was processed
    processedActivity = activity;
  });

  // Create malicious activity claiming to be from victim actor
  const maliciousActivity = new Create({
    id: new URL("https://attacker.example.com/activities/malicious"),
    actor: new URL("https://victim.example.com/users/alice"), // Impersonating victim
    object: new Note({
      id: new URL("https://attacker.example.com/notes/forged"),
      attribution: new URL("https://victim.example.com/users/alice"),
      content: "This is a forged message from the victim!",
    }),
  });

  // Sign request with attacker's key (not victim's key)
  const maliciousRequest = await signRequest(
    new Request("https://example.com/", {
      method: "POST",
      body: JSON.stringify(await maliciousActivity.toJsonLd()),
    }),
    rsaPrivateKey3, // Attacker's private key
    rsaPublicKey3.id!, // Attacker's public key ID
  );

  const maliciousContext = createRequestContext({
    request: maliciousRequest,
    url: new URL(maliciousRequest.url),
    data: undefined,
    documentLoader: mockDocumentLoader,
    federation,
  });

  const actorDispatcher: ActorDispatcher<void> = (_ctx, identifier) => {
    if (identifier !== "someone") return null;
    return new Person({ name: "Someone" });
  };

  const response = await handleInbox(maliciousRequest, {
    recipient: "someone",
    context: maliciousContext,
    inboxContextFactory(_activity) {
      return createInboxContext({
        url: new URL(maliciousRequest.url),
        data: undefined,
        documentLoader: mockDocumentLoader,
        federation,
        recipient: "someone",
      });
    },
    kv: new MemoryKvStore(),
    kvPrefixes: {
      activityIdempotence: ["_fedify", "activityIdempotence"],
      publicKey: ["_fedify", "publicKey"],
    },
    actorDispatcher,
    inboxListeners,
    onNotFound: () => new Response("Not found", { status: 404 }),
    signatureTimeWindow: { minutes: 5 },
    skipSignatureVerification: false,
  });

  // The vulnerability: Even though the response is 401 (unauthorized),
  // the malicious activity was already processed by routeActivity()
  assertEquals(response.status, 401);
  assertEquals(await response.text(), "The signer and the actor do not match.");

  assertEquals(
    processedActivity,
    undefined,
    `SECURITY VULNERABILITY: Malicious activity with mismatched signature was processed! ` +
      `Activity ID: ${processedActivity?.id?.href}, ` +
      `Claimed actor: ${processedActivity?.actorId?.href}`,
  );

  // If we reach here, the vulnerability is fixed - activities with mismatched
  // signatures are properly rejected before processing
});

test("respondWithObjectIfAcceptable", async () => {
  let request = new Request("https://example.com/", {
    headers: { Accept: "application/activity+json" },
+14 −14
Original line number Diff line number Diff line
@@ -705,20 +705,6 @@ async function handleInboxInternal<TContextData>(
    span.setAttribute("activitypub.activity.id", activity.id.href);
  }
  span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
  const routeResult = await routeActivity({
    context: ctx,
    json,
    activity,
    recipient,
    inboxListeners,
    inboxContextFactory,
    inboxErrorHandler,
    kv,
    kvPrefixes,
    queue,
    span,
    tracerProvider,
  });
  if (
    httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)
  ) {
@@ -741,6 +727,20 @@ async function handleInboxInternal<TContextData>(
      headers: { "Content-Type": "text/plain; charset=utf-8" },
    });
  }
  const routeResult = await routeActivity({
    context: ctx,
    json,
    activity,
    recipient,
    inboxListeners,
    inboxContextFactory,
    inboxErrorHandler,
    kv,
    kvPrefixes,
    queue,
    span,
    tracerProvider,
  });
  if (routeResult === "alreadyProcessed") {
    return new Response(
      `Activity <${activity.id}> has already been processed.`,