Skip to content

Add DID-mismatch guard to delete_post_by_tids (hard-delete path) #71

@kraftbj

Description

@kraftbj

Follow-up from PR 63 (round-4 approval): the DID-mismatch guard added in Publisher::delete_post() (PR 63 round-2 commit dc5f926) defends the disconnect → reconnect-to-different-DID flow for the soft-cleanup path. The hard-delete path is uncovered.

Threat

Site is connected to DID A. Author publishes post 42 under DID A. Post::META_TID and Post::META_DID = A are persisted. Operator disconnects, reconnects to DID B. Later, the post is permanently deleted (Trash → Delete Permanently, or wp post delete --force). Atmosphere::on_before_delete() captures TIDs and schedules atmosphere_delete_records — by the time that cron fires, the post row and its meta are gone. Publisher::delete_post_by_tids() builds the applyWrites#delete batch from the captured TIDs and issues against DID B. AT Proto returns 200 idempotently. The record on DID A is permanently orphaned.

Same threat model as delete_post(), but the cron event carries TIDs only — no post meta to read at fire time — so the guard delete_post() uses cannot be applied without plumbing.

Affected code

  • includes/class-atmosphere.phpAtmosphere::on_before_delete() (around line 575) reads Post::META_TID / Document::META_TID / comment TIDs and schedules atmosphere_delete_records with just those values.
  • includes/class-publisher.php:1199-1240Publisher::delete_post_by_tids( $bsky_tids, $doc_tid, $comment_tids ) builds the applyWrites batch and ships it, no DID-mismatch check.

Suggested shape

Capture provenance at schedule time, gate at fire time:

  1. In Atmosphere::on_before_delete(), read Post::META_DID and Document::META_DID alongside the TIDs.
  2. Extend the atmosphere_delete_records cron event signature to carry origin DIDs as a fourth array argument keyed by collection (e.g. array( 'bsky' => 'did:plc:A', 'doc' => 'did:plc:A', 'comments' => array( 'tid1' => 'did:plc:A', ... ) )). Existing schedulers pass an empty array for legacy entries already in the cron queue at upgrade time.
  3. Update Publisher::delete_post_by_tids() to accept the origin-DID map. For each per-collection delete in the writes array, compare its origin DID against get_did(). Skip writes whose origin DID is non-empty and differs from current; log skipped writes via log_cron_error() with an atmosphere_did_mismatch op tag.
  4. Return success for the writes that DID get issued; surface the skipped set in result data so the cleanup action's listeners can observe partial-success vs full-success vs full-skip.
  5. Backfill — same legacy-fallback concern as the post-side: pre-PR-63 hard-deletes already in the cron queue have no origin DID. Treat empty origin DID as "no provenance, fall through" to match the delete_post() legacy-fallback behavior, and rely on the upgrade-time META_DID backfill (P3 polish item) to populate provenance for future scheduled events.
  6. Tests: hard-delete after reconnect skips the wrong-DID writes; hard-delete before any DID rotation succeeds entirely; partial skip (bsky on A, doc on A, but comment on B somehow) cleanly skips the comment.

Priority

P2 — requires a specific account-rotation sequence + permanent-delete (rather than trash) to exploit. Pairs with the Comment::META_DID follow-up since they share a threat model and ideally land together.

Related

  • Closed by PR 63 for Publisher::delete_post(). This is the hard-delete-path sibling.
  • See also the Comment::META_DID follow-up for the parallel comment-side gap.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions