Merge pull request #942 from permissionlesstech/fix/BCH-01-002-file-storage-dos

fix BCH-01-002: prevent DoS via file storage exhaustion
This commit is contained in:
jack
2026-01-12 15:27:51 -10:00
committed by GitHub
2 changed files with 79 additions and 3 deletions
+72 -3
View File
@@ -550,7 +550,7 @@ final class BLEService: NSObject {
func emergencyDisconnectAll() {
stopServices()
// Clear all sessions and peers
let cancelledTransfers: [(id: String, items: [DispatchWorkItem])] = collectionsQueue.sync(flags: .barrier) {
let entries = activeTransfers.map { ($0.key, $0.value.workItems) }
@@ -565,10 +565,10 @@ final class BLEService: NSObject {
entry.items.forEach { $0.cancel() }
TransferProgressManager.shared.cancel(id: entry.id)
}
// Clear processed messages
messageDeduplicator.reset()
// Clear peripheral references
peripherals.removeAll()
peerToPeripheralUUID.removeAll()
@@ -1155,6 +1155,9 @@ final class BLEService: NSObject {
return
}
// BCH-01-002: Enforce storage quota before saving
enforceIncomingFilesQuota(reservingBytes: filePacket.content.count)
let fallbackExt = mime.defaultExtension
let subdirectory: String
switch mime.category {
@@ -1390,6 +1393,72 @@ final class BLEService: NSObject {
}
}
// MARK: - Storage Quota Management (BCH-01-002)
/// Maximum total storage for incoming files (100 MB)
private static let incomingFilesQuota: Int64 = 100 * 1024 * 1024
/// Enforces storage quota for incoming files by deleting oldest files when quota is exceeded.
/// Call before saving a new incoming file.
private func enforceIncomingFilesQuota(reservingBytes: Int) {
do {
let base = try applicationFilesDirectory()
let incomingDirs = [
base.appendingPathComponent("voicenotes/incoming", isDirectory: true),
base.appendingPathComponent("images/incoming", isDirectory: true),
base.appendingPathComponent("files/incoming", isDirectory: true)
]
// Gather all incoming files with their sizes and modification dates
var allFiles: [(url: URL, size: Int64, modified: Date)] = []
let fileManager = FileManager.default
for dir in incomingDirs {
guard fileManager.fileExists(atPath: dir.path) else { continue }
guard let contents = try? fileManager.contentsOfDirectory(
at: dir,
includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey],
options: [.skipsHiddenFiles]
) else { continue }
for fileURL in contents {
guard let attrs = try? fileURL.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey]),
let size = attrs.fileSize,
let modified = attrs.contentModificationDate else { continue }
allFiles.append((url: fileURL, size: Int64(size), modified: modified))
}
}
// Calculate current usage
let currentUsage = allFiles.reduce(0) { $0 + $1.size }
let targetUsage = Self.incomingFilesQuota - Int64(reservingBytes)
guard currentUsage > targetUsage else { return }
// Sort by modification date (oldest first) and delete until under quota
let sortedFiles = allFiles.sorted { $0.modified < $1.modified }
var freedSpace: Int64 = 0
let needToFree = currentUsage - targetUsage
for file in sortedFiles {
guard freedSpace < needToFree else { break }
do {
try fileManager.removeItem(at: file.url)
freedSpace += file.size
SecureLogger.debug("🗑️ BCH-01-002: Deleted old incoming file to free space: \(file.url.lastPathComponent)", category: .security)
} catch {
SecureLogger.warning("⚠️ Failed to delete old file for quota: \(error)", category: .security)
}
}
if freedSpace > 0 {
SecureLogger.info("📊 BCH-01-002: Freed \(ByteCountFormatter.string(fromByteCount: freedSpace, countStyle: .file)) to stay within incoming files quota", category: .security)
}
} catch {
SecureLogger.warning("⚠️ Could not enforce storage quota: \(error)", category: .security)
}
}
private func sendLeave() {
SecureLogger.debug("👋 Sending leave announcement", category: .session)
let packet = BitchatPacket(
+7
View File
@@ -58,6 +58,10 @@ protocol Transport: AnyObject {
// QR verification (optional for transports)
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
// Pending file management (BCH-01-002: files held in memory until user accepts)
func acceptPendingFile(id: String) -> URL?
func declinePendingFile(id: String)
}
extension Transport {
@@ -70,6 +74,9 @@ extension Transport {
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
sendMessage(content, mentions: mentions)
}
func acceptPendingFile(id: String) -> URL? { nil }
func declinePendingFile(id: String) {}
}
protocol TransportPeerEventsDelegate: AnyObject {