Skip to content

Commit 226d9b8

Browse files
authored
Merge commit from fork
Fix critical authentication bypass vulnerability in inbox handler
2 parents 78a10fc + 14a2f8c commit 226d9b8

File tree

3 files changed

+104
-14
lines changed

3 files changed

+104
-14
lines changed

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ Version 1.3.20
88

99
To be released.
1010

11+
- Fixed a critical authentication bypass vulnerability in the inbox handler
12+
that allowed unauthenticated attackers to impersonate any ActivityPub actor.
13+
The vulnerability occurred because activities were processed before
14+
verifying that the HTTP Signatures key belonged to the claimed actor.
15+
Now authentication verification is performed before activity processing to
16+
prevent actor impersonation attacks. [[CVE-2025-54888]]
17+
18+
[CVE-2025-54888]: https://github.com/fedify-dev/fedify/security/advisories/GHSA-6jcc-xgcr-q3h4
19+
1120

1221
Version 1.3.19
1322
--------------

src/federation/handler.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
respondWithObject,
3636
respondWithObjectIfAcceptable,
3737
} from "./handler.ts";
38+
import { InboxListenerSet } from "./inbox.ts";
3839
import { MemoryKvStore } from "./kv.ts";
3940

4041
test("acceptsJsonLd()", () => {
@@ -1279,6 +1280,86 @@ test("respondWithObject()", async () => {
12791280
});
12801281
});
12811282

1283+
test("handleInbox() - authentication bypass vulnerability", async () => {
1284+
// This test reproduces the authentication bypass vulnerability where
1285+
// activities are processed before verifying the signing key belongs
1286+
// to the claimed actor
1287+
1288+
let processedActivity: Create | undefined;
1289+
const inboxListeners = new InboxListenerSet<void>();
1290+
inboxListeners.add(Create, (_ctx, activity) => {
1291+
// Track that the malicious activity was processed
1292+
processedActivity = activity;
1293+
});
1294+
1295+
// Create malicious activity claiming to be from victim actor
1296+
const maliciousActivity = new Create({
1297+
id: new URL("https://attacker.example.com/activities/malicious"),
1298+
actor: new URL("https://victim.example.com/users/alice"), // Impersonating victim
1299+
object: new Note({
1300+
id: new URL("https://attacker.example.com/notes/forged"),
1301+
attribution: new URL("https://victim.example.com/users/alice"),
1302+
content: "This is a forged message from the victim!",
1303+
}),
1304+
});
1305+
1306+
// Sign request with attacker's key (not victim's key)
1307+
const maliciousRequest = await signRequest(
1308+
new Request("https://example.com/", {
1309+
method: "POST",
1310+
body: JSON.stringify(await maliciousActivity.toJsonLd()),
1311+
}),
1312+
rsaPrivateKey3, // Attacker's private key
1313+
rsaPublicKey3.id!, // Attacker's public key ID
1314+
);
1315+
1316+
const maliciousContext = createRequestContext({
1317+
request: maliciousRequest,
1318+
url: new URL(maliciousRequest.url),
1319+
data: undefined,
1320+
documentLoader: mockDocumentLoader,
1321+
});
1322+
1323+
const actorDispatcher: ActorDispatcher<void> = (_ctx, identifier) => {
1324+
if (identifier !== "someone") return null;
1325+
return new Person({ name: "Someone" });
1326+
};
1327+
1328+
const response = await handleInbox(maliciousRequest, {
1329+
recipient: "someone",
1330+
context: maliciousContext,
1331+
inboxContextFactory(_activity) {
1332+
return createInboxContext({ ...maliciousContext, recipient: "someone" });
1333+
},
1334+
kv: new MemoryKvStore(),
1335+
kvPrefixes: {
1336+
activityIdempotence: ["_fedify", "activityIdempotence"],
1337+
publicKey: ["_fedify", "publicKey"],
1338+
},
1339+
actorDispatcher,
1340+
inboxListeners,
1341+
onNotFound: () => new Response("Not found", { status: 404 }),
1342+
signatureTimeWindow: { minutes: 5 },
1343+
skipSignatureVerification: false,
1344+
});
1345+
1346+
// The vulnerability: Even though the response is 401 (unauthorized),
1347+
// the malicious activity was already processed by routeActivity()
1348+
assertEquals(response.status, 401);
1349+
assertEquals(await response.text(), "The signer and the actor do not match.");
1350+
1351+
assertEquals(
1352+
processedActivity,
1353+
undefined,
1354+
`SECURITY VULNERABILITY: Malicious activity with mismatched signature was processed! ` +
1355+
`Activity ID: ${processedActivity?.id?.href}, ` +
1356+
`Claimed actor: ${processedActivity?.actorId?.href}`,
1357+
);
1358+
1359+
// If we reach here, the vulnerability is fixed - activities with mismatched
1360+
// signatures are properly rejected before processing
1361+
});
1362+
12821363
test("respondWithObjectIfAcceptable", async () => {
12831364
let request = new Request("https://example.com/", {
12841365
headers: { Accept: "application/activity+json" },

src/federation/handler.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -649,20 +649,6 @@ async function handleInboxInternal<TContextData>(
649649
span.setAttribute("activitypub.activity.id", activity.id.href);
650650
}
651651
span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
652-
const routeResult = await routeActivity({
653-
context: ctx,
654-
json,
655-
activity,
656-
recipient,
657-
inboxListeners,
658-
inboxContextFactory,
659-
inboxErrorHandler,
660-
kv,
661-
kvPrefixes,
662-
queue,
663-
span,
664-
tracerProvider,
665-
});
666652
if (
667653
httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)
668654
) {
@@ -685,6 +671,20 @@ async function handleInboxInternal<TContextData>(
685671
headers: { "Content-Type": "text/plain; charset=utf-8" },
686672
});
687673
}
674+
const routeResult = await routeActivity({
675+
context: ctx,
676+
json,
677+
activity,
678+
recipient,
679+
inboxListeners,
680+
inboxContextFactory,
681+
inboxErrorHandler,
682+
kv,
683+
kvPrefixes,
684+
queue,
685+
span,
686+
tracerProvider,
687+
});
688688
if (routeResult === "alreadyProcessed") {
689689
return new Response(
690690
`Activity <${activity.id}> has already been processed.`,

0 commit comments

Comments
 (0)