fix(security): block automatic metadata fetch for private IP addresses

This commit is contained in:
nimbold
2026-06-07 09:08:11 +03:30
parent 125a8b9e6d
commit 6552bde261
2 changed files with 38 additions and 5 deletions
+5 -4
View File
@@ -83,7 +83,7 @@ struct AddDownloadsView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Spacer() Spacer()
Button { Button {
refreshMetadata(for: linkText) refreshMetadata(for: linkText, isAutoFetch: false)
} label: { } label: {
Label("Refresh Metadata", systemImage: "arrow.clockwise") Label("Refresh Metadata", systemImage: "arrow.clockwise")
} }
@@ -422,7 +422,7 @@ struct AddDownloadsView: View {
try? await Task.sleep(for: .milliseconds(350)) try? await Task.sleep(for: .milliseconds(350))
guard !Task.isCancelled else { return } guard !Task.isCancelled else { return }
await MainActor.run { await MainActor.run {
refreshMetadata(for: text) refreshMetadata(for: text, isAutoFetch: true)
} }
} }
} }
@@ -445,7 +445,7 @@ struct AddDownloadsView: View {
controller.pendingReferer = nil controller.pendingReferer = nil
} }
private func refreshMetadata(for text: String) { private func refreshMetadata(for text: String, isAutoFetch: Bool) {
let urls = DownloadURLParser.parse(text) let urls = DownloadURLParser.parse(text)
metadataTask?.cancel() metadataTask?.cancel()
@@ -476,7 +476,8 @@ struct AddDownloadsView: View {
for: url, for: url,
settings: settings, settings: settings,
credentials: metadataCredentials(for: url), credentials: metadataCredentials(for: url),
transferOptions: transferOptions transferOptions: transferOptions,
isAutoFetch: isAutoFetch
) )
loaded.append(item) loaded.append(item)
await MainActor.run { await MainActor.run {
+33 -1
View File
@@ -35,7 +35,8 @@ enum DownloadMetadataFetcher {
for url: URL, for url: URL,
settings: AppSettings, settings: AppSettings,
credentials: DownloadCredentials? = nil, credentials: DownloadCredentials? = nil,
transferOptions: DownloadTransferOptions = DownloadTransferOptions() transferOptions: DownloadTransferOptions = DownloadTransferOptions(),
isAutoFetch: Bool = false
) async -> PendingDownload { ) async -> PendingDownload {
let initialName = FileClassifier.fileName(from: url) let initialName = FileClassifier.fileName(from: url)
let initialCategory = FileClassifier.category(forFileName: initialName) let initialCategory = FileClassifier.category(forFileName: initialName)
@@ -53,6 +54,11 @@ enum DownloadMetadataFetcher {
return pending return pending
} }
if isAutoFetch, let host = url.host, isPrivateHost(host) {
pending.state = .loaded
return pending
}
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = "HEAD" request.httpMethod = "HEAD"
request.timeoutInterval = 12 request.timeoutInterval = 12
@@ -122,6 +128,32 @@ enum DownloadMetadataFetcher {
} }
return nil return nil
} }
private static func isPrivateHost(_ host: String) -> Bool {
let h = host.lowercased()
if h == "localhost" || h.hasSuffix(".local") { return true }
if !h.contains(".") && !h.contains(":") { return true }
let parts = h.split(separator: ".")
if parts.count == 4, let first = Int(parts[0]), let second = Int(parts[1]) {
if first == 127 || first == 10 || (first == 192 && second == 168) {
return true
}
if first == 172 && (16...31).contains(second) {
return true
}
if first == 169 && second == 254 {
return true
}
}
if h.contains(":") {
if h == "[::1]" || h.hasPrefix("[fc") || h.hasPrefix("[fd") || h.hasPrefix("[fe8") || h.hasPrefix("[fe9") || h.hasPrefix("[fea") || h.hasPrefix("[feb") {
return true
}
}
return false
}
} }
enum ByteFormatter { enum ByteFormatter {