Skip to content
GHSA Cr64 H6xp 9g2j

GHSA Cr64 H6xp 9g2j

github.com • October 26, 2026

The ActivityPub outbox sync handler ( ActivityPubService::syncActorPosts ) processes remote activities (Create, Update, Delete) and performs garbage collection of mirrored entries without any per-activity authorization checks. The parallel inbox handler ( processActivity ) enforces assertOwns() and sameHost() on every mutation, but the sync handler reaches the same EntryManager::delete , update , and create sinks with no equivalent guards. A malicious ActivityPub server operator can cause a YesWiki administrator (the confused deputy) to delete, modify, or create wiki entries belonging to other federated actors by returning a poisoned outbox during a routine sync operation.

The fix for GHSA-rm6r-grfg-4v78 (commit 6431f3c ) added assertOwns() and sameHost() authorization checks to the processActivity() inbox handler but did not apply the same guards to syncActorPosts() , which reaches identical destructive sinks.

An attacker who operates a malicious ActivityPub server can cause a YesWiki admin to unknowingly execute cross-actor entry manipulation by syncing with the attacker's actor. The admin must initiate the sync (or visit a page containing a CSRF link, since the sync action uses GET with no CSRF token). The attacker needs no privileges on the target YesWiki instance.

Demonstrated capabilities: (1) delete any mirrored entry by including a Delete activity with a cross-actor objectId; (2) overwrite any mirrored entry via Update; (3) create entries with fraudulent sourceUrls via Create; (4) mass-delete all mirrored entries from a shared hosting domain via the garbage-collection logic, which removes every local entry whose sourceUrl host matches the attacker's actor host but is absent from the attacker's outbox.

The attacker operates a server that returns the following actor document and outbox:

The outbox contains a Delete activity targeting a third-party entry:

When an admin syncs with the attacker's actor via ?vue=abonnements&action=sync&idformulaire={id}&actor= , the entry mirrored from is deleted without authorization.

Exposure: The sync action is reachable via BazarAction.php routing to FormController::syncActorPosts with GET query parameters vue=abonnements&action=sync&idformulaire={id}&actor={uri} . Admin authentication is required at FormController.php:269 .

Control: The attacker controls the entire outbox response, including activity types, object IDs, and object content. The objectId at line 343 flows directly from attacker-controlled JSON to the TripleStore lookup with no validation.

Path: Admin triggers sync; syncActorPosts fetches the actor document (line 320), retrieves the outbox via fetchAllOutboxItems (line 332), then iterates activities calling EntryManager::delete (line 347), create (line 369), or update (line 380) for each item.

Guard: The inbox handler processActivity enforces assertOwns() on Update (line 210) and Delete (line 224), and sameHost() on Create (line 196). syncActorPosts has zero equivalent checks on any operation.

Primitive: Delete any mirrored entry by sourceUrl; overwrite any mirrored entry content; create entries with fraudulent sourceUrls; mass-delete via host-scoped garbage collection.

Result: Cross-actor, cross-host entry manipulation. The attacker achieves the exact primitives that GHSA-rm6r-grfg-4v78 fixed for the inbox path but left unaddressed in the sync path.

GHSA-rm6r-grfg-4v78 (published 2026-09-01) fixed cross-actor authorization in the ActivityPub inbox handler. The fix commit 6431f3c added assertOwns() , sameHost() , and rememberOwner() to processActivity() . The commit diff shows zero changes to syncActorPosts() . The method was present from the initial ActivityPub commit and was never modified.

Side-by-side comparison of authorization in the two code paths reaching the same sinks:

The test ActivityPubServiceTest::testAnActorCannotDeleteAnEntryThatBelongsToAnother confirms the inbox path rejects cross-actor deletion. No equivalent test exists for the sync path.

Strongest disproof attempted: "Admin already controls everything, so no new capability is granted." This fails because (1) the project's own security model requires per-activity authorization even after identity verification, as shown by the inbox path guards; (2) the admin's intent is single-actor sync, not cross-actor manipulation; (3) the GC logic can cause mass collateral damage the admin cannot anticipate.

Ecosystem: Composer (Packagist)

Package: yeswiki/yeswiki

Confirmed affected range: >= 4.6.2, <= 4.6.7

Latest stable release checked: 4.6.7 (Packagist)

Fix status: not fixed

ActivityPub support was introduced in v4.6.2. The syncActorPosts function has existed without authorization checks since that version. In v4.6.2 through v4.6.6, both the inbox and sync paths lacked authorization (the original GHSA-rm6r-grfg-4v78 scope). In v4.6.7, the inbox path was fixed but the sync path was not. The prerelease v5.0.0-alpha1 (reorganized to src/Federation/Service/ActivityPubService.php ) also contains the unguarded sync handler.

Apply assertOwns() and sameHost() checks in syncActorPosts() for all activity types (Delete, Update, Create), mirroring the guards in processActivity() . The garbage-collection deletion (lines 387-402) should verify per-actor ownership rather than scoping deletion to all entries from the same host. Additionally, record ownership via rememberOwner() when creating entries through the sync path, so that subsequent authorization checks have owner data available.

As an interim mitigation, administrators can avoid syncing with untrusted remote ActivityPub actors. However, the trust determination is difficult because a previously trusted actor's server could be compromised. Adding a CSRF token requirement to the sync action would prevent the cross-site request forgery amplification vector.

Reported by zx (GitHub: @manus-pi ).

Extracted Entities