Follow-up from PR 63 (round-4 approval): the post-side DID-provenance guard added in PR 63 (Post::META_DID + Document::META_DID + the atmosphere_did_mismatch check in Publisher::delete_post) defends the disconnect → reconnect-to-different-DID flow for posts. The symmetric defense is missing for comments.
Threat
Site is connected to DID A. Author publishes a comment under DID A. The Comment transformer mints a TID and writes it to comment meta. Operator disconnects, reconnects to DID B. Later, the comment is unapproved or the parent post is permanently deleted. Publisher::delete_comment() or Publisher::delete_comment_by_tid() issues applyWrites#delete against DID B's repo for a TID that lives (if it lives) on DID A's repo. AT Proto returns 200 idempotently. Local meta clears. The reply on DID A is permanently orphaned.
This is the same threat model the round-2 work blocked for Publisher::delete_post, just on a path that wasn't widened.
Affected code
includes/transformer/class-comment.php — Comment::get_rkey() (around line 138-144) persists Comment::META_TID but no Comment::META_DID.
includes/class-publisher.php:1449 — Publisher::delete_comment( \WP_Comment $comment ) has no DID-mismatch check.
includes/class-publisher.php:1494 — Publisher::delete_comment_by_tid( string $tid ) has no DID-mismatch check.
Suggested shape
Mirror the Post/Document pattern from PR 63:
- Add
Comment::META_DID = '_atmosphere_bsky_comment_did'.
- In
Comment::get_rkey(), write META_DID = \Atmosphere\get_did() unconditionally on every call, BEFORE the META_TID write (matching the order Post and Document use so a partial failure leaves "DID set, no TID" rather than "TID set, no DID").
- In
Publisher::delete_comment(), read Comment::META_DID and compare against get_did(). Skip with WP_Error( 'atmosphere_did_mismatch', ... ) when origin DID is non-empty and differs from current. Preserve local meta on bail for manual remediation.
- For
Publisher::delete_comment_by_tid() — the path called from atmosphere_delete_comment_record cron after a comment row is already gone — the comment meta is unavailable at fire time. Either: (a) capture META_DID in the comment's on_before_delete handler and pass it through the cron event args alongside the TID, or (b) accept that this path is best-effort and document.
- Add the new meta key to
uninstall.php and clear_all_record_meta() equivalents.
- Backfill
Comment::META_DID = get_did() once on upgrade for comments with META_TID but no META_DID — analogous concern to the legacy-post backfill noted in the P3 polish list for PR 63.
- Tests:
atmosphere_did_mismatch happy path, legacy-fallback (no META_DID present), and ambient retry-after-reconnect-to-original-DID.
Priority
P2 — bounded by the specific account-rotation sequence and the fact that the orphaned record is on a repo the site no longer authenticates to. Not exploitable on its own; pairs naturally with the delete_post_by_tids follow-up.
Related
- Closed by PR 63 for the post path. This is the comment-shaped sibling.
- See also the
delete_post_by_tids DID-guard follow-up (sibling issue).
Follow-up from PR 63 (round-4 approval): the post-side DID-provenance guard added in PR 63 (
Post::META_DID+Document::META_DID+ theatmosphere_did_mismatchcheck inPublisher::delete_post) defends the disconnect → reconnect-to-different-DID flow for posts. The symmetric defense is missing for comments.Threat
Site is connected to DID A. Author publishes a comment under DID A. The Comment transformer mints a TID and writes it to comment meta. Operator disconnects, reconnects to DID B. Later, the comment is unapproved or the parent post is permanently deleted.
Publisher::delete_comment()orPublisher::delete_comment_by_tid()issuesapplyWrites#deleteagainst DID B's repo for a TID that lives (if it lives) on DID A's repo. AT Proto returns 200 idempotently. Local meta clears. The reply on DID A is permanently orphaned.This is the same threat model the round-2 work blocked for
Publisher::delete_post, just on a path that wasn't widened.Affected code
includes/transformer/class-comment.php—Comment::get_rkey()(around line 138-144) persistsComment::META_TIDbut noComment::META_DID.includes/class-publisher.php:1449—Publisher::delete_comment( \WP_Comment $comment )has no DID-mismatch check.includes/class-publisher.php:1494—Publisher::delete_comment_by_tid( string $tid )has no DID-mismatch check.Suggested shape
Mirror the Post/Document pattern from PR 63:
Comment::META_DID = '_atmosphere_bsky_comment_did'.Comment::get_rkey(), writeMETA_DID = \Atmosphere\get_did()unconditionally on every call, BEFORE the META_TID write (matching the order Post and Document use so a partial failure leaves "DID set, no TID" rather than "TID set, no DID").Publisher::delete_comment(), readComment::META_DIDand compare againstget_did(). Skip withWP_Error( 'atmosphere_did_mismatch', ... )when origin DID is non-empty and differs from current. Preserve local meta on bail for manual remediation.Publisher::delete_comment_by_tid()— the path called fromatmosphere_delete_comment_recordcron after a comment row is already gone — the comment meta is unavailable at fire time. Either: (a) capture META_DID in the comment'son_before_deletehandler and pass it through the cron event args alongside the TID, or (b) accept that this path is best-effort and document.uninstall.phpandclear_all_record_meta()equivalents.Comment::META_DID = get_did()once on upgrade for comments withMETA_TIDbut noMETA_DID— analogous concern to the legacy-post backfill noted in the P3 polish list for PR 63.atmosphere_did_mismatchhappy path, legacy-fallback (no META_DID present), and ambient retry-after-reconnect-to-original-DID.Priority
P2 — bounded by the specific account-rotation sequence and the fact that the orphaned record is on a repo the site no longer authenticates to. Not exploitable on its own; pairs naturally with the
delete_post_by_tidsfollow-up.Related
delete_post_by_tidsDID-guard follow-up (sibling issue).