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
+69
View File
@@ -1155,6 +1155,9 @@ final class BLEService: NSObject {
return return
} }
// BCH-01-002: Enforce storage quota before saving
enforceIncomingFilesQuota(reservingBytes: filePacket.content.count)
let fallbackExt = mime.defaultExtension let fallbackExt = mime.defaultExtension
let subdirectory: String let subdirectory: String
switch mime.category { 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() { private func sendLeave() {
SecureLogger.debug("👋 Sending leave announcement", category: .session) SecureLogger.debug("👋 Sending leave announcement", category: .session)
let packet = BitchatPacket( let packet = BitchatPacket(
+7
View File
@@ -58,6 +58,10 @@ protocol Transport: AnyObject {
// QR verification (optional for transports) // QR verification (optional for transports)
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data) func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
func sendVerifyResponse(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 { extension Transport {
@@ -70,6 +74,9 @@ extension Transport {
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) { func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
sendMessage(content, mentions: mentions) sendMessage(content, mentions: mentions)
} }
func acceptPendingFile(id: String) -> URL? { nil }
func declinePendingFile(id: String) {}
} }
protocol TransportPeerEventsDelegate: AnyObject { protocol TransportPeerEventsDelegate: AnyObject {