Coordinated Disclosure Timeline

Summary

Signal for iOS v8.2 is affected by a vulnerability (GHSL-2026-095) where an attacker can exploit an improper authorization check within the Admin Delete message handler, leading to unauthorized message deletion.

Project

Signal-iOS

Tested Version

v8.2

Details

Admin Delete Authorization Check After Destructive Action (GHSL-2026-095)

Admin Delete is a new group moderation feature intended to allow group administrators to delete unwanted messages. While at the moment of reporting it is not available in the UI, the BuildFlags.AdminDelete.receive flag is already enabled and handled in the publicly available v8.2 Signal app. Differently from Signal for Android it is not a server flag and the functionality cannot be turned off remotely without users updating their Signal. A malicious custom (or modified) client can craft and send a custom message packet with an Admin Delete request.

In AdminDeleteManager.tryToAdminDeleteMessage(), the destructive action TSMessage.remotelyDeleteMessage() [1] is called BEFORE the admin authorization check in insertAdminDelete() [2]. The admin check groupModel.membership.isFullMemberAndAdministrator(deleteAuthor) [3] occurs inside insertAdminDelete(). If the sender is NOT an admin, insertAdminDelete throws .invalidDelete, but remotelyDeleteMessage() has already called markMessageAsRemotelyDeleted() (lines 388/404/424), which destructively removes the message content via updateWithRemotelyDeletedAndRemoveRenderableContent. Since all these operations occur within the same DBWriteTransaction and the thrown error is caught by the caller at MessageReceiver.swift (which logs and returns nil without causing a transaction rollback), the destructive deletion is committed to the database.

    public func tryToAdminDeleteMessage(
...
    ) throws(TSMessage.RemoteDeleteError) {
        guard SDS.fitsInInt64(sentAtTimestamp) else {
            owsFailDebug("Unable to delete a message with invalid sentAtTimestamp: \(sentAtTimestamp)")
            throw .invalidDelete
        }


        if
            let threadUniqueId, let messageToDelete = InteractionFinder.findMessage(
...
            )
        {
            let allowDeleteTimeframe = RemoteConfig.current.adminDeleteMaxAgeInSeconds + .day
            let latestMessage = try TSMessage.remotelyDeleteMessage(                            // [1]
                messageToDelete,
                deleteAuthorAci: deleteAuthorAci,
                allowedDeleteTimeframeSeconds: allowDeleteTimeframe,
                serverTimestamp: serverTimestamp,
                transaction: transaction,
            )


            return try insertAdminDelete(                                                       // [2]
                groupThread: groupThread,
                interactionId: latestMessage.sqliteRowId!,
                deleteAuthor: deleteAuthorAci,
                tx: transaction,
            )
        } else {
            throw .deletedMessageMissing
        }
    }

    private func insertAdminDelete(
...
    ) throws(TSMessage.RemoteDeleteError) {
        guard
            let groupModel = groupThread.groupModel as? TSGroupModelV2,
            groupModel.membership.isFullMemberAndAdministrator(deleteAuthor)                   // [3]
        else {
            logger.error("Failed to process admin delete for non-admin")
            throw .invalidDelete
        }
...
    }

Attack Scenario:

  1. Attacker is a regular (non-admin) member of a Signal group.
  2. Attacker crafts and sends a protobuf message with an adminDelete field (SSKProtoDataMessageAdminDelete) containing:
    • targetAuthorAciBinary: The ACI of any group member whose message they want to delete
    • targetSentTimestamp: The timestamp of the target message
  3. The receiving client at MessageReceiver.swift processes the admin delete.
  4. preprocessDataMessage verifies the attacker is a full group member — this passes.
  5. tryToAdminDeleteMessage is called with deleteAuthorAci: envelope.sourceAci (the attacker’s cryptographically authenticated ACI).
  6. InteractionFinder.findMessage locates the target message.
  7. TSMessage.remotelyDeleteMessage is called, which checks time windows and then calls markMessageAsRemotelyDeleted, destructively removing the message content and all attachments from the local database.
  8. insertAdminDelete checks isFullMemberAndAdministrator(deleteAuthor) — this fails because the attacker is not an admin — throws .invalidDelete.
  9. The error is caught. The transaction commits with the message already deleted.

Impact

A non-admin group member can delete any other member’s messages (including admin messages) from all group members’ devices, as long as the target message is within the adminDeleteMaxAgeInSeconds + 1 day time window (default ~2 days). This bypasses the intended admin-only restriction on admin delete functionality.

CWEs

Credit

This issue was discovered with the GitHub Security Lab Taskflow Agent and verified by GHSL team member @JarLob (Jaroslav Lobačevski).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2026-095 in any communication regarding this issue.