fix(security): prevent notifications from blocked users [BCH-01-012]

Block notifications from bypassing the user blocking feature on iOS.

Vulnerabilities fixed:
1. Nostr public messages: checkForMentions() was called regardless of
   blocking status, allowing blocked users to trigger mention notifications
2. Noise-encrypted DMs: didReceiveNoisePayload() didn't check blocking
   before calling handlePrivateMessage(), allowing DM notifications

Changes:
- ChatViewModel+Nostr.swift: Add blocking check before checkForMentions()
  and sendHapticFeedback() in subscribeNostrEvent
- ChatViewModel.swift: Add isPeerBlocked() check in didReceiveNoisePayload
  before processing private messages
- Add NotificationBlockingTests.swift with 5 tests for blocking behavior

Security: Blocked users can no longer trigger notifications through any
message path (Nostr public, Nostr DM, or BLE Noise DM).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-01-12 11:08:09 -10:00
co-authored by Claude Opus 4.5
parent b84c36c6fa
commit 09818a02ed
3 changed files with 143 additions and 16 deletions
+20 -13
View File
@@ -3052,20 +3052,27 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
switch type {
case .privateMessage:
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
// BCH-01-012: Check blocking before processing private message to prevent notification bypass
if isPeerBlocked(peerID) {
SecureLogger.debug("🚫 Ignoring Noise payload from blocked peer: \(peerID)", category: .security)
return
}
let senderName = unifiedPeerService.getPeer(by: peerID)?.nickname ?? "Unknown"
let pmMentions = parseMentions(from: pm.content)
let msg = BitchatMessage(
id: pm.messageID,
sender: senderName,
content: pm.content,
timestamp: timestamp,
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: nickname,
senderPeerID: peerID,
mentions: pmMentions.isEmpty ? nil : pmMentions
)
let pmMentions = parseMentions(from: pm.content)
let msg = BitchatMessage(
id: pm.messageID,
sender: senderName,
content: pm.content,
timestamp: timestamp,
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: nickname,
senderPeerID: peerID,
mentions: pmMentions.isEmpty ? nil : pmMentions
)
handlePrivateMessage(msg)
// Send delivery ACK back over BLE
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
@@ -130,12 +130,21 @@ extension ChatViewModel {
mentions: mentions.isEmpty ? nil : mentions
)
Task { @MainActor in
// BCH-01-012: Check blocking before any notifications
// handlePublicMessage has its own blocking check but returns silently,
// so we must also guard checkForMentions to prevent notification bypass
let isBlocked = identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased())
handlePublicMessage(msg)
checkForMentions(msg)
sendHapticFeedback(for: msg)
// Only check mentions and send haptic if sender is not blocked
if !isBlocked {
checkForMentions(msg)
sendHapticFeedback(for: msg)
}
}
}
func subscribeGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) {
guard !deduplicationService.hasProcessedNostrEvent(giftWrap.id) else { return }
deduplicationService.recordNostrEvent(giftWrap.id)