@@ -35,6 +35,7 @@ import {
3535 respondWithObject ,
3636 respondWithObjectIfAcceptable ,
3737} from "./handler.ts" ;
38+ import { InboxListenerSet } from "./inbox.ts" ;
3839import { MemoryKvStore } from "./kv.ts" ;
3940
4041test ( "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+
12821363test ( "respondWithObjectIfAcceptable" , async ( ) => {
12831364 let request = new Request ( "https://example.com/" , {
12841365 headers : { Accept : "application/activity+json" } ,
0 commit comments