Skip to content

Commit 7f5e8c6

Browse files
authored
Merge branch 'main' into fix/426
2 parents d08cc46 + cbc8aca commit 7f5e8c6

23 files changed

Lines changed: 12479 additions & 2057 deletions

File tree

CHANGES.md

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,43 @@ To be released.
1010

1111
### @fedify/fedify
1212

13+
- Implemented [FEP-fe34] origin-based security model to protect against
14+
content spoofing attacks and ensure secure federation practices. The
15+
security model enforces same-origin policy for ActivityPub objects and
16+
their properties, preventing malicious actors from impersonating content
17+
from other servers. [[#440]]
18+
19+
- Added `crossOrigin` option to Activity Vocabulary property accessors
20+
(`get*()` methods) with three security levels: `"ignore"` (default,
21+
logs warning and returns `null`), `"throw"` (throws error), and
22+
`"trust"` (bypasses checks).
23+
- Added `LookupObjectOptions.crossOrigin` option to `lookupObject()`
24+
function and `Context.lookupObject()` method for controlling
25+
cross-origin validation.
26+
- Embedded objects are now validated against their parent object's origin
27+
and only trusted when they share the same origin or are explicitly
28+
marked as trusted.
29+
- Property hydration now respects origin-based security, automatically
30+
performing remote fetches when embedded objects have different origins.
31+
- Internal trust tracking system maintains security context throughout
32+
object lifecycles (construction, cloning, and property access).
33+
34+
- Added `withIdempotency()` method to configure activity idempotency
35+
strategies for inbox processing. This addresses issue [#441] where
36+
activities with the same ID sent to different inboxes were incorrectly
37+
deduplicated globally instead of per-inbox. [[#441]]
38+
39+
- Added `IdempotencyStrategy` type.
40+
- Added `IdempotencyKeyCallback` type.
41+
- Added `InboxListenerSetters.withIdempotency()` method.
42+
- By default, `"per-origin"` strategy is used for backward compatibility.
43+
This will change to `"per-inbox"` in Fedify 2.0. We recommend
44+
explicitly setting the strategy to avoid unexpected behavior changes.
45+
1346
- Fixed handling of ActivityPub objects containing relative URLs. The
14-
Activity Vocabulary classes now properly resolve relative URLs when
15-
a `baseUrl` option is provided to `fromJsonLd()` method, improving
47+
Activity Vocabulary classes now automatically resolve relative URLs by
48+
inferring the base URL from the object's `@id` or document URL, eliminating
49+
the need for manual `baseUrl` specification in most cases. This improves
1650
interoperability with ActivityPub servers that emit relative URLs in
1751
properties like `icon.url` and `image.url`. [[#411], [#443] by Jiwon Kwon]
1852

@@ -72,6 +106,7 @@ To be released.
72106
Node.js's `--experimental-require-module` flag and resolves dual package
73107
hazard issues. [[#429], [#431]]
74108

109+
[FEP-fe34]: https://w3id.org/fep/fe34
75110
[FEP-5711]: https://w3id.org/fep/5711
76111
[OStatus 1.0 Draft 2]: https://www.w3.org/community/ostatus/wiki/images/9/93/OStatus_1.0_Draft_2.pdf
77112
[RFC 7033 Section 4.4.4.3]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3
@@ -87,6 +122,8 @@ To be released.
87122
[#411]: https://github.com/fedify-dev/fedify/issues/411
88123
[#429]: https://github.com/fedify-dev/fedify/issues/429
89124
[#431]: https://github.com/fedify-dev/fedify/pull/431
125+
[#440]: https://github.com/fedify-dev/fedify/issues/440
126+
[#441]: https://github.com/fedify-dev/fedify/issues/441
90127
[#443]: https://github.com/fedify-dev/fedify/pull/443
91128

92129
### @fedify/cli
@@ -230,7 +267,7 @@ Released on September 17, 2025.
230267
Version 1.8.10
231268
--------------
232269

233-
Released on Steptember 17, 2025.
270+
Released on September 17, 2025.
234271

235272
### @fedify/fedify
236273

@@ -5197,4 +5234,7 @@ Version 0.1.0
51975234
Initial release. Released on March 8, 2024.
51985235

51995236
<!-- cSpell: ignore Dogeon Fabien Wressell Emelia Fróði Karlsson -->
5200-
<!-- cSpell: ignore Hana Heesun Kyunghee Jiyu Revath Kumar -->
5237+
<!-- cSpell: ignore Hana Heesun Kyunghee Jiyu Revath Kumar Jaeyeol -->
5238+
<!-- cSpell: ignore Jiwon Kwon Hyeonseo Chanhaeng Hasang Hyunchae KeunHyeong -->
5239+
<!-- cSpell: ignore Jang Hanarae ByeongJun Subin -->
5240+
<!-- cSpell: ignore Wayst Konsole Ghostty Aplc -->

FEDERATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Supported FEPs
3131
- [FEP-8b32][]: Object Integrity Proofs
3232
- [FEP-521a][]: Representing actor's public keys
3333
- [FEP-5feb][]: Search indexing consent for actors
34-
- [FEP-c7d3][]: Ownership
34+
- [FEP-fe34][]: Origin-based security model
3535
- [FEP-c0e0][]: Emoji reactions
3636
- [FEP-e232][]: Object Links
3737

@@ -42,7 +42,7 @@ Supported FEPs
4242
[FEP-8b32]: https://w3id.org/fep/8b32
4343
[FEP-521a]: https://w3id.org/fep/521a
4444
[FEP-5feb]: https://w3id.org/fep/5feb
45-
[FEP-c7d3]: https://w3id.org/fep/c7d3
45+
[FEP-fe34]: https://w3id.org/fep/fe34
4646
[FEP-c0e0]: https://w3id.org/fep/c0e0
4747
[FEP-e232]: https://w3id.org/fep/e232
4848

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"keypair",
5454
"langstr",
5555
"Lemmy",
56+
"lifecycles",
5657
"litepub",
5758
"logtape",
5859
"lume",

docs/manual/context.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,44 @@ const note = await ctx.lookupObject(
412412
> `DocumentLoader`*](#getting-an-authenticated-documentloader)
413413
> section for details.
414414
415+
> [!CAUTION]
416+
> For security reasons, the `~Context.lookupObject()` method implements
417+
> origin-based validation following [FEP-fe34]. If the fetched JSON-LD
418+
> document contains an `@id` that has a different origin than the requested
419+
> URL, the method will return `null` by default to prevent content spoofing
420+
> attacks.
421+
>
422+
> For example, if you request `https://example.com/notes/123` but the fetched
423+
> document has `@id: "https://malicious.com/notes/456"`, the method will
424+
> refuse to return the object and log a warning instead.
425+
>
426+
> You can control this behavior using the `crossOrigin` option:
427+
>
428+
> ~~~~ typescript twoslash
429+
> import { type Context } from "@fedify/fedify";
430+
> const ctx = null as unknown as Context<void>;
431+
> // ---cut-before---
432+
> // Default behavior: return null for cross-origin objects (recommended)
433+
> const objectDefault = await ctx.lookupObject("https://example.com/notes/123");
434+
>
435+
> // Throw an error when encountering cross-origin objects
436+
> const objectStrict = await ctx.lookupObject(
437+
> "https://example.com/notes/123",
438+
> { crossOrigin: "throw" }
439+
> );
440+
>
441+
> // Bypass origin checks (not recommended, potential security risk)
442+
> const objectBypass = await ctx.lookupObject(
443+
> "https://example.com/notes/123",
444+
> { crossOrigin: "trust" }
445+
> );
446+
> ~~~~
447+
>
448+
> Only use `crossOrigin: "trust"` if you fully understand the security
449+
> implications and have implemented additional validation measures.
450+
451+
[FEP-fe34]: https://w3id.org/fep/fe34
452+
415453
416454
WebFinger lookups
417455
-----------------

docs/manual/inbox.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,96 @@ duplicate retry mechanisms and leverages the backend's optimized retry features.
384384
[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis
385385

386386

387+
Activity idempotency
388+
--------------------
389+
390+
*This API is available since Fedify 1.9.0.*
391+
392+
In ActivityPub, the same activity might be delivered multiple times to your
393+
inbox for various reasons, such as network failures, server restarts, or
394+
federation protocol retries. To prevent processing the same activity multiple
395+
times, Fedify provides idempotency mechanisms that detect and skip duplicate
396+
activities.
397+
398+
### Idempotency strategies
399+
400+
Fedify supports three built-in idempotency strategies:
401+
402+
`"per-inbox"`
403+
: Activities are deduplicated per inbox. The same activity ID can be
404+
processed once per inbox, allowing the same activity to be delivered to
405+
multiple inboxes independently. This follows standard ActivityPub behavior
406+
and will be the default in Fedify 2.0.
407+
408+
`"per-origin"`
409+
: Activities are deduplicated per receiving server's origin. The same
410+
activity ID will be processed only once on each receiving server,
411+
but can be processed separately on different receiving servers.
412+
This was the default behavior in Fedify 1.x versions.
413+
414+
`"global"`
415+
: Activities are deduplicated globally across all inboxes and origins.
416+
The same activity ID will be processed only once, regardless of
417+
which inbox receives it or which server sent it.
418+
419+
You can configure the idempotency strategy using the
420+
`~InboxListenerSetters.withIdempotency()` method:
421+
422+
~~~~ typescript twoslash
423+
import { type Federation, Follow } from "@fedify/fedify";
424+
const federation = null as unknown as Federation<void>;
425+
// ---cut-before---
426+
federation
427+
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
428+
.withIdempotency("per-inbox") // Standard ActivityPub behavior
429+
.on(Follow, async (ctx, follow) => {
430+
// Handle the follow activity
431+
});
432+
~~~~
433+
434+
> [!WARNING]
435+
> If you don't explicitly configure an idempotency strategy, Fedify currently
436+
> uses `"per-origin"` as the default for backward compatibility. However, this
437+
> default will change to `"per-inbox"` in Fedify 2.0. We recommend explicitly
438+
> setting the strategy to avoid unexpected behavior changes.
439+
440+
### Custom idempotency strategy
441+
442+
If the built-in strategies don't meet your needs, you can implement a custom
443+
idempotency strategy by providing a callback function. The callback receives
444+
the inbox context and the activity, and should return a unique cache key for
445+
the activity, or `null` to skip idempotency checking for that activity:
446+
447+
~~~~ typescript twoslash
448+
import { type Federation, Follow } from "@fedify/fedify";
449+
const federation = null as unknown as Federation<void>;
450+
// ---cut-before---
451+
federation
452+
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
453+
.withIdempotency(async (ctx, activity) => {
454+
// Skip idempotency for Follow activities
455+
if (activity instanceof Follow) return null;
456+
457+
// Use per-inbox strategy for other activities
458+
const inboxId
459+
= ctx.recipient == null
460+
? "shared"
461+
: `actor\n${ctx.recipient}`;
462+
return `${ctx.origin}\n${activity.id?.href}\n${inboxId}`;
463+
})
464+
.on(Follow, async (ctx, follow) => {
465+
// This Follow activity will not be deduplicated
466+
});
467+
~~~~
468+
469+
### Idempotency cache
470+
471+
Processed activities are cached for 24 hours to detect duplicates. The cache
472+
uses the same [key–value store](./kv.md) that you provided to
473+
the `createFederation()` function. Cache keys are automatically namespaced to
474+
avoid conflicts with other data.
475+
476+
387477
Error handling
388478
--------------
389479

docs/manual/vocab.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,94 @@ corresponding TypeScript types:
495495
[`CryptoKey`]: https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey
496496

497497

498+
Origin-based security model
499+
---------------------------
500+
501+
*This section is applicable since Fedify 1.9.0.*
502+
503+
Fedify implements an origin-based security model following [FEP-fe34] to protect
504+
against content spoofing attacks and maintain secure federation practices.
505+
This security model ensures that objects and their properties respect origin
506+
boundaries, preventing malicious actors from impersonating content from other
507+
servers.
508+
509+
[FEP-fe34]: https://w3id.org/fep/fe34
510+
511+
### Same-origin policy for properties
512+
513+
When accessing properties of ActivityPub objects, Fedify enforces same-origin
514+
policy rules. Even if an object appears to be embedded in the JSON-LD
515+
representation, property accessors will automatically perform hydration (remote
516+
fetching) if the embedded object's `@id` has a different origin than its parent
517+
object.
518+
519+
For example, consider this JSON-LD representation:
520+
521+
~~~~ json
522+
{
523+
"@context": "https://www.w3.org/ns/activitystreams",
524+
"type": "Create",
525+
"id": "https://example.com/activities/123",
526+
"actor": "https://example.com/users/alice",
527+
"object": {
528+
"type": "Note",
529+
"id": "https://different-origin.com/notes/456",
530+
"content": "This is from a different origin"
531+
}
532+
}
533+
~~~~
534+
535+
In this case, when you access the `object` property of the `Create` activity,
536+
Fedify will not trust the embedded `Note` object because its `@id` has a
537+
different origin (`different-origin.com`) than the parent activity's origin
538+
(`example.com`). Instead, it will fetch the `Note` object directly from
539+
`https://different-origin.com/notes/456` to verify its authenticity.
540+
541+
### Controlling origin checks
542+
543+
You can control this behavior using the `crossOrigin` option when calling
544+
property accessors:
545+
546+
~~~~ typescript twoslash
547+
import { Create } from "@fedify/fedify";
548+
const create = {} as unknown as Create;
549+
// ---cut-before---
550+
// Default behavior: ignore untrusted embedded objects (recommended)
551+
const objectDefault = await create.getObject();
552+
553+
// Throw an error when encountering cross-origin objects
554+
const objectStrict = await create.getObject({ crossOrigin: "throw" });
555+
556+
// Bypass origin checks (not recommended, potential security risk)
557+
const objectBypass = await create.getObject({ crossOrigin: "trust" });
558+
~~~~
559+
560+
The `crossOrigin` option accepts the following values:
561+
562+
`"ignore"` (default)
563+
: Ignore untrusted embedded objects and fetch from origin
564+
565+
`"throw"`
566+
: Throw an error when encountering cross-origin embedded objects
567+
568+
`"trust"`
569+
: Trust embedded objects regardless of origin (⚠️ security risk)
570+
571+
> [!WARNING]
572+
> Using `crossOrigin: "trust"` can expose your application to security
573+
> vulnerabilities, including content spoofing attacks. Only use this option
574+
> if you fully understand the security implications and have implemented
575+
> additional validation measures.
576+
577+
### Trust tracking
578+
579+
Internally, Fedify maintains trust information for each property value. Objects
580+
that are constructed locally, fetched directly from their authoritative source,
581+
or explicitly validated are marked as trusted. This trust information is used
582+
to determine whether property accessors need to perform additional validation
583+
or fetching.
584+
585+
498586
Extending the vocabulary
499587
------------------------
500588

0 commit comments

Comments
 (0)