mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:05:19 +00:00
Harden Nostr tag handling
This commit is contained in:
@@ -501,6 +501,10 @@ struct NostrEvent: Codable {
|
|||||||
throw NostrError.invalidEvent
|
throw NostrError.invalidEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard Self.isWithinInboundTagLimits(tags) else {
|
||||||
|
throw NostrError.invalidEvent
|
||||||
|
}
|
||||||
|
|
||||||
self.id = dict["id"] as? String ?? ""
|
self.id = dict["id"] as? String ?? ""
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.created_at = createdAt
|
self.created_at = createdAt
|
||||||
@@ -509,6 +513,17 @@ struct NostrEvent: Codable {
|
|||||||
self.content = content
|
self.content = content
|
||||||
self.sig = dict["sig"] as? String
|
self.sig = dict["sig"] as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func isWithinInboundTagLimits(_ tags: [[String]]) -> Bool {
|
||||||
|
guard tags.count <= TransportConfig.nostrMaxEventTags else { return false }
|
||||||
|
|
||||||
|
for tag in tags {
|
||||||
|
guard tag.count <= TransportConfig.nostrMaxEventTagValues else { return false }
|
||||||
|
guard tag.allSatisfy({ $0.utf8.count <= TransportConfig.nostrMaxEventTagValueBytes }) else { return false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func sign(with key: P256K.Schnorr.PrivateKey) throws -> NostrEvent {
|
func sign(with key: P256K.Schnorr.PrivateKey) throws -> NostrEvent {
|
||||||
let (eventId, eventIdHash) = try calculateEventId()
|
let (eventId, eventIdHash) = try calculateEventId()
|
||||||
|
|||||||
@@ -963,6 +963,7 @@ private enum ParsedInbound {
|
|||||||
|
|
||||||
init?(_ message: URLSessionWebSocketTask.Message) {
|
init?(_ message: URLSessionWebSocketTask.Message) {
|
||||||
guard let data = message.data,
|
guard let data = message.data,
|
||||||
|
data.count <= TransportConfig.nostrMaxInboundMessageBytes,
|
||||||
let array = try? JSONSerialization.jsonObject(with: data) as? [Any],
|
let array = try? JSONSerialization.jsonObject(with: data) as? [Any],
|
||||||
array.count >= 2,
|
array.count >= 2,
|
||||||
let type = array[0] as? String else {
|
let type = array[0] as? String else {
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ enum TransportConfig {
|
|||||||
static let nostrGeohashSampleLimit: Int = 100
|
static let nostrGeohashSampleLimit: Int = 100
|
||||||
static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400
|
static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400
|
||||||
|
|
||||||
|
// Nostr inbound event safety limits
|
||||||
|
static let nostrMaxInboundMessageBytes: Int = 256 * 1024
|
||||||
|
static let nostrMaxEventTags: Int = 64
|
||||||
|
static let nostrMaxEventTagValues: Int = 16
|
||||||
|
static let nostrMaxEventTagValueBytes: Int = 1024
|
||||||
|
|
||||||
// Nostr helpers
|
// Nostr helpers
|
||||||
static let nostrShortKeyDisplayLength: Int = 8
|
static let nostrShortKeyDisplayLength: Int = 8
|
||||||
static let nostrConvKeyPrefixLength: Int = 16
|
static let nostrConvKeyPrefixLength: Int = 16
|
||||||
|
|||||||
@@ -262,8 +262,10 @@ final class ChatNostrCoordinator {
|
|||||||
if viewModel.deduplicationService.hasProcessedNostrEvent(event.id) { return }
|
if viewModel.deduplicationService.hasProcessedNostrEvent(event.id) { return }
|
||||||
viewModel.deduplicationService.recordNostrEvent(event.id)
|
viewModel.deduplicationService.recordNostrEvent(event.id)
|
||||||
|
|
||||||
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
|
SecureLogger.debug(
|
||||||
SecureLogger.debug("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)", category: .session)
|
"GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tagCount=\(event.tags.count)",
|
||||||
|
category: .session
|
||||||
|
)
|
||||||
|
|
||||||
if viewModel.identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
if viewModel.identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -241,6 +241,51 @@ struct NostrProtocolTests {
|
|||||||
#expect(!signed.isValidSignature())
|
#expect(!signed.isValidSignature())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func inboundNostrEventRejectsTooManyTags() throws {
|
||||||
|
var eventDict = Self.validInboundEventDict()
|
||||||
|
eventDict["tags"] = Array(
|
||||||
|
repeating: ["g", "u4pruyd"],
|
||||||
|
count: TransportConfig.nostrMaxEventTags + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(throws: NostrError.invalidEvent) {
|
||||||
|
_ = try NostrEvent(from: eventDict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func inboundNostrEventRejectsTooManyTagValues() throws {
|
||||||
|
var eventDict = Self.validInboundEventDict()
|
||||||
|
eventDict["tags"] = [Array(
|
||||||
|
repeating: "value",
|
||||||
|
count: TransportConfig.nostrMaxEventTagValues + 1
|
||||||
|
)]
|
||||||
|
|
||||||
|
#expect(throws: NostrError.invalidEvent) {
|
||||||
|
_ = try NostrEvent(from: eventDict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func inboundNostrEventRejectsOversizedTagValues() throws {
|
||||||
|
var eventDict = Self.validInboundEventDict()
|
||||||
|
eventDict["tags"] = [[
|
||||||
|
"g",
|
||||||
|
String(repeating: "a", count: TransportConfig.nostrMaxEventTagValueBytes + 1)
|
||||||
|
]]
|
||||||
|
|
||||||
|
#expect(throws: NostrError.invalidEvent) {
|
||||||
|
_ = try NostrEvent(from: eventDict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func inboundNostrEventAcceptsTagsWithinLimits() throws {
|
||||||
|
var eventDict = Self.validInboundEventDict()
|
||||||
|
eventDict["tags"] = [["g", "u4pruyd"], ["t", "teleport"]]
|
||||||
|
|
||||||
|
let event = try NostrEvent(from: eventDict)
|
||||||
|
|
||||||
|
#expect(event.tags.count == 2)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func geohashNotesSingleFilter_encodesExpectedTagShape() throws {
|
@Test func geohashNotesSingleFilter_encodesExpectedTagShape() throws {
|
||||||
let since = Date(timeIntervalSince1970: 1_234_567)
|
let since = Date(timeIntervalSince1970: 1_234_567)
|
||||||
let filter = NostrFilter.geohashNotes("u4pruyd", since: since, limit: 42)
|
let filter = NostrFilter.geohashNotes("u4pruyd", since: since, limit: 42)
|
||||||
@@ -254,6 +299,18 @@ struct NostrProtocolTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
private static func validInboundEventDict() -> [String: Any] {
|
||||||
|
[
|
||||||
|
"id": String(repeating: "0", count: 64),
|
||||||
|
"pubkey": String(repeating: "1", count: 64),
|
||||||
|
"created_at": 1_234_567,
|
||||||
|
"kind": NostrProtocol.EventKind.ephemeralEvent.rawValue,
|
||||||
|
"tags": [["g", "u4pruyd"]],
|
||||||
|
"content": "hello",
|
||||||
|
"sig": String(repeating: "2", count: 128)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
private static func base64URLDecode(_ s: String) -> Data? {
|
private static func base64URLDecode(_ s: String) -> Data? {
|
||||||
var str = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
var str = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||||
let rem = str.count % 4
|
let rem = str.count % 4
|
||||||
|
|||||||
Reference in New Issue
Block a user